github.com/argoproj/argo-cd/v3@v3.2.1/commitserver/commit/hydratorhelper.go (about) 1 package commit 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path/filepath" 8 "text/template" 9 10 "github.com/Masterminds/sprig/v3" 11 log "github.com/sirupsen/logrus" 12 "gopkg.in/yaml.v3" 13 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 14 15 "github.com/argoproj/argo-cd/v3/commitserver/apiclient" 16 "github.com/argoproj/argo-cd/v3/common" 17 appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 18 "github.com/argoproj/argo-cd/v3/util/hydrator" 19 "github.com/argoproj/argo-cd/v3/util/io" 20 ) 21 22 var sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance 23 24 const gitAttributesContents = `*/README.md linguist-generated=true 25 */hydrator.metadata linguist-generated=true` 26 27 func init() { 28 // Avoid allowing the user to learn things about the environment. 29 delete(sprigFuncMap, "env") 30 delete(sprigFuncMap, "expandenv") 31 delete(sprigFuncMap, "getHostByName") 32 } 33 34 // WriteForPaths writes the manifests, hydrator.metadata, and README.md files for each path in the provided paths. It 35 // also writes a root-level hydrator.metadata file containing the repo URL and dry SHA. 36 func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *appv1.RevisionMetadata, paths []*apiclient.PathDetails) error { //nolint:revive //FIXME(var-naming) 37 hydratorMetadata, err := hydrator.GetCommitMetadata(repoUrl, drySha, dryCommitMetadata) 38 if err != nil { 39 return fmt.Errorf("failed to retrieve hydrator metadata: %w", err) 40 } 41 42 // Write the top-level readme. 43 err = writeMetadata(root, "", hydratorMetadata) 44 if err != nil { 45 return fmt.Errorf("failed to write top-level hydrator metadata: %w", err) 46 } 47 48 // Write .gitattributes 49 err = writeGitAttributes(root) 50 if err != nil { 51 return fmt.Errorf("failed to write git attributes: %w", err) 52 } 53 54 for _, p := range paths { 55 hydratePath := p.Path 56 if hydratePath == "." { 57 hydratePath = "" 58 } 59 60 // Only create directory if path is not empty (root directory case) 61 if hydratePath != "" { 62 err = root.MkdirAll(hydratePath, 0o755) 63 if err != nil { 64 return fmt.Errorf("failed to create path: %w", err) 65 } 66 } 67 68 // Write the manifests 69 err = writeManifests(root, hydratePath, p.Manifests) 70 if err != nil { 71 return fmt.Errorf("failed to write manifests: %w", err) 72 } 73 74 // Write hydrator.metadata containing information about the hydration process. 75 hydratorMetadata := hydrator.HydratorCommitMetadata{ 76 Commands: p.Commands, 77 DrySHA: drySha, 78 RepoURL: repoUrl, 79 } 80 err = writeMetadata(root, hydratePath, hydratorMetadata) 81 if err != nil { 82 return fmt.Errorf("failed to write hydrator metadata: %w", err) 83 } 84 85 // Write README 86 err = writeReadme(root, hydratePath, hydratorMetadata) 87 if err != nil { 88 return fmt.Errorf("failed to write readme: %w", err) 89 } 90 } 91 return nil 92 } 93 94 // writeMetadata writes the metadata to the hydrator.metadata file. 95 func writeMetadata(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error { 96 hydratorMetadataPath := filepath.Join(dirPath, "hydrator.metadata") 97 f, err := root.Create(hydratorMetadataPath) 98 if err != nil { 99 return fmt.Errorf("failed to create hydrator metadata file: %w", err) 100 } 101 defer io.Close(f) 102 e := json.NewEncoder(f) 103 e.SetIndent("", " ") 104 // We don't need to escape HTML, because we're not embedding this JSON in HTML. 105 e.SetEscapeHTML(false) 106 err = e.Encode(metadata) 107 if err != nil { 108 return fmt.Errorf("failed to encode hydrator metadata: %w", err) 109 } 110 return nil 111 } 112 113 // writeReadme writes the readme to the README.md file. 114 func writeReadme(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error { 115 readmeTemplate, err := template.New("readme").Funcs(sprigFuncMap).Parse(manifestHydrationReadmeTemplate) 116 if err != nil { 117 return fmt.Errorf("failed to parse readme template: %w", err) 118 } 119 // Create writer to template into 120 // No need to use SecureJoin here, as the path is already sanitized. 121 readmePath := filepath.Join(dirPath, "README.md") 122 readmeFile, err := root.Create(readmePath) 123 if err != nil && !os.IsExist(err) { 124 return fmt.Errorf("failed to create README file: %w", err) 125 } 126 err = readmeTemplate.Execute(readmeFile, metadata) 127 closeErr := readmeFile.Close() 128 if closeErr != nil { 129 log.WithError(closeErr).Error("failed to close README file") 130 } 131 if err != nil { 132 return fmt.Errorf("failed to execute readme template: %w", err) 133 } 134 return nil 135 } 136 137 func writeGitAttributes(root *os.Root) error { 138 gitAttributesFile, err := root.Create(".gitattributes") 139 if err != nil { 140 return fmt.Errorf("failed to create git attributes file: %w", err) 141 } 142 143 defer func() { 144 err = gitAttributesFile.Close() 145 if err != nil { 146 log.WithFields(log.Fields{ 147 common.SecurityField: common.SecurityMedium, 148 common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor, 149 }).Errorf("error closing file %q: %v", gitAttributesFile.Name(), err) 150 } 151 }() 152 153 _, err = gitAttributesFile.WriteString(gitAttributesContents) 154 if err != nil { 155 return fmt.Errorf("failed to write git attributes: %w", err) 156 } 157 158 return nil 159 } 160 161 // writeManifests writes the manifests to the manifest.yaml file, truncating the file if it exists and appending the 162 // manifests in the order they are provided. 163 func writeManifests(root *os.Root, dirPath string, manifests []*apiclient.HydratedManifestDetails) error { 164 // If the file exists, truncate it. 165 // No need to use SecureJoin here, as the path is already sanitized. 166 manifestPath := filepath.Join(dirPath, "manifest.yaml") 167 168 file, err := root.OpenFile(manifestPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) 169 if err != nil { 170 return fmt.Errorf("failed to open manifest file: %w", err) 171 } 172 defer func() { 173 err := file.Close() 174 if err != nil { 175 log.WithError(err).Error("failed to close file") 176 } 177 }() 178 179 enc := yaml.NewEncoder(file) 180 defer func() { 181 err := enc.Close() 182 if err != nil { 183 log.WithError(err).Error("failed to close yaml encoder") 184 } 185 }() 186 enc.SetIndent(2) 187 188 for _, m := range manifests { 189 obj := &unstructured.Unstructured{} 190 err = json.Unmarshal([]byte(m.ManifestJSON), obj) 191 if err != nil { 192 return fmt.Errorf("failed to unmarshal manifest: %w", err) 193 } 194 err = enc.Encode(&obj.Object) 195 if err != nil { 196 return fmt.Errorf("failed to encode manifest: %w", err) 197 } 198 } 199 200 return nil 201 }