Skip to content

Commit f46bd8c

Browse files
authored
Merge pull request #132 from nicholasSUSE/make-lifecycle-assets
Make lifecycle assets
2 parents a7001e3 + 125a7f0 commit f46bd8c

File tree

11 files changed

+1138
-0
lines changed

11 files changed

+1138
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ go 1.18
88
replace k8s.io/client-go => k8s.io/client-go v0.24.3
99

1010
require (
11+
github.com/Masterminds/semver v1.5.0
1112
github.com/blang/semver v3.5.1+incompatible
1213
github.com/go-git/go-billy/v5 v5.3.1
1314
github.com/go-git/go-git/v5 v5.4.2

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YH
5656
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
5757
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
5858
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
59+
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
5960
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
6061
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
6162
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=

main.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/rancher/charts-build-scripts/pkg/filesystem"
1212
"github.com/rancher/charts-build-scripts/pkg/helm"
1313
"github.com/rancher/charts-build-scripts/pkg/images"
14+
"github.com/rancher/charts-build-scripts/pkg/lifecycle"
1415
"github.com/rancher/charts-build-scripts/pkg/options"
1516
"github.com/rancher/charts-build-scripts/pkg/path"
1617
"github.com/rancher/charts-build-scripts/pkg/puller"
@@ -38,6 +39,8 @@ const (
3839
DefaultPorcelainEnvironmentVariable = "PORCELAIN"
3940
// DefaultCacheEnvironmentVariable is the default environment variable that indicates that a cache should be used on pulls to remotes
4041
DefaultCacheEnvironmentVariable = "USE_CACHE"
42+
// DefaultDebugEnvironmentVariable is the default environment variable that indicates that debug mode should be enabled
43+
DefaultDebugEnvironmentVariable = "DEBUG"
4144
)
4245

4346
var (
@@ -64,6 +67,8 @@ var (
6467
RemoteMode bool
6568
// CacheMode indicates that caching should be used on all remotely pulled resources
6669
CacheMode = false
70+
// DebugMode indicates that debug mode should be enabled
71+
DebugMode = false
6772
)
6873

6974
func main() {
@@ -116,6 +121,16 @@ func main() {
116121
Destination: &CacheMode,
117122
EnvVar: DefaultCacheEnvironmentVariable,
118123
}
124+
branchVersionFlag := cli.StringFlag{
125+
Name: "branch-version",
126+
Usage: "Available inputs: (2.5; 2.6; 2.7; 2.8; 2.9). The branch version to compare against. This is used to determine which assets to remove from the repository. ",
127+
}
128+
debugFlag := cli.BoolFlag{
129+
Name: "debugFlag",
130+
Usage: "Enable debug mode",
131+
Destination: &DebugMode,
132+
EnvVar: DefaultDebugEnvironmentVariable,
133+
}
119134
app.Commands = []cli.Command{
120135
{
121136
Name: "list",
@@ -236,6 +251,12 @@ func main() {
236251
Action: downloadIcon,
237252
Flags: []cli.Flag{packageFlag, configFlag, cacheFlag},
238253
},
254+
{
255+
Name: "lifecycle-assets",
256+
Usage: "Clean up assets that don't belong on this branch",
257+
Action: lifecycleAssetsClean,
258+
Flags: []cli.Flag{branchVersionFlag, chartFlag, debugFlag},
259+
},
239260
}
240261

241262
if err := app.Run(os.Args); err != nil {
@@ -534,3 +555,18 @@ func checkRCTagsAndVersions(c *cli.Context) {
534555

535556
logrus.Info("RC check has succeeded")
536557
}
558+
559+
func lifecycleAssetsClean(c *cli.Context) {
560+
// Initialize dependencies with branch-version, current chart and debug mode
561+
repoRoot := getRepoRoot()
562+
lifeCycleDep, err := lifecycle.InitDependencies(repoRoot, c.String("branch-version"), CurrentChart, DebugMode)
563+
if err != nil {
564+
logrus.Fatalf("encountered error while initializing dependencies for lifecycle-assets-clean: %s", err)
565+
}
566+
567+
// Apply versioning rules
568+
err = lifeCycleDep.ApplyRules(CurrentChart, DebugMode)
569+
if err != nil {
570+
logrus.Fatalf("Failed to apply versioning rules for lifecycle-assets-clean: %s", err)
571+
}
572+
}

pkg/lifecycle/git.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package lifecycle
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
)
8+
9+
func checkIfGitIsClean(debug bool) (bool, error) {
10+
cmd := exec.Command("git", "status", "--porcelain")
11+
if debug {
12+
cmd := exec.Command("git", "status")
13+
cmd.Stdout = os.Stdout
14+
cmd.Stderr = os.Stderr
15+
cmd.Run()
16+
}
17+
output, err := cmd.CombinedOutput()
18+
if err != nil {
19+
return false, fmt.Errorf("error while checking if git is clean: %s", err)
20+
}
21+
if len(output) > 0 {
22+
return false, nil
23+
}
24+
return true, nil
25+
}
26+
27+
func gitAddAndCommit(message string) error {
28+
29+
// Stage all changes, including deletions
30+
cmd := exec.Command("git", "add", "-A")
31+
if err := cmd.Run(); err != nil {
32+
return err
33+
}
34+
35+
// Commit the staged changes
36+
cmd = exec.Command("git", "commit", "-m", message)
37+
if err := cmd.Run(); err != nil {
38+
return err
39+
}
40+
41+
return nil
42+
}

pkg/lifecycle/lifecycle.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
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

Comments
 (0)