|
| 1 | +package lifecycle |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "os/exec" |
| 7 | + |
| 8 | + "github.com/go-git/go-billy/v5" |
| 9 | + "github.com/rancher/charts-build-scripts/pkg/filesystem" |
| 10 | + "github.com/rancher/charts-build-scripts/pkg/path" |
| 11 | + "github.com/sirupsen/logrus" |
| 12 | +) |
| 13 | + |
| 14 | +// Asset represents an asset with its version and path in the repository |
| 15 | +type Asset struct { |
| 16 | + version string |
| 17 | + path string |
| 18 | +} |
| 19 | + |
| 20 | +// Dependencies holds the necessary filesystem, |
| 21 | +// assets versions map, version rules and methods to apply the lifecycle rules in the target branch |
| 22 | +type Dependencies struct { |
| 23 | + rootFs billy.Filesystem |
| 24 | + assetsVersionsMap map[string][]Asset |
| 25 | + vr *VersionRules |
| 26 | + // These wrappers are used to mock the filesystem and git status in the tests |
| 27 | + walkDirWrapper WalkDirFunc |
| 28 | + makeRemoveWrapper MakeRemoveFunc |
| 29 | + checkIfGitIsCleanWrapper CheckIfGitIsCleanFunc |
| 30 | + gitAddAndCommitWrapper GitAddAndCommitFunc |
| 31 | +} |
| 32 | + |
| 33 | +// Function types to be mocked in the tests and used on Dependencies struct methods |
| 34 | + |
| 35 | +// WalkDirFunc is a function type that will be used to walk through the filesystem |
| 36 | +type WalkDirFunc func(fs billy.Filesystem, dirPath string, doFunc filesystem.RelativePathFunc) error |
| 37 | + |
| 38 | +// MakeRemoveFunc is a function type that will be used to execute make remove |
| 39 | +type MakeRemoveFunc func(chart, version string, debug bool) error |
| 40 | + |
| 41 | +// CheckIfGitIsCleanFunc is a function type that will be used to check if the git tree is clean |
| 42 | +type CheckIfGitIsCleanFunc func(debug bool) (bool, error) |
| 43 | + |
| 44 | +// GitAddAndCommitFunc is a function type that will be used to add and commit changes in the git tree |
| 45 | +type GitAddAndCommitFunc func(message string) error |
| 46 | + |
| 47 | +// cycleLog is a function to log debug messages if debug mode is enabled |
| 48 | +func cycleLog(debugMode bool, msg string, data interface{}) { |
| 49 | + if debugMode { |
| 50 | + if data != nil { |
| 51 | + logrus.Debugf("%s: %#+v \n", msg, data) |
| 52 | + } else { |
| 53 | + logrus.Debug(msg) |
| 54 | + } |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +// InitDependencies will check the filesystem, branch version, |
| 59 | +// git status, initialize the Dependencies struct and populate it. |
| 60 | +// If anything fails the operation will be aborted. |
| 61 | +func InitDependencies(repoRoot, branchVersion string, currentChart string, debug bool) (*Dependencies, error) { |
| 62 | + logrus.SetFormatter(&logrus.TextFormatter{ |
| 63 | + DisableQuote: true, |
| 64 | + }) |
| 65 | + var err error |
| 66 | + // Create the Dependencies struct which will be used for the entire process |
| 67 | + dep := &Dependencies{ |
| 68 | + walkDirWrapper: filesystem.WalkDir, // Assign the WalkDir function to the wrapper |
| 69 | + makeRemoveWrapper: makeRemove, // Assign the makeRemove function to the wrapper |
| 70 | + checkIfGitIsCleanWrapper: checkIfGitIsClean, // Assign the checkIfGitIsClean function to the wrapper |
| 71 | + gitAddAndCommitWrapper: gitAddAndCommit, // Assign the gitAddAndCommit function to the wrapper |
| 72 | + } |
| 73 | + |
| 74 | + // Git tree must be clean before proceeding with removing charts |
| 75 | + clean, err := dep.checkIfGitIsCleanWrapper(debug) |
| 76 | + if !clean { |
| 77 | + return nil, fmt.Errorf("git is not clean, it must be clean before proceeding with removing charts") |
| 78 | + } |
| 79 | + if err != nil { |
| 80 | + return nil, err |
| 81 | + } |
| 82 | + |
| 83 | + cycleLog(debug, "Getting branch version rules for: ", branchVersion) |
| 84 | + // Initialize and check version rules for the current branch |
| 85 | + dep.vr, err = GetVersionRules(branchVersion, debug) |
| 86 | + if err != nil { |
| 87 | + return nil, fmt.Errorf("encountered error while getting current branch version: %s", err) |
| 88 | + } |
| 89 | + |
| 90 | + // Get the filesystem and index.yaml path for the repository |
| 91 | + dep.rootFs = filesystem.GetFilesystem(repoRoot) |
| 92 | + |
| 93 | + // Check if the assets folder and Helm index file exists in the repository |
| 94 | + exists, err := filesystem.PathExists(dep.rootFs, path.RepositoryAssetsDir) |
| 95 | + if err != nil { |
| 96 | + return nil, fmt.Errorf("encountered error while checking if assets folder already exists in repository: %s", err) |
| 97 | + } |
| 98 | + if !exists { |
| 99 | + return nil, fmt.Errorf("assets folder does not exist in the repository") |
| 100 | + } |
| 101 | + exists, err = filesystem.PathExists(dep.rootFs, path.RepositoryHelmIndexFile) |
| 102 | + if err != nil { |
| 103 | + return nil, fmt.Errorf("encountered error while checking if Helm index file already exists in repository: %s", err) |
| 104 | + } |
| 105 | + if !exists { |
| 106 | + return nil, fmt.Errorf("Helm index file does not exist in the repository") |
| 107 | + } |
| 108 | + |
| 109 | + // Get the absolute path of the Helm index file and assets versions map to apply rules |
| 110 | + helmIndexPath := filesystem.GetAbsPath(dep.rootFs, path.RepositoryHelmIndexFile) |
| 111 | + dep.assetsVersionsMap, err = getAssetsMapFromIndex(helmIndexPath, currentChart, debug) |
| 112 | + if len(dep.assetsVersionsMap) == 0 { |
| 113 | + return nil, fmt.Errorf("no assets found in the repository") |
| 114 | + } |
| 115 | + if err != nil { |
| 116 | + return nil, err // Abort and return error if the assets map is empty |
| 117 | + } |
| 118 | + |
| 119 | + return dep, nil |
| 120 | +} |
| 121 | + |
| 122 | +// ApplyRules will populate all assets versions and paths, sort the versions, |
| 123 | +// and execute make remove for each chart and version. |
| 124 | +// After each chart removal, it will commit the changes in a single commit |
| 125 | +// for all versions of that chart. |
| 126 | +func (ld *Dependencies) ApplyRules(currentChart string, debug bool) error { |
| 127 | + // Populate the assets versions and paths combining the index.yaml and assets folder |
| 128 | + err := ld.populateAssetsVersionsPath(debug) |
| 129 | + if err != nil { |
| 130 | + return err |
| 131 | + } |
| 132 | + // Sort the versions before removing |
| 133 | + ld.sortAssetsVersions() |
| 134 | + |
| 135 | + // Execute make remove for each chart and version that is not in the lifecycle. |
| 136 | + // Commit after each chart removal. |
| 137 | + removedAssetsVersions, err := ld.removeAssetsVersions(debug) |
| 138 | + if err != nil { |
| 139 | + return err |
| 140 | + } |
| 141 | + |
| 142 | + if len(removedAssetsVersions) == 0 { |
| 143 | + logrus.Infof("No assets were removed") |
| 144 | + } |
| 145 | + |
| 146 | + logrus.Infof("Removed a total of %d assets", len(removedAssetsVersions)) |
| 147 | + cycleLog(debug, "Removed assets", removedAssetsVersions) |
| 148 | + |
| 149 | + return nil |
| 150 | +} |
| 151 | + |
| 152 | +// removeAssetsVersions will iterate through assetsVersionsMap and remove the versions that are not in the lifecycle committing the changes |
| 153 | +func (ld *Dependencies) removeAssetsVersions(debug bool) (map[string][]Asset, error) { |
| 154 | + logrus.Info("Executing make remove") |
| 155 | + |
| 156 | + // Save what was removed for validation |
| 157 | + var removedAssetsVersionsMap map[string][]Asset = make(map[string][]Asset) |
| 158 | + var removedAssetsVersion []Asset |
| 159 | + |
| 160 | + // Loop through the assetsVersionsMap, i.e: entries in the index.yaml |
| 161 | + for chartName, assetsVersionsMap := range ld.assetsVersionsMap { |
| 162 | + cycleLog(debug, "Chart name", chartName) |
| 163 | + |
| 164 | + // Reset the slice for the next iteration |
| 165 | + removedAssetsVersion = nil |
| 166 | + |
| 167 | + // Skip if there are no versions to remove |
| 168 | + if len(assetsVersionsMap) == 0 { |
| 169 | + cycleLog(debug, "Skipping... no versions found for", chartName) |
| 170 | + continue |
| 171 | + } |
| 172 | + |
| 173 | + // Loop through the versions of the asset and remove the ones that are not in the lifecycle |
| 174 | + for _, asset := range assetsVersionsMap { |
| 175 | + isVersionInLifecycle := ld.vr.checkChartVersionForLifecycle(asset.version) |
| 176 | + if isVersionInLifecycle { |
| 177 | + logrus.Debugf("Version %s is in lifecycle for %s", asset.version, chartName) |
| 178 | + continue // Skipping version in lifecycle |
| 179 | + } else { |
| 180 | + err := ld.makeRemoveWrapper(chartName, asset.version, debug) |
| 181 | + if err != nil { |
| 182 | + logrus.Errorf("Error while removing %s version %s: %s", chartName, asset.version, err) |
| 183 | + return nil, err // Abort and return error if the removal fails |
| 184 | + } |
| 185 | + // Saving removed asset version |
| 186 | + removedAssetsVersion = append(removedAssetsVersion, asset) |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + // If no versions were removed from the existing ones, do not commit. |
| 191 | + clean, err := ld.checkIfGitIsCleanWrapper(debug) |
| 192 | + if err != nil { |
| 193 | + return nil, err |
| 194 | + } |
| 195 | + if clean { |
| 196 | + logrus.Infof("No versions were removed for %s", chartName) |
| 197 | + continue // Skipping |
| 198 | + } |
| 199 | + |
| 200 | + // Commit each chart removal versions in a single commit |
| 201 | + err = ld.gitAddAndCommitWrapper(fmt.Sprintf("Remove %s versions", chartName)) |
| 202 | + if err != nil { |
| 203 | + logrus.Errorf("Error while committing the removal of %s versions: %s", chartName, err) |
| 204 | + return nil, err // Abort and return error if the commit fails |
| 205 | + } |
| 206 | + |
| 207 | + // Saving removed asset versions |
| 208 | + removedAssetsVersionsMap[chartName] = removedAssetsVersion |
| 209 | + } |
| 210 | + |
| 211 | + logrus.Info("lifecycle-assets-clean is Done!") |
| 212 | + return removedAssetsVersionsMap, nil |
| 213 | +} |
| 214 | + |
| 215 | +// makeRemove will execute make remove script to a specific chart and version |
| 216 | +func makeRemove(chart, version string, debug bool) error { |
| 217 | + |
| 218 | + chartArg := fmt.Sprintf("CHART=%s", chart) |
| 219 | + versionArg := fmt.Sprintf("VERSION=%s", version) |
| 220 | + logrus.Infof("Executing > make remove %s %s \n", chartArg, versionArg) |
| 221 | + cmd := exec.Command("make", "remove", chartArg, versionArg) |
| 222 | + |
| 223 | + if debug { |
| 224 | + cmd.Stdout = os.Stdout |
| 225 | + cmd.Stderr = os.Stderr |
| 226 | + } |
| 227 | + |
| 228 | + err := cmd.Run() |
| 229 | + if err != nil { |
| 230 | + return fmt.Errorf("cmd.Run() failed for chart:%s and version:%s with error:%w", |
| 231 | + chart, version, err) |
| 232 | + } |
| 233 | + return nil |
| 234 | +} |
0 commit comments