package remoteagentconfig import ( "bytes" "context" "errors" "fmt" "io/fs" "sort" "strconv" "strings" "text/template" "time" corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "edge-infra.dev/pkg/edge/info" "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config" "edge-infra.dev/pkg/sds/lib/os/file" ) var ( remoteAgentHostDataDir = "/host-data/remote-access-agent" remoteAgentSecretDir = remoteAgentHostDataDir remoteAgentDataDir = "/data/remote-access-agent" remoteAgentConfigFileName = "config.yaml" remoteAgentSFileName = "%d.adc.json" configFileMode = fs.FileMode(0644) secretFileMode = fs.FileMode(0644) secretNamespace = "sds" secretName = "remote-agent-configuration" configEntry = "config.yaml.tpl" adcEntry = "key.json" ) type Plugin struct{} type templateFields struct { TerminalID string StoreID string BannerID string Provider string CredentialsPath string } func (remoteAccessAgentPlugin Plugin) Reconcile(ctx context.Context, object client.Object, conf config.Config) error { secret, ok := object.(*corev1.Secret) if !ok { return nil } edgeInfo, err := conf.GetEdgeInfoConfig(ctx) if err != nil { return err } log := ctrl.LoggerFrom(ctx, "pluginName", "remoteagentconfig") ctrl.LoggerInto(ctx, log) // only reconcile the target secret if !isTargetSecret(secret) { return nil } terminalInfo, err := getTerminalInfo(ctx, conf) if err != nil { return err } // locate remote agent config dir fileHandler := file.New() return UpdateRemoteAgentConfig(ctx, secret, terminalInfo, edgeInfo, fileHandler) } func getTerminalInfo(ctx context.Context, conf config.Config) (map[string]string, error) { ienode, err := conf.GetHostIENode(ctx) if err != nil { return nil, fmt.Errorf("locating IENode: %w", err) } return map[string]string{"terminalID": ienode.ObjectMeta.Labels["node.ncr.com/terminal-id"]}, nil } func isTargetSecret(secret *corev1.Secret) bool { return secret.Name == secretName && secret.Namespace == secretNamespace } func UpdateRemoteAgentConfig(ctx context.Context, secret *corev1.Secret, terminalInfo map[string]string, edgeConf *info.EdgeInfo, fileHandler file.File) error { newSecret, err := getSecretData(secret) if err != nil { return err } adcFiles, err := updateADCJSON(ctx, newSecret, fileHandler) if err != nil { return err } recentADCidx := len(adcFiles) - 1 latestADC := adcFiles[recentADCidx] err = updateConfigyaml(ctx, secret, latestADC, terminalInfo, edgeConf, fileHandler) if err != nil { return err } // delete all but latest adc.json for _, file := range adcFiles[:recentADCidx] { err = fileHandler.Remove(remoteAgentSecretDir + "/" + file) if err != nil { return err } } return nil } func updateConfigyaml(ctx context.Context, secret *corev1.Secret, adcFilepath string, terminalInfo map[string]string, edgeConf *info.EdgeInfo, fileHandler file.File) error { conf, err := templateConfig(secret, adcFilepath, terminalInfo, edgeConf) if err != nil { return err } currentData, err := fileHandler.Read(remoteAgentHostDataDir + "/" + remoteAgentConfigFileName) if err != nil { if !errors.Is(err, fs.ErrNotExist) { return err } currentData = []byte{} } if bytes.Equal(conf, currentData) { return nil } ctrl.LoggerFrom(ctx).Info("Updating config.yaml data") return fileHandler.SafeWrite(remoteAgentHostDataDir, remoteAgentConfigFileName, conf, configFileMode) } // Returns the decoded adc.json from the secret func getSecretData(secret *corev1.Secret) ([]byte, error) { data, ok := secret.Data[adcEntry] if !ok || data == nil { return nil, fmt.Errorf("cannot find adc entry %s in secret %s", adcEntry, secret.Name) } return data, nil } // Returns a sorted list of all adc files in the config dir func sortedADCFiles(fileHandler file.File) ([]string, error) { allFiles, err := fileHandler.ReadDir(remoteAgentSecretDir) if err != nil { return nil, fmt.Errorf("listing adc entries: %w", err) } // Find latest adc in dir var adcFiles []struct { timestamp int64 filename string } // Filter the files by ones that follow the pattern `.adc.json` for _, file := range allFiles { if !strings.HasSuffix(file.Name(), ".adc.json") { continue } parts := strings.Split(file.Name(), ".") if len(parts) != 3 { continue } timestamp, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { continue } adcFiles = append(adcFiles, struct { timestamp int64 filename string }{ timestamp: timestamp, filename: file.Name(), }) } // Sort by timestamp sort.Slice(adcFiles, func(i, j int) bool { return adcFiles[i].timestamp < adcFiles[j].timestamp }) var adcFilenames []string for _, file := range adcFiles { adcFilenames = append(adcFilenames, file.filename) } return adcFilenames, nil } // Returns the data from the latest adc file on the disk. Returns an empty slice // if there is no file on disk func getCurrentADCData(adcFilenames []string, fileHandler file.File) ([]byte, error) { recentIdx := len(adcFilenames) - 1 if recentIdx < 0 { return []byte{}, nil } currentData, err := fileHandler.Read(remoteAgentSecretDir + "/" + adcFilenames[recentIdx]) if err != nil { if !errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("reading adc file: %w", err) } currentData = []byte{} } return currentData, nil } // Finds all existing secrets in the default configuration folder. Creates a new // secret with the given data when the latest secret does not match the given // data. Returns a slice of all secrets in the folder with the secret with the // given data at the end of the slice func updateADCJSON(ctx context.Context, secretData []byte, fileHandler file.File) ([]string, error) { adcFilenames, err := sortedADCFiles(fileHandler) if err != nil { return nil, err } currentData, err := getCurrentADCData(adcFilenames, fileHandler) if err != nil { return nil, err } if bytes.Equal(secretData, currentData) { return adcFilenames, nil } newFilename := fmt.Sprintf(remoteAgentSFileName, time.Now().Unix()) ctrl.LoggerFrom(ctx).Info("Updating adc secret", "adcFile", newFilename) err = fileHandler.SafeWrite(remoteAgentSecretDir, newFilename, secretData, secretFileMode) if err != nil { return nil, fmt.Errorf("writing adc file: %w", err) } return append(adcFilenames, newFilename), nil } func templateConfig(secret *corev1.Secret, adcFilepath string, terminalInfo map[string]string, edgeConf *info.EdgeInfo) ([]byte, error) { var res bytes.Buffer values := templateFields{ TerminalID: terminalInfo["terminalID"], StoreID: edgeConf.ClusterEdgeID, BannerID: edgeConf.BannerEdgeID, Provider: edgeConf.ProjectID, CredentialsPath: remoteAgentDataDir + "/" + adcFilepath, } data, ok := secret.Data[configEntry] if !ok || data == nil { return nil, fmt.Errorf("cannot find template %s in secret", configEntry) } template, err := template.New(remoteAgentConfigFileName + ".tpl").Parse(string(data)) if err != nil { return nil, err } err = template.Execute(&res, values) if err != nil { return nil, err } return res.Bytes(), nil }