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  }