diff --git a/Makefile b/Makefile index c3e565e..f8b2ff1 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ test: go test -v -cover -race ./... +dev: + go run main.go start -v debug --config-file config/config.yml + start: - go run main.go start --debug + go run main.go start -v debug lint: golangci-lint run diff --git a/config/_testdata/config-success.yml b/config/_testdata/config-success.yml index 8bdc1aa..4fa3cd6 100644 --- a/config/_testdata/config-success.yml +++ b/config/_testdata/config-success.yml @@ -17,6 +17,7 @@ sources: - "./internal/app/_testdata/test_chart2/Chart.yaml" - "./internal/app/_testdata/test_chart3/Chart.yaml" - "./internal/app/_testdata/test_chart4/Chart.yaml" + - "./internal/app/_testdata/test_chart5/Chart.yaml" filters: stabilityDays: 21 skipPrerelease: true diff --git a/config/dev.config.yml b/config/dev.config.yml new file mode 100644 index 0000000..02e32d7 --- /dev/null +++ b/config/dev.config.yml @@ -0,0 +1,29 @@ +global: + interval: 10m + aws: + region: us-east-1 +sources: + argocdHelm: + - enabled: false + filesystemHelm: + - enabled: true + paths: + - "./internal/app/_testdata/test_chart/Chart.yaml" + - "./internal/app/_testdata/test_chart2/Chart.yaml" + - "./internal/app/_testdata/test_chart3/Chart.yaml" + - "./internal/app/_testdata/test_chart4/Chart.yaml" + - "./internal/app/_testdata/test_chart5/Chart.yaml" + filters: + semver-versions: + remove-pre-release: true + remove-first-major-version: true + recent-versions: + days: 21 + repositories-aliases: + "@my-alias": "https://prometheus-community.github.io/helm-charts" +http: + host: 0.0.0.0 + port: 10000 + write-timeout: 10 + read-timeout: 10 + read-header-timeout: 10 diff --git a/internal/app/_testdata/test_chart4/Chart.yaml b/internal/app/_testdata/test_chart4/Chart.yaml index 47d3ff7..27feb37 100644 --- a/internal/app/_testdata/test_chart4/Chart.yaml +++ b/internal/app/_testdata/test_chart4/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 appVersion: v2.3.3 -description: A Helm chart for ArgoCD, a declarative, GitOps continuous delivery tool for Kubernetes. -name: argo-cd +description: kube-prometheus-stack collects Kubernetes manifests, Grafana dashboards, and Prometheus rules +name: test-app-5 version: 4.5.8 dependencies: - name: kube-prometheus-stack diff --git a/internal/app/_testdata/test_chart5/Chart.yaml b/internal/app/_testdata/test_chart5/Chart.yaml new file mode 100644 index 0000000..c3eeaa5 --- /dev/null +++ b/internal/app/_testdata/test_chart5/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +appVersion: v2.3.3 +description: A Helm chart for ArgoCD, a declarative, GitOps continuous delivery tool for Kubernetes. +name: TestApp5 +version: 4.5.8 +dependencies: + - name: kube-prometheus-stack + version: 60.0.0 + repository: '@my-alias' diff --git a/internal/app/sources/argohelm/config.go b/internal/app/sources/argohelm/config.go index e3353ac..c5513d2 100644 --- a/internal/app/sources/argohelm/config.go +++ b/internal/app/sources/argohelm/config.go @@ -6,13 +6,14 @@ import ( ) type Config struct { - Enabled bool `yaml:"enabled"` - Name string `yaml:"name"` - ClusterURL string `yaml:"cluster-url"` - ArgoCDNamespace string `yaml:"argocd-namespace" validate:"required"` - GitSecretsNamespace string `yaml:"git-credentials-secrets-namespace" validate:"required"` - GitCredentialsSecretsPattern string `yaml:"git-credentials-secrets-pattern" validate:"required"` - Filters FiltersConfig `yaml:"filters"` + Enabled bool `yaml:"enabled"` + Name string `yaml:"name"` + ClusterURL string `yaml:"cluster-url"` + ArgoCDNamespace string `yaml:"argocd-namespace" validate:"required"` + GitSecretsNamespace string `yaml:"git-credentials-secrets-namespace" validate:"required"` + GitCredentialsSecretsPattern string `yaml:"git-credentials-secrets-pattern" validate:"required"` + Filters FiltersConfig `yaml:"filters"` + RepositoriesAliases map[string]string `yaml:"repositories-aliases"` } type FiltersConfig struct { diff --git a/internal/app/sources/argohelm/source.go b/internal/app/sources/argohelm/source.go index 210b5ba..0928d9c 100644 --- a/internal/app/sources/argohelm/source.go +++ b/internal/app/sources/argohelm/source.go @@ -87,12 +87,12 @@ func (s *Source) Load() ([]*soft.Software, error) { s.log.Warn(fmt.Sprintf("Could not convert argo app %s to software: %s", app.Name, err)) continue } - err = versions.PopulateTopLevelSoftware(s.s3Api, s.log, topLevelSoftware, app.RepoURL, app.Chart, s.versionFilter) + err = versions.PopulateTopLevelSoftware(s.s3Api, s.log, s.cfg.RepositoriesAliases, topLevelSoftware, app.RepoURL, app.Chart, s.versionFilter) if err != nil { s.log.Warn(fmt.Sprintf("Could not populate top level software %s: %s", app.Name, err)) } if chart != nil { - err = versions.PopulateSoftwareDependencies(s.s3Api, s.log, topLevelSoftware, chart, ArgoHelm, s.versionFilter) + err = versions.PopulateSoftwareDependencies(s.s3Api, s.log, s.cfg.RepositoriesAliases, topLevelSoftware, chart, ArgoHelm, s.versionFilter) if err != nil { s.log.Error(fmt.Sprintf("Could not load %s chart as software, error: %s", chart.Name(), err)) continue diff --git a/internal/app/sources/deployments/config.go b/internal/app/sources/deployments/config.go index bfe50cc..250ed6a 100644 --- a/internal/app/sources/deployments/config.go +++ b/internal/app/sources/deployments/config.go @@ -6,9 +6,10 @@ import ( ) type Config struct { - Name string `yaml:"name"` - Namespace string `yaml:"namespace"` - LabelSelector map[string]string `yaml:"label-selector"` - Registries map[string]registry.Config `yaml:"registries"` - Filters filters.Config `yaml:"filters"` + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + LabelSelector map[string]string `yaml:"label-selector"` + Registries map[string]registry.Config `yaml:"registries"` + Filters filters.Config `yaml:"filters"` + RepositoriesAliases map[string]string `yaml:"repositories-aliases"` } diff --git a/internal/app/sources/filesystemhelm/config.go b/internal/app/sources/filesystemhelm/config.go index aae6821..f79bcbb 100644 --- a/internal/app/sources/filesystemhelm/config.go +++ b/internal/app/sources/filesystemhelm/config.go @@ -5,7 +5,8 @@ import ( ) type Config struct { - Enabled bool `yaml:"enabled"` - Paths []string `yaml:"paths" validate:"dive,file"` - Filters filters.Config `yaml:"filters"` + Enabled bool `yaml:"enabled"` + Paths []string `yaml:"paths" validate:"dive,file"` + Filters filters.Config `yaml:"filters"` + RepositoriesAliases map[string]string `yaml:"repositories-aliases"` } diff --git a/internal/app/sources/filesystemhelm/source.go b/internal/app/sources/filesystemhelm/source.go index f982b83..e1ff414 100644 --- a/internal/app/sources/filesystemhelm/source.go +++ b/internal/app/sources/filesystemhelm/source.go @@ -60,7 +60,7 @@ func (s *Source) Load() ([]*soft.Software, error) { Type: FileSystemHelm, } - err := versions.PopulateSoftwareDependencies(s.s3Api, s.log, topLevelSoftware, &chart, FileSystemHelm, s.filter) + err := versions.PopulateSoftwareDependencies(s.s3Api, s.log, s.cfg.RepositoriesAliases, topLevelSoftware, &chart, FileSystemHelm, s.filter) if err != nil { s.log.Error(fmt.Sprintf("Could not load %s chart as software, error: %s", chart.Name(), err)) continue diff --git a/internal/app/sources/helm/versions/repo_backend.go b/internal/app/sources/helm/versions/repo_backend.go index 13ad44c..f739584 100644 --- a/internal/app/sources/helm/versions/repo_backend.go +++ b/internal/app/sources/helm/versions/repo_backend.go @@ -70,7 +70,12 @@ func getRepoBackendType(repoUrl string) (RepoBackendType, error) { return "", fmt.Errorf("could not determine RepoBackendType for url %s", repoUrl) } -func buildRepoBackend(repoURL string, chartName string, log *slog.Logger, s3Api aws.S3Api) (RepoBackend, error) { +func buildRepoBackend(repoAliases map[string]string, repoURL string, chartName string, log *slog.Logger, s3Api aws.S3Api) (RepoBackend, error) { + if repoAliases != nil && repoAliases[repoURL] != "" { + log.Debug(fmt.Sprintf("Repo URL %s resolved to %s", repoURL, repoAliases[repoURL])) + repoURL = repoAliases[repoURL] + } + repoType, err := getRepoBackendType(repoURL) if err != nil { return nil, err diff --git a/internal/app/sources/helm/versions/repo_backend_test.go b/internal/app/sources/helm/versions/repo_backend_test.go index f004aec..dd89046 100644 --- a/internal/app/sources/helm/versions/repo_backend_test.go +++ b/internal/app/sources/helm/versions/repo_backend_test.go @@ -1,6 +1,12 @@ package versions -import "testing" +import ( + "log/slog" + "testing" + + "github.com/qonto/upgrade-manager/internal/infra/aws" + "github.com/stretchr/testify/assert" +) func TestGetRepoBackendType(t *testing.T) { testCases := map[string]RepoBackendType{ @@ -24,3 +30,92 @@ func TestGetRepoBackendType(t *testing.T) { } } } + +func TestBuildRepoBackend(t *testing.T) { + logger := slog.Default() + mockS3Api := new(aws.S3Mock) + + tests := []struct { + name string + repoAliases map[string]string + repoURL string + chartName string + expectedType interface{} + expectError bool + }{ + { + name: "HelmRepo", + repoAliases: nil, + repoURL: "https://charts.helm.sh/stable", + chartName: "mysql", + expectedType: &HelmRepoBackend{}, + expectError: false, + }, + { + name: "S3HelmRepo", + repoAliases: nil, + repoURL: "s3://my-bucket/charts", + chartName: "my-chart", + expectedType: &S3HelmRepoBackend{}, + expectError: false, + }, + { + name: "GitRepo", + repoAliases: nil, + repoURL: "https://github.com/user/repo.git", + chartName: "my-chart", + expectedType: nil, + expectError: false, + }, + { + name: "Invalid URL", + repoAliases: nil, + repoURL: "invalid://url", + chartName: "my-chart", + expectedType: nil, + expectError: true, + }, + { + name: "With Repo Alias", + repoAliases: map[string]string{"@alias": "https://charts.helm.sh/stable"}, + repoURL: "@alias", + chartName: "mysql", + expectedType: &HelmRepoBackend{}, + expectError: false, + }, + { + name: "With Repo Alias targeting a S3 bucket", + repoAliases: map[string]string{"alias": "s3://my-bucket/charts"}, + repoURL: "alias", + chartName: "my-chart", + expectedType: &S3HelmRepoBackend{}, + expectError: false, + }, + { + name: "With Repo Alias targeting a Git repo", + repoAliases: map[string]string{"@alias": "https://github.com/user/repo.git"}, + repoURL: "@alias", + chartName: "my-chart", + expectedType: nil, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + backend, err := buildRepoBackend(tt.repoAliases, tt.repoURL, tt.chartName, logger, mockS3Api) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if tt.expectedType == nil { + assert.Nil(t, backend) + } else { + assert.IsType(t, tt.expectedType, backend) + } + }) + } +} diff --git a/internal/app/sources/helm/versions/versions.go b/internal/app/sources/helm/versions/versions.go index 7a711bb..d600bbb 100644 --- a/internal/app/sources/helm/versions/versions.go +++ b/internal/app/sources/helm/versions/versions.go @@ -18,9 +18,9 @@ import ( "helm.sh/helm/v3/pkg/repo" ) -func PopulateTopLevelSoftware(s3Api aws.S3Api, log *slog.Logger, topLevelSoftware *soft.Software, repoURL string, chartName string, filter filters.Filter) error { +func PopulateTopLevelSoftware(s3Api aws.S3Api, log *slog.Logger, repoAliases map[string]string, topLevelSoftware *soft.Software, repoURL string, chartName string, filter filters.Filter) error { log.Debug(fmt.Sprintf("Populate top level software for chart %s repo backend %s", chartName, repoURL)) - repoBackend, err := buildRepoBackend(repoURL, chartName, log, s3Api) + repoBackend, err := buildRepoBackend(repoAliases, repoURL, chartName, log, s3Api) if err != nil { return err } @@ -34,7 +34,7 @@ func PopulateTopLevelSoftware(s3Api aws.S3Api, log *slog.Logger, topLevelSoftwar return nil } -func PopulateSoftwareDependencies(s3Api aws.S3Api, log *slog.Logger, topLevelSoftware *soft.Software, chart *chart.Chart, st soft.SoftwareType, filter filters.Filter) error { +func PopulateSoftwareDependencies(s3Api aws.S3Api, log *slog.Logger, repoAliases map[string]string, topLevelSoftware *soft.Software, chart *chart.Chart, st soft.SoftwareType, filter filters.Filter) error { softwareDependencies := []*soft.Software{} for _, dependency := range chart.Metadata.Dependencies { var depName string @@ -52,7 +52,7 @@ func PopulateSoftwareDependencies(s3Api aws.S3Api, log *slog.Logger, topLevelSof } log.Debug(fmt.Sprintf("Populate dependency software for dep %s repo backend %s", depName, dependency.Repository)) - depRepoBackend, err := buildRepoBackend(dependency.Repository, dependency.Name, log, s3Api) + depRepoBackend, err := buildRepoBackend(repoAliases, dependency.Repository, dependency.Name, log, s3Api) if err != nil { return err }