From 2f86ee0a4a9259d08d3a3d75e39837024623d396 Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Tue, 30 Jul 2024 18:21:23 -0300 Subject: [PATCH 01/13] adding new command 'release' to charts-build-scripts --- main.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/main.go b/main.go index a7d36b58..6c31a866 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,8 @@ const ( defaultBranchVersionEnvironmentVariable = "BRANCH_VERSION" // defaultForkEnvironmentVariable is the default environment variable that indicates the fork URL defaultForkEnvironmentVariable = "FORK" + // defaultChartVersionEnvironmentVariable is the default environment variable that indicates the version to release + defaultChartVersionEnvironmentVariable = "" ) var ( @@ -76,6 +78,8 @@ var ( DebugMode = false // ForkURL represents the fork URL configured as a remote in your local git repository ForkURL = "" + // ChartVersion of the chart to release + ChartVersion = "" ) func main() { @@ -171,6 +175,18 @@ func main() { Destination: &ForkURL, EnvVar: defaultForkEnvironmentVariable, } + chartVersionFlag := cli.StringFlag{ + Name: "version", + Usage: `Usage: + ./bin/charts-build-scripts --version="" + VERSION="" make + + Target version of chart to release. + `, + Required: true, + Destination: &ChartVersion, + EnvVar: defaultChartVersionEnvironmentVariable, + } app.Commands = []cli.Command{ { Name: "list", @@ -319,6 +335,13 @@ func main() { Action: autoForwardPort, Flags: []cli.Flag{branchVersionFlag, chartFlag, forkFlag}, }, + { + Name: "release", + Usage: `Execute the release script to release a chart to the production branch. + `, + Action: release, + Flags: []cli.Flag{branchVersionFlag, chartFlag, chartVersionFlag, forkFlag}, + }, } if err := app.Run(os.Args); err != nil { From 0a770526479e74e5c5a778b9c53b2ac1a115ca2c Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Tue, 30 Jul 2024 18:22:28 -0300 Subject: [PATCH 02/13] defining the release function --- main.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/main.go b/main.go index 6c31a866..c751237f 100644 --- a/main.go +++ b/main.go @@ -711,3 +711,46 @@ func autoForwardPort(c *cli.Context) { } } + +func release(c *cli.Context) { + if ForkURL == "" { + logrus.Fatal("FORK environment variable must be set to run release cmd") + } + + if CurrentChart == "" { + logrus.Fatal("CHART environment variable must be set to run release cmd") + } + + rootFs := filesystem.GetFilesystem(getRepoRoot()) + + dependencies, err := lifecycle.InitDependencies(rootFs, c.String("branch-version"), CurrentChart, false) + if err != nil { + logrus.Fatalf("encountered error while initializing dependencies: %v", err) + } + + status, err := lifecycle.LoadState(rootFs) + if err != nil { + logrus.Fatalf("could not load state; please run lifecycle-status before this command: %v", err) + } + + release, err := auto.InitRelease(dependencies, status, ChartVersion, CurrentChart, ForkURL) + if err != nil { + logrus.Fatalf("failed to initialize release: %v", err) + } + + if err := release.PullAsset(); err != nil { + logrus.Fatalf("failed to execute release: %v", err) + } + + // Unzip Assets: ASSET=/- Date: Tue, 30 Jul 2024 18:23:25 -0300 Subject: [PATCH 03/13] making rootFs billy.Filesystem public --- pkg/lifecycle/lifecycle.go | 10 +++++----- pkg/lifecycle/parser.go | 2 +- pkg/lifecycle/state.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go index b811a808..42b8360e 100644 --- a/pkg/lifecycle/lifecycle.go +++ b/pkg/lifecycle/lifecycle.go @@ -22,7 +22,7 @@ type Asset struct { // Dependencies holds the necessary filesystem, // assets versions map, version rules and methods to apply the lifecycle rules in the target branch type Dependencies struct { - rootFs billy.Filesystem + RootFs billy.Filesystem assetsVersionsMap map[string][]Asset VR *VersionRules Git *git.Git @@ -103,17 +103,17 @@ func InitDependencies(rootFs billy.Filesystem, branchVersion string, currentChar } // Get the filesystem and index.yaml path for the repository - dep.rootFs = rootFs + dep.RootFs = rootFs // Check if the assets folder and Helm index file exists in the repository - exists, err := filesystem.PathExists(dep.rootFs, path.RepositoryAssetsDir) + exists, err := filesystem.PathExists(dep.RootFs, path.RepositoryAssetsDir) if err != nil { return nil, fmt.Errorf("encountered error while checking if assets folder already exists in repository: %s", err) } if !exists { return nil, fmt.Errorf("assets folder does not exist in the repository") } - exists, err = filesystem.PathExists(dep.rootFs, path.RepositoryHelmIndexFile) + exists, err = filesystem.PathExists(dep.RootFs, path.RepositoryHelmIndexFile) if err != nil { return nil, fmt.Errorf("encountered error while checking if Helm index file already exists in repository: %s", err) } @@ -122,7 +122,7 @@ func InitDependencies(rootFs billy.Filesystem, branchVersion string, currentChar } // Get the absolute path of the Helm index file and assets versions map to apply rules - helmIndexPath := filesystem.GetAbsPath(dep.rootFs, path.RepositoryHelmIndexFile) + helmIndexPath := filesystem.GetAbsPath(dep.RootFs, path.RepositoryHelmIndexFile) dep.assetsVersionsMap, err = getAssetsMapFromIndex(helmIndexPath, currentChart, debug) if len(dep.assetsVersionsMap) == 0 { return nil, fmt.Errorf("no assets found in the repository") diff --git a/pkg/lifecycle/parser.go b/pkg/lifecycle/parser.go index 46f75efa..0008268e 100644 --- a/pkg/lifecycle/parser.go +++ b/pkg/lifecycle/parser.go @@ -77,7 +77,7 @@ func (ld *Dependencies) populateAssetsVersionsPath(debug bool) error { dirPath := fmt.Sprintf("assets/%s", chart) cycleLog(debug, "Getting assets at path", dirPath) - if err := ld.walkDirWrapper(ld.rootFs, dirPath, doFunc); err != nil { + if err := ld.walkDirWrapper(ld.RootFs, dirPath, doFunc); err != nil { return fmt.Errorf("encountered error while walking through the assets directory: %w", err) } diff --git a/pkg/lifecycle/state.go b/pkg/lifecycle/state.go index 4b061bd7..f3823e36 100644 --- a/pkg/lifecycle/state.go +++ b/pkg/lifecycle/state.go @@ -73,7 +73,7 @@ func (s *Status) checkStateFileExist() (bool, error) { // createStateFile will create a new state file at the charts repo func (s *Status) createStateFile() error { - stateFilePath := filesystem.GetAbsPath(s.ld.rootFs, path.RepositoryStateFile) + stateFilePath := filesystem.GetAbsPath(s.ld.RootFs, path.RepositoryStateFile) if _, err := os.Create(stateFilePath); err != nil { return err From b71afd3762840a69dacd3fbb1b3c56a2d545ad90 Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Tue, 30 Jul 2024 18:24:10 -0300 Subject: [PATCH 04/13] adding release.go with the execution of the process --- pkg/auto/release.go | 181 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 pkg/auto/release.go diff --git a/pkg/auto/release.go b/pkg/auto/release.go new file mode 100644 index 00000000..2d0dab3f --- /dev/null +++ b/pkg/auto/release.go @@ -0,0 +1,181 @@ +package auto + +import ( + "bytes" + "errors" + "fmt" + "os" + + "github.com/rancher/charts-build-scripts/pkg/filesystem" + "github.com/rancher/charts-build-scripts/pkg/git" + "github.com/rancher/charts-build-scripts/pkg/lifecycle" + "github.com/rancher/charts-build-scripts/pkg/path" + "gopkg.in/yaml.v2" +) + +// Release holds necessary metadata to release a chart version +type Release struct { + git *git.Git + VR *lifecycle.VersionRules + AssetTgz string + AssetPath string + ChartVersion string + Chart string + ReleaseYamlPath string + ForkRemoteURL string +} + +// ReleaseVersions represents the versions within the release.yaml file +type ReleaseVersions map[string][]string + +// InitRelease will create the Release struct with access to the necessary dependencies. +func InitRelease(d *lifecycle.Dependencies, s *lifecycle.Status, v, c, f string) (*Release, error) { + r := &Release{ + git: d.Git, + VR: d.VR, + ChartVersion: v, + Chart: c, + ForkRemoteURL: f, + } + + var assetVersions []lifecycle.Asset = make([]lifecycle.Asset, 0) + var ok bool + + assetVersions, ok = s.AssetsToBeReleased[r.Chart] + if !ok { + assetVersions, ok = s.AssetsToBeForwardPorted[r.Chart] + if !ok { + return nil, errors.New("no asset version to release for chart:" + r.Chart) + } + } + + var assetVersion string + for _, version := range assetVersions { + if version.Version == r.ChartVersion { + assetVersion = version.Version + break + } + } + if assetVersion == "" { + return nil, errors.New("no asset version to release for chart:" + r.Chart + " version:" + r.ChartVersion) + } + + r.AssetPath, r.AssetTgz = mountAssetVersionPath(r.Chart, assetVersion) + + // Check again if the asset was already released in the local repository + if exist := checkAssetReleased(r.AssetPath); exist { + return nil, errors.New("asset already released for chart:" + r.Chart + " version:" + r.ChartVersion) + } + + // Check if we have a release.yaml file in the expected path + if exist, err := filesystem.PathExists(d.RootFs, path.RepositoryReleaseYaml); err != nil || !exist { + return nil, errors.New("release.yaml not found") + } + + r.ReleaseYamlPath = filesystem.GetAbsPath(d.RootFs, path.RepositoryReleaseYaml) + + return r, nil +} + +// PullAsset will execute the release porting for a chart in the repository +func (r *Release) PullAsset() error { + if err := r.git.FetchBranch(r.VR.DevBranch); err != nil { + return err + } + + if err := r.git.CheckFileExists(r.AssetPath, r.VR.DevBranch); err != nil { + return fmt.Errorf("asset version not found in dev branch: %w", err) + } + + if err := r.git.CheckoutFile(r.VR.DevBranch, r.AssetPath); err != nil { + return err + } + + if err := r.git.ResetHEAD(); err != nil { + return err + } + + return nil +} + +func checkAssetReleased(chartVersion string) bool { + if _, err := os.Stat(chartVersion); err != nil { + return false + } + + return true +} + +func mountAssetVersionPath(chart, version string) (assetPath string, assetTgz string) { + // example: assets/longhorn/longhorn-100.0.0+up0.0.0.tgz + assetTgz = chart + "-" + version + ".tgz" + assetPath = "assets/" + chart + "/" + assetTgz + return +} + +// UpdateReleaseYaml reads and parse the release.yaml file to a struct, appends the new version and writes it back to the file. +func (r *Release) UpdateReleaseYaml() error { + var releaseVersions ReleaseVersions + + rvBytes, err := os.ReadFile(r.ReleaseYamlPath) + if err != nil { + return err + } + + if err := yaml.Unmarshal(rvBytes, &releaseVersions); err != nil { + return err + } + + // Append new version and remove duplicates if any + releaseVersions[r.Chart] = append(releaseVersions[r.Chart], r.ChartVersion) + releaseVersions[r.Chart] = removeDuplicates(releaseVersions[r.Chart]) + + // Marshal the updated releaseVersions back to YAML + updatedYAML, err := yaml.Marshal(releaseVersions) + if err != nil { + return err + } + + // Post-process YAML to adjust indentation for list items + updatedYAMLIndented := enforceYamlStandard(updatedYAML) + + // Write the updated YAML back to the file + if err := os.WriteFile(r.ReleaseYamlPath, updatedYAMLIndented, 0644); err != nil { + return err + } + + return nil +} + +// removeDuplicates takes a slice of strings and returns a new slice with duplicates removed. +func removeDuplicates(slice []string) []string { + seen := make(map[string]struct{}) // map to keep track of seen strings + var result []string // slice to hold the results + + for _, val := range slice { + if _, ok := seen[val]; !ok { + seen[val] = struct{}{} // mark string as seen + result = append(result, val) // append to result if not seen before + } + } + + return result +} + +// enforceYamlStandard adds indentation to list items in a YAML string and a new line at the end of the file. +func enforceYamlStandard(yamlBytes []byte) []byte { + var indentedBuffer bytes.Buffer + lines := bytes.Split(yamlBytes, []byte("\n")) + for i, line := range lines { + // Check if the line is not the last empty line after splitting + if i != len(lines)-1 { + if bytes.HasPrefix(line, []byte("- ")) { + indentedBuffer.Write([]byte(" ")) // Add two spaces of indentation + } + indentedBuffer.Write(line) + indentedBuffer.WriteByte('\n') + } + } + // Ensure only one newline at the end + return append(indentedBuffer.Bytes(), '\n') +} From 4e87eedcc0268a0787f40a9e919041a29614f00b Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Tue, 30 Jul 2024 18:24:52 -0300 Subject: [PATCH 05/13] adding 3 more functions to git.go to be used by release.go --- pkg/git/git.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkg/git/git.go b/pkg/git/git.go index 740ee260..714189cd 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -170,6 +170,20 @@ func (g *Git) CheckoutBranch(branch string) error { return nil } +// CheckoutFile checks out a file in a branch +// ex: git checkout / -- +func (g *Git) CheckoutFile(branch, file string) error { + upstreamRemote := g.Remotes["https://github.com/rancher/charts"] + targetBranch := upstreamRemote + "/" + branch + + cmd := exec.Command("git", "-C", g.Dir, "checkout", targetBranch, "--", file) + if err := cmd.Run(); err != nil { + return err // Return the error if the file does not exist or any other git error occurs + } + + return nil +} + // CreateAndCheckoutBranch creates and checks out to a given branch. // Equivalent to: git checkout -b func (g *Git) CreateAndCheckoutBranch(branch string) error { @@ -180,6 +194,7 @@ func (g *Git) CreateAndCheckoutBranch(branch string) error { logrus.Errorf("error while creating and checking out branch: %s; err: %v", branch, err) return fmt.Errorf("error while creating and checking out branch: %s", err) } + return nil } @@ -263,3 +278,26 @@ func (g *Git) DeleteBranch(branch string) error { } return nil } + +// CheckFileExists checks if a file exists in the git repository for a specific branch +func (g *Git) CheckFileExists(file, branch string) error { + upstreamRemote := g.Remotes["https://github.com/rancher/charts"] + target := upstreamRemote + "/" + branch + ":" + file + + // Corrected command to only include the necessary arguments + cmd := exec.Command("git", "-C", g.Dir, "cat-file", "-e", target) + if err := cmd.Run(); err != nil { + return err // Return the error if the file does not exist or any other git error occurs + } + return nil // Return nil if the file exists +} + +// ResetHEAD resets the HEAD of the git repository +// ex: git reset HEAD +func (g *Git) ResetHEAD() error { + cmd := exec.Command("git", "-C", g.Dir, "reset", "HEAD") + if err := cmd.Run(); err != nil { + return err + } + return nil +} From 8b3d510439ae2ac072156be70d5ba77d32fa8ed3 Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Tue, 30 Jul 2024 18:25:10 -0300 Subject: [PATCH 06/13] path for release.yaml in the repository --- pkg/path/path.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/path/path.go b/pkg/path/path.go index 3fbba4ed..be2e696e 100644 --- a/pkg/path/path.go +++ b/pkg/path/path.go @@ -52,4 +52,7 @@ const ( // RepositoryStAte file is a file to hold the current status of the released and developed assets versions RepositoryStateFile = "state.json" + + // RepositoryReleaseYaml is the file on your Staging/Live branch that contains the release information + RepositoryReleaseYaml = "release.yaml" ) From cc64fa903634b915ef15434818846d8aa94ed567 Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Tue, 30 Jul 2024 18:25:18 -0300 Subject: [PATCH 07/13] unit tests --- pkg/auto/release_test.go | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 pkg/auto/release_test.go diff --git a/pkg/auto/release_test.go b/pkg/auto/release_test.go new file mode 100644 index 00000000..a39b1d9f --- /dev/null +++ b/pkg/auto/release_test.go @@ -0,0 +1,85 @@ +package auto + +import "testing" + +func Test_mountAssetVersionPath(t *testing.T) { + type input struct { + chart, version string + } + inputs := []input{ + {"chart1", "1.0.0"}, + {"chart2", "100.0.0+up0.0.0"}, + } + type output struct { + assetPath, assetTgz string + } + outputs := []output{ + {"assets/chart1/chart1-1.0.0.tgz", "chart1-1.0.0.tgz"}, + {"assets/chart2/chart2-100.0.0+up0.0.0.tgz", "chart2-100.0.0+up0.0.0.tgz"}, + } + + type test struct { + name string + input input + output output + } + + tests := []test{ + {"#1", inputs[0], outputs[0]}, + {"#2", inputs[1], outputs[1]}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assetPath, assetTgz := mountAssetVersionPath(test.input.chart, test.input.version) + if assetPath != test.output.assetPath { + t.Errorf("expected %s, got %s", test.output.assetPath, assetPath) + } + if assetTgz != test.output.assetTgz { + t.Errorf("expected %s, got %s", test.output.assetTgz, assetTgz) + } + }) + } +} + +func Test_removeDuplicates(t *testing.T) { + type input struct { + versions []string + } + inputs := []input{ + {[]string{"1.0.0", "1.0.0", "1.0.0"}}, + {[]string{"1.0.0", "1.0.0", "2.0.0", "2.0.0", "3.0.0"}}, + } + type output struct { + uniqueVersions []string + } + outputs := []output{ + {[]string{"1.0.0"}}, + {[]string{"1.0.0", "2.0.0", "3.0.0"}}, + } + + type test struct { + name string + input input + output output + } + + tests := []test{ + {"#1", inputs[0], outputs[0]}, + {"#2", inputs[1], outputs[1]}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + uniqueVersions := removeDuplicates(test.input.versions) + if len(uniqueVersions) != len(test.output.uniqueVersions) { + t.Errorf("expected %v, got %v", test.output.uniqueVersions, uniqueVersions) + } + for i := range uniqueVersions { + if uniqueVersions[i] != test.output.uniqueVersions[i] { + t.Errorf("expected %v, got %v", test.output.uniqueVersions, uniqueVersions) + } + } + }) + } +} From c34578cebacef014147ebc443ad70082a349a56a Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Tue, 30 Jul 2024 18:54:29 -0300 Subject: [PATCH 08/13] fixes --- pkg/auto/release.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/auto/release.go b/pkg/auto/release.go index 2d0dab3f..4b93c0f6 100644 --- a/pkg/auto/release.go +++ b/pkg/auto/release.go @@ -38,8 +38,8 @@ func InitRelease(d *lifecycle.Dependencies, s *lifecycle.Status, v, c, f string) ForkRemoteURL: f, } - var assetVersions []lifecycle.Asset = make([]lifecycle.Asset, 0) var ok bool + var assetVersions []lifecycle.Asset assetVersions, ok = s.AssetsToBeReleased[r.Chart] if !ok { From 23323fe461825c944ad8cd9c3fa71ddbb7502cdc Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Fri, 2 Aug 2024 18:52:51 -0300 Subject: [PATCH 09/13] fixes 2 --- pkg/git/git.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/pkg/git/git.go b/pkg/git/git.go index 714189cd..f1bb78a1 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -283,21 +283,11 @@ func (g *Git) DeleteBranch(branch string) error { func (g *Git) CheckFileExists(file, branch string) error { upstreamRemote := g.Remotes["https://github.com/rancher/charts"] target := upstreamRemote + "/" + branch + ":" + file - - // Corrected command to only include the necessary arguments - cmd := exec.Command("git", "-C", g.Dir, "cat-file", "-e", target) - if err := cmd.Run(); err != nil { - return err // Return the error if the file does not exist or any other git error occurs - } - return nil // Return nil if the file exists + return exec.Command("git", "-C", g.Dir, "cat-file", "-e", target).Run() } // ResetHEAD resets the HEAD of the git repository // ex: git reset HEAD func (g *Git) ResetHEAD() error { - cmd := exec.Command("git", "-C", g.Dir, "reset", "HEAD") - if err := cmd.Run(); err != nil { - return err - } - return nil + return exec.Command("git", "-C", g.Dir, "reset", "HEAD").Run() } From c993b9a7ac002a2d40a867001112cb62c8c6288c Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Mon, 5 Aug 2024 16:44:38 -0300 Subject: [PATCH 10/13] fixes 3 --- pkg/auto/release.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/auto/release.go b/pkg/auto/release.go index 4b93c0f6..98d3990e 100644 --- a/pkg/auto/release.go +++ b/pkg/auto/release.go @@ -4,13 +4,14 @@ import ( "bytes" "errors" "fmt" + "io" "os" "github.com/rancher/charts-build-scripts/pkg/filesystem" "github.com/rancher/charts-build-scripts/pkg/git" "github.com/rancher/charts-build-scripts/pkg/lifecycle" "github.com/rancher/charts-build-scripts/pkg/path" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) // Release holds necessary metadata to release a chart version @@ -25,9 +26,6 @@ type Release struct { ForkRemoteURL string } -// ReleaseVersions represents the versions within the release.yaml file -type ReleaseVersions map[string][]string - // InitRelease will create the Release struct with access to the necessary dependencies. func InitRelease(d *lifecycle.Dependencies, s *lifecycle.Status, v, c, f string) (*Release, error) { r := &Release{ @@ -63,8 +61,8 @@ func InitRelease(d *lifecycle.Dependencies, s *lifecycle.Status, v, c, f string) r.AssetPath, r.AssetTgz = mountAssetVersionPath(r.Chart, assetVersion) // Check again if the asset was already released in the local repository - if exist := checkAssetReleased(r.AssetPath); exist { - return nil, errors.New("asset already released for chart:" + r.Chart + " version:" + r.ChartVersion) + if err := checkAssetReleased(r.AssetPath); err != nil { + return nil, fmt.Errorf("failed to check for chart:%s ; err: %w", r.Chart, err) } // Check if we have a release.yaml file in the expected path From d18d49ed7e14a0d511dcda6be8a8afbf31c470c2 Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Mon, 5 Aug 2024 16:44:56 -0300 Subject: [PATCH 11/13] changing read/write for yaml file strategy to encode/decode --- pkg/auto/release.go | 61 +++++++++------- pkg/auto/release_test.go | 148 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 183 insertions(+), 26 deletions(-) diff --git a/pkg/auto/release.go b/pkg/auto/release.go index 98d3990e..7ee8da03 100644 --- a/pkg/auto/release.go +++ b/pkg/auto/release.go @@ -89,56 +89,67 @@ func (r *Release) PullAsset() error { return err } - if err := r.git.ResetHEAD(); err != nil { + return r.git.ResetHEAD() +} + +func checkAssetReleased(chartVersion string) error { + if _, err := os.Stat(chartVersion); err != nil { return err } return nil } -func checkAssetReleased(chartVersion string) bool { - if _, err := os.Stat(chartVersion); err != nil { - return false +// mountAssetVersionPath returns the asset path and asset tgz name for a given chart and version. +// example: assets/longhorn/longhorn-100.0.0+up0.0.0.tgz +func mountAssetVersionPath(chart, version string) (string, string) { + assetTgz := chart + "-" + version + ".tgz" + assetPath := "assets/" + chart + "/" + assetTgz + return assetPath, assetTgz +} + +func (r *Release) readReleaseYaml() (map[string][]string, error) { + var releaseVersions = make(map[string][]string, 0) + + file, err := os.Open(r.ReleaseYamlPath) + if err != nil { + return nil, err } + defer file.Close() - return true -} + decoder := yaml.NewDecoder(file) + if err := decoder.Decode(&releaseVersions); err != nil { + if err == io.EOF { + // Handle EOF error gracefully + return releaseVersions, nil + } + return nil, err + } -func mountAssetVersionPath(chart, version string) (assetPath string, assetTgz string) { - // example: assets/longhorn/longhorn-100.0.0+up0.0.0.tgz - assetTgz = chart + "-" + version + ".tgz" - assetPath = "assets/" + chart + "/" + assetTgz - return + return releaseVersions, nil } // UpdateReleaseYaml reads and parse the release.yaml file to a struct, appends the new version and writes it back to the file. func (r *Release) UpdateReleaseYaml() error { - var releaseVersions ReleaseVersions - - rvBytes, err := os.ReadFile(r.ReleaseYamlPath) + releaseVersions, err := r.readReleaseYaml() if err != nil { return err } - if err := yaml.Unmarshal(rvBytes, &releaseVersions); err != nil { - return err - } - // Append new version and remove duplicates if any releaseVersions[r.Chart] = append(releaseVersions[r.Chart], r.ChartVersion) releaseVersions[r.Chart] = removeDuplicates(releaseVersions[r.Chart]) - // Marshal the updated releaseVersions back to YAML - updatedYAML, err := yaml.Marshal(releaseVersions) + // Since we opened and read the file before we can truncate it. + outputFile, err := os.Create(r.ReleaseYamlPath) if err != nil { return err } + defer outputFile.Close() - // Post-process YAML to adjust indentation for list items - updatedYAMLIndented := enforceYamlStandard(updatedYAML) - - // Write the updated YAML back to the file - if err := os.WriteFile(r.ReleaseYamlPath, updatedYAMLIndented, 0644); err != nil { + encoder := yaml.NewEncoder(outputFile) + encoder.SetIndent(2) // Assuming you want to set a specific indentation + if err := encoder.Encode(releaseVersions); err != nil { return err } diff --git a/pkg/auto/release_test.go b/pkg/auto/release_test.go index a39b1d9f..7e4570e4 100644 --- a/pkg/auto/release_test.go +++ b/pkg/auto/release_test.go @@ -1,6 +1,152 @@ package auto -import "testing" +import ( + "os" + "testing" +) + +func Test_UpdateReleaseYaml(t *testing.T) { + type input struct { + ReleaseVersions map[string][]string + ChartVersion string + Chart string + } + type expected struct { + ReleaseVersions map[string][]string + } + type test struct { + name string + i input + ex expected + } + tests := []test{ + { + name: "Test #1", + i: input{ + ReleaseVersions: map[string][]string{}, + ChartVersion: "1.0.0", + Chart: "chart1", + }, + ex: expected{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0"}, + }, + }, + }, + { + name: "Test #2", + i: input{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0"}, + }, + ChartVersion: "2.0.0", + Chart: "chart1", + }, + ex: expected{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0", "2.0.0"}, + }, + }, + }, + { + name: "Test #3", + i: input{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0", "2.0.0"}, + }, + ChartVersion: "3.0.0", + Chart: "chart1", + }, + ex: expected{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0", "2.0.0", "3.0.0"}, + }, + }, + }, + + { + name: "Test #4", + i: input{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0", "2.0.0", "3.0.0"}, + }, + ChartVersion: "2.0.0", + Chart: "chart2", + }, + ex: expected{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0", "2.0.0", "3.0.0"}, + "chart2": {"2.0.0"}, + }, + }, + }, + // Test for duplicate versions + { + name: "Test #5", + i: input{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0", "2.0.0", "3.0.0"}, + "chart2": {"2.0.0"}, + }, + ChartVersion: "2.0.0", + Chart: "chart2", + }, + ex: expected{ + ReleaseVersions: map[string][]string{ + "chart1": {"1.0.0", "2.0.0", "3.0.0"}, + "chart2": {"2.0.0"}, + }, + }, + }, + } + + tempDir, err := os.MkdirTemp("", "unit-test-tmp") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a release.yaml file + if _, err := os.Create(tempDir + "/release.yaml"); err != nil { + t.Fatalf("failed to create release.yaml file: %v", err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + r := &Release{ + ChartVersion: tt.i.ChartVersion, + Chart: tt.i.Chart, + ReleaseYamlPath: tempDir + "/release.yaml", + } + + if err := r.UpdateReleaseYaml(); err != nil { + t.Fatalf("expected nil, got %v", err) + } + + releaseVersions, err := r.readReleaseYaml() + if err != nil { + t.Fatalf("expected nil, got %v", err) + } + + if len(releaseVersions) != len(tt.ex.ReleaseVersions) { + t.Errorf("expected %v, got %v", tt.ex.ReleaseVersions, tt.i.ReleaseVersions) + } + + for k, v := range tt.ex.ReleaseVersions { + if len(releaseVersions[k]) != len(v) { + t.Errorf("expected %v, got %v", v, releaseVersions[k]) + } + for i := range v { + if v[i] != releaseVersions[k][i] { + t.Errorf("expected %v, got %v", v, releaseVersions[k]) + } + } + } + }) + } + +} func Test_mountAssetVersionPath(t *testing.T) { type input struct { From 00730da17eaacce052a78a2fbd278c819605aa59 Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Mon, 5 Aug 2024 16:46:37 -0300 Subject: [PATCH 12/13] removing enforceYamlStandard in favor of .SetIndent(2) previously implemented --- pkg/auto/release.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pkg/auto/release.go b/pkg/auto/release.go index 7ee8da03..64ace9af 100644 --- a/pkg/auto/release.go +++ b/pkg/auto/release.go @@ -1,7 +1,6 @@ package auto import ( - "bytes" "errors" "fmt" "io" @@ -170,21 +169,3 @@ func removeDuplicates(slice []string) []string { return result } - -// enforceYamlStandard adds indentation to list items in a YAML string and a new line at the end of the file. -func enforceYamlStandard(yamlBytes []byte) []byte { - var indentedBuffer bytes.Buffer - lines := bytes.Split(yamlBytes, []byte("\n")) - for i, line := range lines { - // Check if the line is not the last empty line after splitting - if i != len(lines)-1 { - if bytes.HasPrefix(line, []byte("- ")) { - indentedBuffer.Write([]byte(" ")) // Add two spaces of indentation - } - indentedBuffer.Write(line) - indentedBuffer.WriteByte('\n') - } - } - // Ensure only one newline at the end - return append(indentedBuffer.Bytes(), '\n') -} From cb000caf92ae2fb5fba6f825565f08db0741a5be Mon Sep 17 00:00:00 2001 From: nicholasSSUSE Date: Mon, 5 Aug 2024 16:53:52 -0300 Subject: [PATCH 13/13] refactoring git.go --- pkg/auto/release.go | 2 +- pkg/git/git.go | 60 +++++++++------------------------------------ 2 files changed, 12 insertions(+), 50 deletions(-) diff --git a/pkg/auto/release.go b/pkg/auto/release.go index 64ace9af..c1e8b518 100644 --- a/pkg/auto/release.go +++ b/pkg/auto/release.go @@ -147,7 +147,7 @@ func (r *Release) UpdateReleaseYaml() error { defer outputFile.Close() encoder := yaml.NewEncoder(outputFile) - encoder.SetIndent(2) // Assuming you want to set a specific indentation + encoder.SetIndent(2) if err := encoder.Encode(releaseVersions); err != nil { return err } diff --git a/pkg/git/git.go b/pkg/git/git.go index f1bb78a1..772bf4e0 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -145,17 +145,12 @@ func (g *Git) FetchAndCheckoutBranch(branch string) error { // FetchBranch fetches a branch func (g *Git) FetchBranch(branch string) error { - remote := g.Remotes["https://github.com/rancher/charts"] cmd := exec.Command("git", "-C", g.Dir, "fetch", remote, branch+":"+branch) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - logrus.Errorf("error while fetching branch: %s; err: %v", branch, err) - return fmt.Errorf("error while fetching branch: %s", err) - } - return nil + return cmd.Run() } // CheckoutBranch checks out a branch @@ -163,11 +158,7 @@ func (g *Git) CheckoutBranch(branch string) error { cmd := exec.Command("git", "-C", g.Dir, "checkout", branch) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - logrus.Errorf("error while checking out branch: %s; err: %v", branch, err) - return fmt.Errorf("error while checking out branch: %s", err) - } - return nil + return cmd.Run() } // CheckoutFile checks out a file in a branch @@ -175,13 +166,10 @@ func (g *Git) CheckoutBranch(branch string) error { func (g *Git) CheckoutFile(branch, file string) error { upstreamRemote := g.Remotes["https://github.com/rancher/charts"] targetBranch := upstreamRemote + "/" + branch - cmd := exec.Command("git", "-C", g.Dir, "checkout", targetBranch, "--", file) - if err := cmd.Run(); err != nil { - return err // Return the error if the file does not exist or any other git error occurs - } - - return nil + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() } // CreateAndCheckoutBranch creates and checks out to a given branch. @@ -190,12 +178,7 @@ func (g *Git) CreateAndCheckoutBranch(branch string) error { cmd := exec.Command("git", "-C", g.Dir, "checkout", "-b", branch) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - logrus.Errorf("error while creating and checking out branch: %s; err: %v", branch, err) - return fmt.Errorf("error while creating and checking out branch: %s", err) - } - - return nil + return cmd.Run() } // IsClean checks if the git repository is clean and, @@ -237,22 +220,12 @@ func (g *Git) StatusProcelain(debug bool) (bool, error) { // equivalent to: git add -A && git commit -m message func (g *Git) AddAndCommit(message string) error { // Stage all changes, including deletions - cmd := exec.Command("git", "-C", g.Dir, "add", "-A") - if err := cmd.Run(); err != nil { - errAdd := fmt.Errorf("error while adding changes: %w", err) - logrus.Error(errAdd) - return errAdd + if err := exec.Command("git", "-C", g.Dir, "add", "-A").Run(); err != nil { + return err } // Commit the staged changes - cmd = exec.Command("git", "commit", "-m", message) - if err := cmd.Run(); err != nil { - errCommit := fmt.Errorf("error while committing changes: %w", err) - logrus.Error(errCommit) - return errCommit - } - - return nil + return exec.Command("git", "commit", "-m", message).Run() } // PushBranch pushes the current branch to a given remote name @@ -260,23 +233,12 @@ func (g *Git) PushBranch(remote, branch string) error { cmd := exec.Command("git", "-C", g.Dir, "push", remote, branch) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - logrus.Errorf("error while pushing branch: %s; err: %v", branch, err) - return fmt.Errorf("error while pushing branch: %s", err) - } - return nil + return cmd.Run() } // DeleteBranch deletes the given branch func (g *Git) DeleteBranch(branch string) error { - cmd := exec.Command("git", "-C", g.Dir, "branch", "-D", branch) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - logrus.Errorf("error while deleting branch: %s; err: %v", g.Branch, err) - return fmt.Errorf("error while deleting branch: %s", err) - } - return nil + return exec.Command("git", "-C", g.Dir, "branch", "-D", branch).Run() } // CheckFileExists checks if a file exists in the git repository for a specific branch