Skip to content

Commit

Permalink
Implement more flexible release filtering rules
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
matipan committed Apr 1, 2024
1 parent c4a65ee commit 0cf2bd5
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 10 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ This project is composed of 4 components:

To install it in your cluster you can run:
```terminal
$ ARGOCD_TOKEN="$(echo -n '<strong_password>' | base64)" envsubst < https://raw.githubusercontent.com/matipan/argocd-github-release-generator/v0.0.2/k8s/install.yaml | k apply -f -
$ ARGOCD_TOKEN="$(echo -n '<strong_password>' | 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=<YOUR_PAT> ARGOCD_TOKEN="$(echo -n '<strong_password>' | base64)" envsubst < https://raw.githubusercontent.com/matipan/argocd-github-release-generator/v0.0.2/k8s/install.yaml | k apply -f -`
> `$ GITHUB_PAT=<YOUR_PAT> ARGOCD_TOKEN="$(echo -n '<strong_password>' | base64)" envsubst < https://raw.githubusercontent.com/matipan/argocd-github-release-generator/v0.0.3/k8s/install.yaml | k apply -f -`
## Setting up your ApplicationSet

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`)
Expand Down
2 changes: 1 addition & 1 deletion k8s/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
74 changes: 67 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"os"
"sort"
"strings"

"github.com/rs/zerolog"
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
},
}

Expand Down Expand Up @@ -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) {
Expand Down
159 changes: 159 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 0cf2bd5

Please sign in to comment.