github.com/verrazzano/verrazzano@v1.7.0/tools/charts-manager/vcm/pkg/fs/fs.go (about)

     1  // Copyright (c) 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package fs
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	vzos "github.com/verrazzano/verrazzano/pkg/os"
    15  	"github.com/verrazzano/verrazzano/pkg/semver"
    16  	"github.com/verrazzano/verrazzano/tools/charts-manager/vcm/pkg/helm"
    17  	"github.com/verrazzano/verrazzano/tools/vz/pkg/helpers"
    18  	"gopkg.in/yaml.v3"
    19  )
    20  
    21  // cmdRunner needed for unit tests
    22  var runner vzos.CmdRunner = vzos.DefaultRunner{}
    23  
    24  // ChartFileSystem represents all the file operations that are performed for helm charts.
    25  type ChartFileSystem interface {
    26  	RearrangeChartDirectory(string, string, string) error
    27  	SaveUpstreamChart(string, string, string, string) error
    28  	SaveChartProvenance(string, *helm.ChartProvenance, string, string) error
    29  	GeneratePatchFile(string, string, string) (string, error)
    30  	GeneratePatchWithSourceDir(string, string, string, string) (string, error)
    31  	FindChartVersionToPatch(string, string, string) (string, error)
    32  	ApplyPatchFile(string, helpers.VZHelper, string, string, string) (bool, error)
    33  }
    34  
    35  // HelmChartFileSystem is the default implementation of ChartFileSystem.
    36  type HelmChartFileSystem struct{}
    37  
    38  // RearrangeChartDirectory moves the downloaded chart directory one level up in the supplied chartsDir so that the structure is
    39  // <chartsDir>/<chart>/<version>/<all chart files>.
    40  func (hfs HelmChartFileSystem) RearrangeChartDirectory(chartsDir string, chart string, targetVersion string) error {
    41  	pulledChartDir := fmt.Sprintf("%s/%s/%s/%s/", chartsDir, chart, targetVersion, chart)
    42  	targetChartDir := fmt.Sprintf("%s/%s/%s", chartsDir, chart, targetVersion)
    43  	cmd := exec.Command("cp", "-R", pulledChartDir, targetChartDir)
    44  	_, _, err := runner.Run(cmd)
    45  	if err != nil {
    46  		return err
    47  	}
    48  
    49  	err = os.RemoveAll(pulledChartDir)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	return nil
    54  }
    55  
    56  // SaveUpstreamChart copies the original chart to <chartsDir>/../provenance/<chart>/upstreams/<version> so that upstream is
    57  // persisted.
    58  func (hfs HelmChartFileSystem) SaveUpstreamChart(chartsDir string, chart string, version string, targetVersion string) error {
    59  	upstreamDir := fmt.Sprintf("%s/../provenance/%s/upstreams/%s", chartsDir, chart, version)
    60  	err := os.RemoveAll(upstreamDir)
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	err = os.MkdirAll(upstreamDir, 0755)
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	chartDir := fmt.Sprintf("%s/%s/%s/", chartsDir, chart, targetVersion)
    71  	cmd := exec.Command("cp", "-R", chartDir, upstreamDir)
    72  	_, _, err = runner.Run(cmd)
    73  	return err
    74  }
    75  
    76  // SaveChartProvenance serializes the chartProvenance generated for a chart to <chartsDir>/../provenance/<chart>/<targetVersion>.yaml.
    77  func (hfs HelmChartFileSystem) SaveChartProvenance(chartsDir string, chartProvenance *helm.ChartProvenance, chart string, targetVersion string) error {
    78  	provenanceDir := fmt.Sprintf("%s/../provenance/%s", chartsDir, chart)
    79  	err := os.MkdirAll(provenanceDir, 0755)
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	provenanceFile := fmt.Sprintf("%s/%s.yaml", provenanceDir, targetVersion)
    85  	out, err := yaml.Marshal(chartProvenance)
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	return os.WriteFile(provenanceFile, out, 0600)
    91  }
    92  
    93  // GeneratePatchFile diffs between the chart/version present in a chart directory against its upstream chart and generates
    94  // a patch file.
    95  func (hfs HelmChartFileSystem) GeneratePatchFile(chartsDir string, chart string, version string) (string, error) {
    96  	provenanceFile := fmt.Sprintf("%s/../provenance/%s/%s.yaml", chartsDir, chart, version)
    97  	if _, err := os.Stat(provenanceFile); err != nil {
    98  		return "", fmt.Errorf("provenance file %s not found, error %v", provenanceFile, err)
    99  	}
   100  
   101  	in, err := os.ReadFile(provenanceFile)
   102  	if err != nil {
   103  		return "", fmt.Errorf("unable to read provenance file %s, error %v", provenanceFile, err)
   104  	}
   105  
   106  	chartProvenance := helm.ChartProvenance{}
   107  	err = yaml.Unmarshal(in, &chartProvenance)
   108  	if err != nil {
   109  		return "", fmt.Errorf("unable to parse provenance file %s, error %v", provenanceFile, err)
   110  	}
   111  
   112  	return hfs.GeneratePatchWithSourceDir(chartsDir, chart, version, fmt.Sprintf("%s/../provenance/%s/%s", chartsDir, chart, chartProvenance.UpstreamChartLocalPath))
   113  
   114  }
   115  
   116  // GeneratePatchWithSourceDir diffs a chart against a given directory and generates a patch file.
   117  func (hfs HelmChartFileSystem) GeneratePatchWithSourceDir(chartsDir string, chart string, version string, sourceDir string) (string, error) {
   118  	chartDir := fmt.Sprintf("%s/%s/%s", chartsDir, chart, version)
   119  	if _, err := os.Stat(chartDir); err != nil {
   120  		return "", fmt.Errorf("chart directory %s not found, error %v", chartDir, err)
   121  	}
   122  
   123  	sourceChartDirectory, err := filepath.Abs(sourceDir)
   124  	if err != nil {
   125  		return "", fmt.Errorf("unable to find absolute path to upstream/source chart directory at %s, error %v", sourceChartDirectory, err)
   126  	}
   127  
   128  	if _, err := os.Stat(sourceChartDirectory); err != nil {
   129  		return "", fmt.Errorf("upstream/source chart directory %s not found, error %v", sourceChartDirectory, err)
   130  	}
   131  
   132  	patchFilePathAbsolute, err := filepath.Abs(fmt.Sprintf("%s/../vz_charts_patch_%s_%s.patch", chartsDir, chart, version))
   133  	if err != nil {
   134  		return "", fmt.Errorf("unable to find absolute path for patch file")
   135  	}
   136  
   137  	patchFile, err := os.Create(patchFilePathAbsolute)
   138  	if err != nil {
   139  		return "", fmt.Errorf("unable to create empty patch file")
   140  	}
   141  
   142  	cmd := exec.Command("diff", "-Naurw", sourceChartDirectory, chartDir)
   143  	cmd.Stdout = patchFile
   144  	err = cmd.Run()
   145  	if err != nil {
   146  		// diff returning exit status 1 even when file diff is completed and no underlying error.
   147  		// error out only when message is different
   148  		if err.Error() != "exit status 1" {
   149  			return "", fmt.Errorf("error running command %s, error %v", cmd.String(), err)
   150  		}
   151  	}
   152  
   153  	patchFileStats, err := os.Stat(patchFile.Name())
   154  	if err != nil {
   155  		return "", fmt.Errorf("unable to stat patch file at %v, error %v", patchFile.Name(), err)
   156  	}
   157  
   158  	if patchFileStats.Size() == 0 {
   159  		err := os.Remove(patchFile.Name())
   160  		if err != nil {
   161  			return "", fmt.Errorf("unable to remove empty patch file at %v, error %v", patchFile.Name(), err)
   162  		}
   163  
   164  		return "", nil
   165  	}
   166  
   167  	return patchFile.Name(), nil
   168  }
   169  
   170  // FindChartVersionToPatch looks up the last higheset version present in the charts directory against a given chart version.
   171  func (hfs HelmChartFileSystem) FindChartVersionToPatch(chartsDir string, chart string, version string) (string, error) {
   172  	chartDirParent := fmt.Sprintf("%s/%s", chartsDir, chart)
   173  	entries, err := os.ReadDir(chartDirParent)
   174  	if err != nil {
   175  		return "", fmt.Errorf("unable to read chart dierctory %s, error %v", chartDirParent, err)
   176  	}
   177  
   178  	currentChartVersion, err := semver.NewSemVersion(version)
   179  	if err != nil {
   180  		return "", fmt.Errorf("invalid chart version %s, error %v", version, err)
   181  	}
   182  
   183  	var versions []*semver.SemVersion
   184  	for _, entry := range entries {
   185  		if entry.IsDir() {
   186  			chartVersion, err := semver.NewSemVersion(entry.Name())
   187  			if err != nil {
   188  				return "", fmt.Errorf("invalid chart version %s, error %v", chartVersion.ToString(), err)
   189  			}
   190  
   191  			if chartVersion.IsLessThan(currentChartVersion) {
   192  				versions = append(versions, chartVersion)
   193  			}
   194  		}
   195  	}
   196  
   197  	if len(versions) == 0 {
   198  		return "", nil
   199  	}
   200  
   201  	highestVersion := versions[0]
   202  	for _, version := range versions {
   203  		if version.IsGreatherThan(highestVersion) {
   204  			highestVersion = version
   205  		}
   206  	}
   207  	return highestVersion.ToString(), nil
   208  }
   209  
   210  // ApplyPatchFile patches a given patch file on a chart.
   211  func (hfs HelmChartFileSystem) ApplyPatchFile(chartsDir string, vzHelper helpers.VZHelper, chart string, version string, patchFile string) (bool, error) {
   212  	chartDir := fmt.Sprintf("%s/%s/%s/", chartsDir, chart, version)
   213  	if _, err := os.Stat(chartDir); err != nil {
   214  		return false, fmt.Errorf("chart directory %s not found, error %v", chartDir, err)
   215  	}
   216  
   217  	if _, err := os.Stat(patchFile); err != nil {
   218  		return false, fmt.Errorf("patch file %s not found, error %v", patchFile, err)
   219  	}
   220  
   221  	rejectsFilePathAbsolute, err := filepath.Abs(fmt.Sprintf("%s/../vz_charts_patch_%s_%s_rejects.rejects", chartsDir, chart, version))
   222  	if err != nil {
   223  		return false, fmt.Errorf("unable to find absolute path for rejects file")
   224  	}
   225  
   226  	_, err = os.Create(rejectsFilePathAbsolute)
   227  	if err != nil {
   228  		return false, fmt.Errorf("unable to create empty rejects file")
   229  	}
   230  
   231  	in, err := os.OpenFile(patchFile, io.SeekStart, os.ModePerm)
   232  	if err != nil {
   233  		return false, fmt.Errorf("unable to read patch file")
   234  	}
   235  
   236  	skipLevels := fmt.Sprintf("-p%v", fmt.Sprint(strings.Count(chartDir, string(os.PathSeparator))))
   237  	cmd := exec.Command("patch", "--no-backup-if-mismatch", skipLevels, "-r", rejectsFilePathAbsolute, "--directory", chartDir)
   238  	cmd.Stdin = in
   239  	out, cmderr := cmd.CombinedOutput()
   240  	if cmderr != nil && cmderr.Error() != "exit status 1" {
   241  		return false, fmt.Errorf("error running command %s, error %v", cmd.String(), err)
   242  	}
   243  
   244  	rejectsFileStats, err := os.Stat(rejectsFilePathAbsolute)
   245  	if err != nil {
   246  		return false, fmt.Errorf("unable to stat reject file at %v, error %v", rejectsFilePathAbsolute, err)
   247  	}
   248  
   249  	if rejectsFileStats.Size() == 0 {
   250  		err := os.Remove(rejectsFilePathAbsolute)
   251  		if err != nil {
   252  			return false, fmt.Errorf("unable to remove empty rejects file at %v, error %v", rejectsFilePathAbsolute, err)
   253  		}
   254  
   255  		rejectsFilePathAbsolute = ""
   256  	}
   257  
   258  	if cmderr != nil && rejectsFilePathAbsolute == "" {
   259  		return false, fmt.Errorf("error running command %s, error %v", cmd.String(), err)
   260  	}
   261  
   262  	if len(out) > 0 {
   263  		fmt.Fprintf(vzHelper.GetOutputStream(), "%s", string(out))
   264  	}
   265  
   266  	if rejectsFilePathAbsolute != "" {
   267  		rejects, err := os.ReadFile(rejectsFilePathAbsolute)
   268  		if err != nil {
   269  			return true, fmt.Errorf("unable to read rejects file at %s, error %v", rejectsFilePathAbsolute, err)
   270  		}
   271  
   272  		fmt.Fprintf(vzHelper.GetOutputStream(), "%s", string(rejects))
   273  		return true, nil
   274  	}
   275  
   276  	return false, nil
   277  }