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 }