From 0cf2bd5b354e23bd0df81d7702aefb5d8aafe384 Mon Sep 17 00:00:00 2001 From: Matias Pan Date: Mon, 1 Apr 2024 17:46:00 -0300 Subject: [PATCH] Implement more flexible release filtering rules This commit adds three new options: - KeepReleases: how many releases should be returned starting from the latest - OnlyLatestMinor: if true returns only the latest minor.patch per major - OnlyLatestPatch: if true returns only the latest patch for each minor. OnlyLatestMinor has precedence over this Signed-off-by: Matias Pan --- README.md | 9 ++- k8s/install.yaml | 2 +- main.go | 74 +++++++++++++++++++--- main_test.go | 159 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 main_test.go diff --git a/README.md b/README.md index 06da951..90f9a96 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ This project is composed of 4 components: To install it in your cluster you can run: ```terminal -$ ARGOCD_TOKEN="$(echo -n '' | base64)" envsubst < https://raw.githubusercontent.com/matipan/argocd-github-release-generator/v0.0.2/k8s/install.yaml | k apply -f - +$ ARGOCD_TOKEN="$(echo -n '' | base64)" envsubst < https://raw.githubusercontent.com/matipan/argocd-github-release-generator/v0.0.3/k8s/install.yaml | k apply -f - ``` > [!TIP] > If you plan to watch private repositories or have a refresh interval lower than 1 per minute then you must specify a GITHUB_PAT. -> `$ GITHUB_PAT= ARGOCD_TOKEN="$(echo -n '' | base64)" envsubst < https://raw.githubusercontent.com/matipan/argocd-github-release-generator/v0.0.2/k8s/install.yaml | k apply -f -` +> `$ GITHUB_PAT= ARGOCD_TOKEN="$(echo -n '' | base64)" envsubst < https://raw.githubusercontent.com/matipan/argocd-github-release-generator/v0.0.3/k8s/install.yaml | k apply -f -` ## Setting up your ApplicationSet @@ -26,6 +26,11 @@ The plugin receives two parameters that you must configure: - `repository`: specify the repository that should be used to look for releases. - `min_release`: specify the starting point for the releases. This value is useful to control how many applications you generate and remove applications related to old releases. +You can optionally configure the following three parameters to further control which releases should be returned: +- `keep_releases`: specify how many releases should be kept. If you set this value to `3` then the plugin will only return the 3 most recent releases. +- `only_latest_minor`: if set to `true` then the plugin will only return the latest release for each major version. +- `only_latest_patch`: if set to `true` then the plugin will only return the latest release for each minor version. This parameter is ignored if `only_latest_minor` is set to `true`. + > [!NOTE] > At the moment this project only supports releases that follow `semver` (e.g `v0.1.0`) diff --git a/k8s/install.yaml b/k8s/install.yaml index 6b9169b..31f031c 100644 --- a/k8s/install.yaml +++ b/k8s/install.yaml @@ -53,7 +53,7 @@ spec: secretKeyRef: key: plugin.argocd-github-release-generator.github_pat name: argocd-github-release-generator - image: ghcr.io/matipan/argocd-github-release-generator:v0.0.2 + image: ghcr.io/matipan/argocd-github-release-generator:v0.0.3 imagePullPolicy: IfNotPresent name: argocd-github-release-generator ports: diff --git a/main.go b/main.go index dec45e0..36d035d 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "os" + "sort" "strings" "github.com/rs/zerolog" @@ -31,7 +32,15 @@ type Input struct { type Parameters struct { Repository string `json:"repository"` + // MinRelease is the minimum release that should be returned. If a release is less than this, it will be filtered out MinRelease string `json:"min_release"` + // KeepReleases is the number of releases to keep. If set to 0, all releases will be returned + KeepReleases int `json:"keep_releases"` + // OnlyLatestMinor is a flag that if set to true, will only return the latest minor version of a release + OnlyLatestMinor bool `json:"only_latest_minor"` + // OnlyLatestPatch is a flag that if set to true, will only return the latest patch version of a release + // if OnlyLatestMinor is set to true, this flag will be ignored + OnlyLatestPatch bool `json:"only_latest_patch"` } type Output struct { @@ -104,11 +113,18 @@ func generatorHandler(l zerolog.Logger) http.HandlerFunc { l.Debug().Msgf("fetched %d releases", len(releases)) + filtered, err := getFilteredReleases(releases, req.Input.Parameters) + if err != nil { + l.Error().Err(err).Msg("failed to filter releases") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + out := Output{ Output: struct { Parameters []Release `json:"parameters"` }{ - Parameters: getFilteredReleases(releases, req.Input.Parameters.MinRelease), + Parameters: filtered, }, } @@ -136,16 +152,60 @@ type Commit struct { URL string `json:"url"` } -func getFilteredReleases(releases []Release, minRelease string) []Release { - var filteredReleases []Release +func getFilteredReleases(releases []Release, params Parameters) ([]Release, error) { + // Sort releases in a descending order so that returning only the latest patch + // of a minor or latest minor of a major version can be done simply with a map + sort.SliceStable(releases, func(i, j int) bool { + return semver.Compare(releases[i].Name, releases[j].Name) > 0 + }) + + var ( + filteredReleases []Release + latestVersion = map[string]string{} + ) for _, r := range releases { - if semver.Compare(r.Name, minRelease) > 0 { - r.NameSlug = strings.ReplaceAll(r.Name, ".", "-") - filteredReleases = append(filteredReleases, r) + if semver.Compare(r.Name, params.MinRelease) < 0 { + continue + } + + // if we reached the amount of releases we want to keep, break out of the loop + if params.KeepReleases != 0 && len(filteredReleases) == params.KeepReleases { + break } + + version := semver.MajorMinor(r.Name) + major := semver.Major(r.Name) + + if params.OnlyLatestMinor { + if _, ok := latestVersion[major]; !ok { + latestVersion[major] = r.Name + r.NameSlug = strings.ReplaceAll(r.Name, ".", "-") + filteredReleases = append(filteredReleases, r) + continue + } + continue + } + + if params.OnlyLatestPatch { + if _, ok := latestVersion[version]; !ok { + latestVersion[version] = r.Name + r.NameSlug = strings.ReplaceAll(r.Name, ".", "-") + filteredReleases = append(filteredReleases, r) + continue + } + continue + } + + r.NameSlug = strings.ReplaceAll(r.Name, ".", "-") + filteredReleases = append(filteredReleases, r) } - return filteredReleases + // sort the releases by increasing order before returning them + sort.SliceStable(filteredReleases, func(i, j int) bool { + return semver.Compare(filteredReleases[i].Name, filteredReleases[j].Name) < 0 + }) + + return filteredReleases, nil } func getReleases(ctx context.Context, repo string) ([]Release, error) { diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..8d8863c --- /dev/null +++ b/main_test.go @@ -0,0 +1,159 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestGetFilteredReleases(t *testing.T) { + cases := []struct { + name string + params Parameters + releases []Release + expectedReleases []Release + }{ + { + name: "no filter", + params: Parameters{ + MinRelease: "v0.0.0", + }, + releases: []Release{ + {Name: "v0.0.0"}, + {Name: "v0.0.1"}, + {Name: "v0.1.0"}, + {Name: "v1.0.0"}, + }, + expectedReleases: []Release{ + {Name: "v0.0.0", NameSlug: "v0-0-0"}, + {Name: "v0.0.1", NameSlug: "v0-0-1"}, + {Name: "v0.1.0", NameSlug: "v0-1-0"}, + {Name: "v1.0.0", NameSlug: "v1-0-0"}, + }, + }, + { + name: "only latest minor", + params: Parameters{ + MinRelease: "v0.0.0", + OnlyLatestMinor: true, + }, + releases: []Release{ + {Name: "v0.0.0"}, + {Name: "v0.0.1"}, + {Name: "v0.1.0"}, + {Name: "v0.1.1"}, + {Name: "v1.0.0"}, + {Name: "v1.0.1"}, + }, + expectedReleases: []Release{ + {Name: "v0.1.1", NameSlug: "v0-1-1"}, + {Name: "v1.0.1", NameSlug: "v1-0-1"}, + }, + }, + { + name: "only latest patch", + params: Parameters{ + MinRelease: "v0.0.0", + OnlyLatestPatch: true, + }, + releases: []Release{ + {Name: "v0.0.0"}, + {Name: "v0.0.1"}, + {Name: "v0.1.0"}, + {Name: "v0.1.1"}, + }, + expectedReleases: []Release{ + {Name: "v0.0.1", NameSlug: "v0-0-1"}, + {Name: "v0.1.1", NameSlug: "v0-1-1"}, + }, + }, + { + name: "only latest minor has precedence", + params: Parameters{ + MinRelease: "v0.0.0", + OnlyLatestMinor: true, + OnlyLatestPatch: true, + }, + releases: []Release{ + {Name: "v0.0.0"}, + {Name: "v0.0.1"}, + {Name: "v0.1.0"}, + {Name: "v0.1.1"}, + {Name: "v1.0.0"}, + {Name: "v1.0.1"}, + }, + expectedReleases: []Release{ + {Name: "v0.1.1", NameSlug: "v0-1-1"}, + {Name: "v1.0.1", NameSlug: "v1-0-1"}, + }, + }, + { + name: "only latest minor combined with min release", + params: Parameters{ + MinRelease: "v0.1.2", + OnlyLatestMinor: true, + }, + releases: []Release{ + {Name: "v0.0.0"}, + {Name: "v0.0.1"}, + {Name: "v0.1.0"}, + {Name: "v0.1.1"}, + {Name: "v1.0.0"}, + {Name: "v1.0.1"}, + }, + expectedReleases: []Release{ + {Name: "v1.0.1", NameSlug: "v1-0-1"}, + }, + }, + { + name: "keep releases returns only the amount of releases we want to keep", + params: Parameters{ + MinRelease: "v0.1.2", + KeepReleases: 2, + }, + releases: []Release{ + {Name: "v0.0.0"}, + {Name: "v0.0.1"}, + {Name: "v0.1.0"}, + {Name: "v0.1.1"}, + {Name: "v1.0.0"}, + {Name: "v1.0.1"}, + }, + expectedReleases: []Release{ + {Name: "v1.0.0", NameSlug: "v1-0-0"}, + {Name: "v1.0.1", NameSlug: "v1-0-1"}, + }, + }, + { + name: "keep releases plus only latest minor returns right releases", + params: Parameters{ + MinRelease: "v0.1.0", + KeepReleases: 1, + OnlyLatestMinor: true, + }, + releases: []Release{ + {Name: "v0.0.0"}, + {Name: "v0.0.1"}, + {Name: "v0.1.0"}, + {Name: "v0.1.1"}, + {Name: "v1.0.0"}, + {Name: "v1.0.1"}, + }, + expectedReleases: []Release{ + {Name: "v1.0.1", NameSlug: "v1-0-1"}, + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + releases, err := getFilteredReleases(test.releases, test.params) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(releases, test.expectedReleases) { + t.Errorf("expected releases to be %v, got %v", test.expectedReleases, releases) + } + }) + } +}