From 839119d54bf6709d76f8fb6216be23942f061639 Mon Sep 17 00:00:00 2001 From: Joseph Lombrozo Date: Mon, 16 Dec 2024 13:07:28 -0500 Subject: [PATCH] handle multi source applications (#298) Co-authored-by: Matt Morrison --- .mockery.yaml | 3 + .tool-versions | 1 + README.md | 4 +- charts/kubechecks/Chart.yaml | 2 +- charts/kubechecks/templates/role.yaml | 15 + charts/kubechecks/templates/rolebinding.yaml | 13 + charts/kubechecks/values.yaml | 3 + cmd/container.go | 123 --- cmd/{controller_cmd.go => controller.go} | 28 +- cmd/locations.go | 2 + cmd/process.go | 37 +- cmd/root.go | 9 +- docs/usage.md | 5 +- mocks/affected_apps/mocks/mock_Matcher.go | 6 +- mocks/affected_apps/mocks/mock_argoClient.go | 10 +- mocks/generator/mocks/mock_AppsGenerator.go | 6 +- mocks/generator/mocks/mock_Generator.go | 14 +- .../mocks/mock_IssuesServices.go | 18 +- .../mocks/mock_PullRequestsServices.go | 18 +- .../mocks/mock_RepositoriesServices.go | 22 +- .../mocks/mock_CommitsServices.go | 6 +- .../mocks/mock_MergeRequestsServices.go | 22 +- .../gitlab_client/mocks/mock_NotesServices.go | 18 +- .../mocks/mock_PipelinesServices.go | 6 +- .../mocks/mock_ProjectsServices.go | 18 +- .../mocks/mock_RepositoryFilesServices.go | 6 +- mocks/vcs/mocks/mock_Client.go | 710 ++++++++++++++++++ pkg/affected_apps/argocd_matcher.go | 9 +- pkg/app_watcher/app_watcher.go | 20 +- pkg/app_watcher/appset_watcher.go | 12 +- pkg/appdir/vcstoargomap.go | 46 +- pkg/argo_client/applications.go | 26 +- pkg/argo_client/client.go | 54 +- pkg/argo_client/manifests.go | 497 ++++++++++-- pkg/argo_client/manifests_test.go | 471 ++++++++++++ pkg/checks/diff/diff.go | 18 +- pkg/checks/kubeconform/check.go | 5 +- pkg/checks/kubeconform/validate.go | 30 +- pkg/checks/kubeconform/validate_test.go | 7 +- pkg/config/config.go | 21 +- pkg/container/main.go | 117 ++- pkg/events/check.go | 48 +- pkg/events/check_test.go | 42 +- pkg/events/runner.go | 12 +- pkg/events/worker.go | 89 ++- pkg/events/worker_test.go | 34 + pkg/git/repo.go | 9 +- pkg/repoUrl.go | 27 + pkg/repoUrl_test.go | 20 + 49 files changed, 2304 insertions(+), 435 deletions(-) create mode 100644 charts/kubechecks/templates/role.yaml create mode 100644 charts/kubechecks/templates/rolebinding.yaml delete mode 100644 cmd/container.go rename cmd/{controller_cmd.go => controller.go} (78%) create mode 100644 mocks/vcs/mocks/mock_Client.go create mode 100644 pkg/argo_client/manifests_test.go create mode 100644 pkg/events/worker_test.go diff --git a/.mockery.yaml b/.mockery.yaml index 41be59a8..88fad221 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -1,6 +1,9 @@ with-expecter: true dir: "mocks/{{.PackageName}}/mocks" packages: + github.com/zapier/kubechecks/pkg/vcs: + config: + all: true github.com/zapier/kubechecks/pkg/vcs/github_client: # place your package-specific config here config: diff --git a/.tool-versions b/.tool-versions index 9931663c..3090ad85 100644 --- a/.tool-versions +++ b/.tool-versions @@ -6,5 +6,6 @@ helm-cr 1.6.1 helm-ct 3.11.0 kubeconform 0.6.7 kustomize 5.5.0 +mockery 2.46.3 staticcheck 2024.1.1 tilt 0.33.2 diff --git a/README.md b/README.md index 08fd860e..fd221b95 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ ## Pull/Merge Request driven checks -When using ArgoCD, it can be difficult to tell just how your Pull/Merge Request (PR/MR) will impact your live deployment. `kubechecks` was designed to address this problem; every time a new PR/MR is created, `kubechecks` will automatically determine what's changed and how it will impact your `main`/default branchs state, informing you of the details directly on the PR/MR. As a bonus, it also lints and checks your Kubernetes manifests to let you know ahead of time if something is outdated, invalid, or otherwise not good practice. +When using ArgoCD, it can be difficult to tell just how your Pull/Merge Request (PR/MR) will impact your live deployment. `kubechecks` was designed to address this problem; every time a new PR/MR is created, `kubechecks` will automatically determine what's changed and how it will impact your `main`/default branch's state, informing you of the details directly on the PR/MR. As a bonus, it also lints and checks your Kubernetes manifests to let you know ahead of time if something is outdated, invalid, or otherwise not good practice. ![Demo](./docs/gif/kubechecks.gif) ### How it works -This tool provides a server function that processes webhooks from Gitlab/Github, clones the repository at the `HEAD` SHA of the PR/MR, and runs various check suites, commenting the output of each check in a single comment on your PR/MR. `kubechecks` talks directly to ArgoCD to get the live state of your deployments to ensure that you have the most accurate information about how your changes will affect your production code. +This tool provides a server function that processes webhooks from Gitlab/Github, clones the repository at the `HEAD` SHA of the PR/MR, and runs various check suites, commenting the output of each check in a single comment on your PR/MR. `kubechecks` talks directly to ArgoCD to get the live state of your deployments and talks directly to ArgoCD's repo server to generate the new resources to ensure that you have the most accurate information about how your changes will affect your production code. ### Architecture diff --git a/charts/kubechecks/Chart.yaml b/charts/kubechecks/Chart.yaml index cc1dc19d..5a1e1a85 100644 --- a/charts/kubechecks/Chart.yaml +++ b/charts/kubechecks/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: kubechecks description: A Helm chart for kubechecks -version: 0.4.6 +version: 0.5.0 type: application maintainers: - name: zapier diff --git a/charts/kubechecks/templates/role.yaml b/charts/kubechecks/templates/role.yaml new file mode 100644 index 00000000..c20fb37b --- /dev/null +++ b/charts/kubechecks/templates/role.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kubechecks + namespace: {{ .Values.argocd.namespace }} +rules: + - apiGroups: + - "" + resources: + - configmaps + - secrets + verbs: + - get + - list + - watch diff --git a/charts/kubechecks/templates/rolebinding.yaml b/charts/kubechecks/templates/rolebinding.yaml new file mode 100644 index 00000000..41a0b33f --- /dev/null +++ b/charts/kubechecks/templates/rolebinding.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kubechecks + namespace: {{ .Values.argocd.namespace }} +roleRef: + kind: Role + name: kubechecks + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "kubechecks.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/charts/kubechecks/values.yaml b/charts/kubechecks/values.yaml index 9e996252..aadae07a 100644 --- a/charts/kubechecks/values.yaml +++ b/charts/kubechecks/values.yaml @@ -1,4 +1,7 @@ # Labels to apply to all resources created by this Helm chart +argocd: + namespace: argocd + commonLabels: {} configMap: diff --git a/cmd/container.go b/cmd/container.go deleted file mode 100644 index c447838a..00000000 --- a/cmd/container.go +++ /dev/null @@ -1,123 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - - "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "github.com/zapier/kubechecks/pkg/app_watcher" - "github.com/zapier/kubechecks/pkg/appdir" - "github.com/zapier/kubechecks/pkg/argo_client" - "github.com/zapier/kubechecks/pkg/config" - "github.com/zapier/kubechecks/pkg/container" - "github.com/zapier/kubechecks/pkg/git" - client "github.com/zapier/kubechecks/pkg/kubernetes" - "github.com/zapier/kubechecks/pkg/vcs/github_client" - "github.com/zapier/kubechecks/pkg/vcs/gitlab_client" -) - -func newContainer(ctx context.Context, cfg config.ServerConfig, watchApps bool) (container.Container, error) { - var err error - - var ctr = container.Container{ - Config: cfg, - RepoManager: git.NewRepoManager(cfg), - } - - // create vcs client - switch cfg.VcsType { - case "gitlab": - ctr.VcsClient, err = gitlab_client.CreateGitlabClient(cfg) - case "github": - ctr.VcsClient, err = github_client.CreateGithubClient(cfg) - default: - err = fmt.Errorf("unknown vcs-type: %q", cfg.VcsType) - } - if err != nil { - return ctr, errors.Wrap(err, "failed to create vcs client") - } - var kubeClient client.Interface - - switch cfg.KubernetesType { - // TODO: expand with other cluster types - case client.ClusterTypeLOCAL: - kubeClient, err = client.New(&client.NewClientInput{ - KubernetesConfigPath: cfg.KubernetesConfig, - ClusterType: cfg.KubernetesType, - }) - if err != nil { - return ctr, errors.Wrap(err, "failed to create kube client") - } - case client.ClusterTypeEKS: - kubeClient, err = client.New(&client.NewClientInput{ - KubernetesConfigPath: cfg.KubernetesConfig, - ClusterType: cfg.KubernetesType, - }, - client.EKSClientOption(ctx, cfg.KubernetesClusterID), - ) - if err != nil { - return ctr, errors.Wrap(err, "failed to create kube client") - } - } - ctr.KubeClientSet = kubeClient - // create argo client - if ctr.ArgoClient, err = argo_client.NewArgoClient(cfg); err != nil { - return ctr, errors.Wrap(err, "failed to create argo client") - } - - // create vcs to argo map - vcsToArgoMap := appdir.NewVcsToArgoMap(ctr.VcsClient.Username()) - ctr.VcsToArgoMap = vcsToArgoMap - - // watch app modifications, if necessary - if cfg.MonitorAllApplications { - if err = buildAppsMap(ctx, ctr.ArgoClient, ctr.VcsToArgoMap); err != nil { - return ctr, errors.Wrap(err, "failed to build apps map") - } - - if err = buildAppSetsMap(ctx, ctr.ArgoClient, ctr.VcsToArgoMap); err != nil { - return ctr, errors.Wrap(err, "failed to build appsets map") - } - - if watchApps { - ctr.ApplicationWatcher, err = app_watcher.NewApplicationWatcher(kubeClient.Config(), vcsToArgoMap, cfg) - if err != nil { - return ctr, errors.Wrap(err, "failed to create watch applications") - } - ctr.ApplicationSetWatcher, err = app_watcher.NewApplicationSetWatcher(kubeClient.Config(), vcsToArgoMap, cfg) - if err != nil { - return ctr, errors.Wrap(err, "failed to create watch application sets") - } - - go ctr.ApplicationWatcher.Run(ctx, 1) - go ctr.ApplicationSetWatcher.Run(ctx) - } - } else { - log.Info().Msgf("not monitoring applications, MonitorAllApplications: %+v", cfg.MonitorAllApplications) - } - - return ctr, nil -} - -func buildAppsMap(ctx context.Context, argoClient *argo_client.ArgoClient, result container.VcsToArgoMap) error { - apps, err := argoClient.GetApplications(ctx) - if err != nil { - return errors.Wrap(err, "failed to list applications") - } - for _, app := range apps.Items { - result.AddApp(&app) - } - return nil -} - -func buildAppSetsMap(ctx context.Context, argoClient *argo_client.ArgoClient, result container.VcsToArgoMap) error { - appSets, err := argoClient.GetApplicationSets(ctx) - if err != nil { - return errors.Wrap(err, "failed to list application sets") - } - for _, appSet := range appSets.Items { - result.AddAppSet(&appSet) - } - return nil -} diff --git a/cmd/controller_cmd.go b/cmd/controller.go similarity index 78% rename from cmd/controller_cmd.go rename to cmd/controller.go index f5587e3a..6b2b758c 100644 --- a/cmd/controller_cmd.go +++ b/cmd/controller.go @@ -11,6 +11,7 @@ import ( "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/zapier/kubechecks/pkg/app_watcher" "github.com/zapier/kubechecks/pkg" "github.com/zapier/kubechecks/pkg/checks" @@ -41,19 +42,39 @@ var ControllerCmd = &cobra.Command{ log.Fatal().Err(err).Msg("failed to parse configuration") } - ctr, err := newContainer(ctx, cfg, true) + ctr, err := container.New(ctx, cfg) if err != nil { log.Fatal().Err(err).Msg("failed to create container") } + // watch app modifications, if necessary + if cfg.MonitorAllApplications { + appWatcher, err := app_watcher.NewApplicationWatcher(ctr) + if err != nil { + log.Fatal().Err(err).Msg("failed to create watch applications") + } + go appWatcher.Run(ctx, 1) + + appSetWatcher, err := app_watcher.NewApplicationSetWatcher(ctr) + if err != nil { + log.Fatal().Err(err).Msg("failed to create watch application sets") + } + go appSetWatcher.Run(ctx) + } else { + log.Info().Msgf("not monitoring applications, MonitorAllApplications: %+v", cfg.MonitorAllApplications) + } + log.Info().Msg("initializing git settings") if err = initializeGit(ctr); err != nil { log.Fatal().Err(err).Msg("failed to initialize git settings") } + log.Info().Strs("locations", cfg.PoliciesLocation).Msg("processing policies locations") if err = processLocations(ctx, ctr, cfg.PoliciesLocation); err != nil { log.Fatal().Err(err).Msg("failed to process policy locations") } + + log.Info().Strs("locations", cfg.SchemasLocations).Msg("processing schemas locations") if err = processLocations(ctx, ctr, cfg.SchemasLocations); err != nil { log.Fatal().Err(err).Msg("failed to process schema locations") } @@ -137,6 +158,11 @@ func init() { newStringOpts().withDefault("1.23.0")) boolFlag(flags, "show-debug-info", "Set to true to print debug info to the footer of MR comments (KUBECHECKS_SHOW_DEBUG_INFO).") + stringFlag(flags, "argocd-repository-endpoint", `Location of the argocd repository service endpoint.`, + newStringOpts().withDefault("argocd-repo-server.argocd:8081")) + boolFlag(flags, "argocd-repository-insecure", `True if you need to skip validating the grpc tls certificate.`, + newBoolOpts().withDefault(true)) + boolFlag(flags, "argocd-send-full-repository", `Set to true if you want to try to send the full repository to ArgoCD when generating manifests.`) stringFlag(flags, "label-filter", `(Optional) If set, The label that must be set on an MR (as "kubechecks:") for kubechecks to process the merge request webhook (KUBECHECKS_LABEL_FILTER).`) stringFlag(flags, "openai-api-token", "OpenAI API Token.") stringFlag(flags, "webhook-url-base", "The endpoint to listen on for incoming PR/MR event webhooks. For example, 'https://checker.mycompany.com'.") diff --git a/cmd/locations.go b/cmd/locations.go index 47b5774d..62705816 100644 --- a/cmd/locations.go +++ b/cmd/locations.go @@ -25,6 +25,8 @@ func processLocations(ctx context.Context, ctr container.Container, locations [] } } + log.Debug().Strs("locations", locations).Msg("locations after processing") + return nil } diff --git a/cmd/process.go b/cmd/process.go index 208ab847..456e0b0f 100644 --- a/cmd/process.go +++ b/cmd/process.go @@ -1,8 +1,13 @@ package cmd import ( + "os" + "path/filepath" + + "github.com/argoproj/argo-cd/v2/common" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/zapier/kubechecks/pkg/container" "github.com/zapier/kubechecks/pkg/config" "github.com/zapier/kubechecks/pkg/server" @@ -15,14 +20,42 @@ var processCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() + tempPath, err := os.MkdirTemp("", "") + if err != nil { + log.Fatal().Err(err).Msg("fail to create ssh data dir") + } + defer func() { + os.RemoveAll(tempPath) + }() + + // symlink local ssh known hosts to argocd ssh known hosts + homeDir, err := os.UserHomeDir() + if err != nil { + log.Fatal().Err(err).Msg("failed to get user home dir") + } + source := filepath.Join(homeDir, ".ssh", "known_hosts") + target := filepath.Join(tempPath, common.DefaultSSHKnownHostsName) + + if err := os.Symlink(source, target); err != nil { + log.Fatal().Err(err).Msg("fail to symlink ssh_known_hosts file") + } + + if err := os.Setenv("ARGOCD_SSH_DATA_PATH", tempPath); err != nil { + log.Fatal().Err(err).Msg("fail to set ARGOCD_SSH_DATA_PATH") + } + cfg, err := config.New() if err != nil { log.Fatal().Err(err).Msg("failed to generate config") } - ctr, err := newContainer(ctx, cfg, false) + if len(args) != 1 { + log.Fatal().Msg("usage: kubechecks process PR_REF") + } + + ctr, err := container.New(ctx, cfg) if err != nil { - log.Fatal().Err(err).Msg("failed to create container") + log.Fatal().Err(err).Msg("failed to create clients") } log.Info().Msg("initializing git settings") diff --git a/cmd/root.go b/cmd/root.go index 759f8710..c52668b1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -85,7 +85,7 @@ func init() { newStringOpts(). withChoices("hide", "delete"). withDefault("hide")) - stringSliceFlag(flags, "schemas-location", "Sets schema locations to be used for every check request. Can be common paths inside the repos being checked or git urls in either git or http(s) format.") + stringSliceFlag(flags, "schemas-location", "Sets schema locations to be used for every check request. Can be a common path on the host or git urls in either git or http(s) format.") boolFlag(flags, "enable-conftest", "Set to true to enable conftest policy checking of manifests.") stringSliceFlag(flags, "policies-location", "Sets rego policy locations to be used for every check request. Can be common path inside the repos being checked or git urls in either git or http(s) format.", newStringSliceOpts(). @@ -124,9 +124,6 @@ func init() { } func setupLogOutput() { - output := zerolog.ConsoleWriter{Out: os.Stdout} - log.Logger = log.Output(output) - // Default level is info, unless debug flag is present levelFlag := viper.GetString("log-level") level, err := zerolog.ParseLevel(levelFlag) @@ -135,6 +132,10 @@ func setupLogOutput() { } zerolog.SetGlobalLevel(level) + + output := zerolog.ConsoleWriter{Out: os.Stdout} + log.Logger = log.Output(output) + log.Debug().Msg("Debug level logging enabled.") log.Trace().Msg("Trace level logging enabled.") log.Info().Msg("Initialized logger.") diff --git a/docs/usage.md b/docs/usage.md index 94cc6f09..64032f55 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -41,6 +41,9 @@ The full list of supported environment variables is described below: |`KUBECHECKS_ARGOCD_API_PLAINTEXT`|Enable to use plaintext connections without TLS.|`false`| |`KUBECHECKS_ARGOCD_API_SERVER_ADDR`|ArgoCD API Server Address.|`argocd-server`| |`KUBECHECKS_ARGOCD_API_TOKEN`|ArgoCD API token.|| +|`KUBECHECKS_ARGOCD_REPOSITORY_ENDPOINT`|Location of the argocd repository service endpoint.|`argocd-repo-server.argocd:8081`| +|`KUBECHECKS_ARGOCD_REPOSITORY_INSECURE`|True if you need to skip validating the grpc tls certificate.|`true`| +|`KUBECHECKS_ARGOCD_SEND_FULL_REPOSITORY`|Set to true if you want to try to send the full repository to ArgoCD when generating manifests.|`false`| |`KUBECHECKS_ENABLE_CONFTEST`|Set to true to enable conftest policy checking of manifests.|`false`| |`KUBECHECKS_ENABLE_HOOKS_RENDERER`|Render hooks.|`true`| |`KUBECHECKS_ENABLE_KUBECONFORM`|Enable kubeconform checks.|`true`| @@ -66,7 +69,7 @@ The full list of supported environment variables is described below: |`KUBECHECKS_POLICIES_LOCATION`|Sets rego policy locations to be used for every check request. Can be common path inside the repos being checked or git urls in either git or http(s) format.|`[./policies]`| |`KUBECHECKS_REPLAN_COMMENT_MSG`|comment message which re-triggers kubechecks on PR.|`kubechecks again`| |`KUBECHECKS_REPO_REFRESH_INTERVAL`|Interval between static repo refreshes (for schemas and policies).|`5m`| -|`KUBECHECKS_SCHEMAS_LOCATION`|Sets schema locations to be used for every check request. Can be common paths inside the repos being checked or git urls in either git or http(s) format.|`[]`| +|`KUBECHECKS_SCHEMAS_LOCATION`|Sets schema locations to be used for every check request. Can be a common path on the host or git urls in either git or http(s) format.|`[]`| |`KUBECHECKS_SHOW_DEBUG_INFO`|Set to true to print debug info to the footer of MR comments.|`false`| |`KUBECHECKS_TIDY_OUTDATED_COMMENTS_MODE`|Sets the mode to use when tidying outdated comments. One of hide, delete.|`hide`| |`KUBECHECKS_VCS_BASE_URL`|VCS base url, useful if self hosting gitlab, enterprise github, etc.|| diff --git a/mocks/affected_apps/mocks/mock_Matcher.go b/mocks/affected_apps/mocks/mock_Matcher.go index 6a0fb41c..e9f34d82 100644 --- a/mocks/affected_apps/mocks/mock_Matcher.go +++ b/mocks/affected_apps/mocks/mock_Matcher.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package affected_apps @@ -29,6 +29,10 @@ func (_m *MockMatcher) EXPECT() *MockMatcher_Expecter { func (_m *MockMatcher) AffectedApps(ctx context.Context, changeList []string, targetBranch string, repo *git.Repo) (affected_apps.AffectedItems, error) { ret := _m.Called(ctx, changeList, targetBranch, repo) + if len(ret) == 0 { + panic("no return value specified for AffectedApps") + } + var r0 affected_apps.AffectedItems var r1 error if rf, ok := ret.Get(0).(func(context.Context, []string, string, *git.Repo) (affected_apps.AffectedItems, error)); ok { diff --git a/mocks/affected_apps/mocks/mock_argoClient.go b/mocks/affected_apps/mocks/mock_argoClient.go index 4b61928b..db3bfb0f 100644 --- a/mocks/affected_apps/mocks/mock_argoClient.go +++ b/mocks/affected_apps/mocks/mock_argoClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package affected_apps @@ -26,6 +26,10 @@ func (_m *MockargoClient) EXPECT() *MockargoClient_Expecter { func (_m *MockargoClient) GetApplications(ctx context.Context) (*v1alpha1.ApplicationList, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetApplications") + } + var r0 *v1alpha1.ApplicationList var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*v1alpha1.ApplicationList, error)); ok { @@ -80,6 +84,10 @@ func (_c *MockargoClient_GetApplications_Call) RunAndReturn(run func(context.Con func (_m *MockargoClient) GetApplicationsByAppset(ctx context.Context, appsetName string) (*v1alpha1.ApplicationList, error) { ret := _m.Called(ctx, appsetName) + if len(ret) == 0 { + panic("no return value specified for GetApplicationsByAppset") + } + var r0 *v1alpha1.ApplicationList var r1 error if rf, ok := ret.Get(0).(func(context.Context, string) (*v1alpha1.ApplicationList, error)); ok { diff --git a/mocks/generator/mocks/mock_AppsGenerator.go b/mocks/generator/mocks/mock_AppsGenerator.go index 8ceb12f1..b9215f75 100644 --- a/mocks/generator/mocks/mock_AppsGenerator.go +++ b/mocks/generator/mocks/mock_AppsGenerator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package generator @@ -29,6 +29,10 @@ func (_m *MockAppsGenerator) EXPECT() *MockAppsGenerator_Expecter { func (_m *MockAppsGenerator) GenerateApplicationSetApps(ctx context.Context, appset v1alpha1.ApplicationSet, ctr *container.Container) ([]v1alpha1.Application, error) { ret := _m.Called(ctx, appset, ctr) + if len(ret) == 0 { + panic("no return value specified for GenerateApplicationSetApps") + } + var r0 []v1alpha1.Application var r1 error if rf, ok := ret.Get(0).(func(context.Context, v1alpha1.ApplicationSet, *container.Container) ([]v1alpha1.Application, error)); ok { diff --git a/mocks/generator/mocks/mock_Generator.go b/mocks/generator/mocks/mock_Generator.go index fc7365ff..faff846b 100644 --- a/mocks/generator/mocks/mock_Generator.go +++ b/mocks/generator/mocks/mock_Generator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package generator @@ -27,6 +27,10 @@ func (_m *MockGenerator) EXPECT() *MockGenerator_Expecter { func (_m *MockGenerator) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetGenerator, applicationSetInfo *v1alpha1.ApplicationSet) ([]map[string]interface{}, error) { ret := _m.Called(appSetGenerator, applicationSetInfo) + if len(ret) == 0 { + panic("no return value specified for GenerateParams") + } + var r0 []map[string]interface{} var r1 error if rf, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator, *v1alpha1.ApplicationSet) ([]map[string]interface{}, error)); ok { @@ -82,6 +86,10 @@ func (_c *MockGenerator_GenerateParams_Call) RunAndReturn(run func(*v1alpha1.App func (_m *MockGenerator) GetRequeueAfter(appSetGenerator *v1alpha1.ApplicationSetGenerator) time.Duration { ret := _m.Called(appSetGenerator) + if len(ret) == 0 { + panic("no return value specified for GetRequeueAfter") + } + var r0 time.Duration if rf, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator) time.Duration); ok { r0 = rf(appSetGenerator) @@ -124,6 +132,10 @@ func (_c *MockGenerator_GetRequeueAfter_Call) RunAndReturn(run func(*v1alpha1.Ap func (_m *MockGenerator) GetTemplate(appSetGenerator *v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate { ret := _m.Called(appSetGenerator) + if len(ret) == 0 { + panic("no return value specified for GetTemplate") + } + var r0 *v1alpha1.ApplicationSetTemplate if rf, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate); ok { r0 = rf(appSetGenerator) diff --git a/mocks/github_client/mocks/mock_IssuesServices.go b/mocks/github_client/mocks/mock_IssuesServices.go index b7be7caf..ec6288f1 100644 --- a/mocks/github_client/mocks/mock_IssuesServices.go +++ b/mocks/github_client/mocks/mock_IssuesServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package github_client @@ -27,6 +27,10 @@ func (_m *MockIssuesServices) EXPECT() *MockIssuesServices_Expecter { func (_m *MockIssuesServices) CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { ret := _m.Called(ctx, owner, repo, number, comment) + if len(ret) == 0 { + panic("no return value specified for CreateComment") + } + var r0 *github.IssueComment var r1 *github.Response var r2 error @@ -94,6 +98,10 @@ func (_c *MockIssuesServices_CreateComment_Call) RunAndReturn(run func(context.C func (_m *MockIssuesServices) DeleteComment(ctx context.Context, owner string, repo string, commentID int64) (*github.Response, error) { ret := _m.Called(ctx, owner, repo, commentID) + if len(ret) == 0 { + panic("no return value specified for DeleteComment") + } + var r0 *github.Response var r1 error if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (*github.Response, error)); ok { @@ -151,6 +159,10 @@ func (_c *MockIssuesServices_DeleteComment_Call) RunAndReturn(run func(context.C func (_m *MockIssuesServices) EditComment(ctx context.Context, owner string, repo string, commentID int64, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) { ret := _m.Called(ctx, owner, repo, commentID, comment) + if len(ret) == 0 { + panic("no return value specified for EditComment") + } + var r0 *github.IssueComment var r1 *github.Response var r2 error @@ -218,6 +230,10 @@ func (_c *MockIssuesServices_EditComment_Call) RunAndReturn(run func(context.Con func (_m *MockIssuesServices) ListComments(ctx context.Context, owner string, repo string, number int, opts *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error) { ret := _m.Called(ctx, owner, repo, number, opts) + if len(ret) == 0 { + panic("no return value specified for ListComments") + } + var r0 []*github.IssueComment var r1 *github.Response var r2 error diff --git a/mocks/github_client/mocks/mock_PullRequestsServices.go b/mocks/github_client/mocks/mock_PullRequestsServices.go index d77ad006..cafa0548 100644 --- a/mocks/github_client/mocks/mock_PullRequestsServices.go +++ b/mocks/github_client/mocks/mock_PullRequestsServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package github_client @@ -27,6 +27,10 @@ func (_m *MockPullRequestsServices) EXPECT() *MockPullRequestsServices_Expecter func (_m *MockPullRequestsServices) Get(ctx context.Context, owner string, repo string, number int) (*github.PullRequest, *github.Response, error) { ret := _m.Called(ctx, owner, repo, number) + if len(ret) == 0 { + panic("no return value specified for Get") + } + var r0 *github.PullRequest var r1 *github.Response var r2 error @@ -93,6 +97,10 @@ func (_c *MockPullRequestsServices_Get_Call) RunAndReturn(run func(context.Conte func (_m *MockPullRequestsServices) GetRaw(ctx context.Context, owner string, repo string, number int, opts github.RawOptions) (string, *github.Response, error) { ret := _m.Called(ctx, owner, repo, number, opts) + if len(ret) == 0 { + panic("no return value specified for GetRaw") + } + var r0 string var r1 *github.Response var r2 error @@ -158,6 +166,10 @@ func (_c *MockPullRequestsServices_GetRaw_Call) RunAndReturn(run func(context.Co func (_m *MockPullRequestsServices) List(ctx context.Context, owner string, repo string, opts *github.PullRequestListOptions) ([]*github.PullRequest, *github.Response, error) { ret := _m.Called(ctx, owner, repo, opts) + if len(ret) == 0 { + panic("no return value specified for List") + } + var r0 []*github.PullRequest var r1 *github.Response var r2 error @@ -224,6 +236,10 @@ func (_c *MockPullRequestsServices_List_Call) RunAndReturn(run func(context.Cont func (_m *MockPullRequestsServices) ListFiles(ctx context.Context, owner string, repo string, number int, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) { ret := _m.Called(ctx, owner, repo, number, opts) + if len(ret) == 0 { + panic("no return value specified for ListFiles") + } + var r0 []*github.CommitFile var r1 *github.Response var r2 error diff --git a/mocks/github_client/mocks/mock_RepositoriesServices.go b/mocks/github_client/mocks/mock_RepositoriesServices.go index a61f7b52..72ecf3a8 100644 --- a/mocks/github_client/mocks/mock_RepositoriesServices.go +++ b/mocks/github_client/mocks/mock_RepositoriesServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package github_client @@ -27,6 +27,10 @@ func (_m *MockRepositoriesServices) EXPECT() *MockRepositoriesServices_Expecter func (_m *MockRepositoriesServices) CreateHook(ctx context.Context, owner string, repo string, hook *github.Hook) (*github.Hook, *github.Response, error) { ret := _m.Called(ctx, owner, repo, hook) + if len(ret) == 0 { + panic("no return value specified for CreateHook") + } + var r0 *github.Hook var r1 *github.Response var r2 error @@ -93,6 +97,10 @@ func (_c *MockRepositoriesServices_CreateHook_Call) RunAndReturn(run func(contex func (_m *MockRepositoriesServices) CreateStatus(ctx context.Context, owner string, repo string, ref string, status *github.RepoStatus) (*github.RepoStatus, *github.Response, error) { ret := _m.Called(ctx, owner, repo, ref, status) + if len(ret) == 0 { + panic("no return value specified for CreateStatus") + } + var r0 *github.RepoStatus var r1 *github.Response var r2 error @@ -160,6 +168,10 @@ func (_c *MockRepositoriesServices_CreateStatus_Call) RunAndReturn(run func(cont func (_m *MockRepositoriesServices) Get(ctx context.Context, owner string, repo string) (*github.Repository, *github.Response, error) { ret := _m.Called(ctx, owner, repo) + if len(ret) == 0 { + panic("no return value specified for Get") + } + var r0 *github.Repository var r1 *github.Response var r2 error @@ -225,6 +237,10 @@ func (_c *MockRepositoriesServices_Get_Call) RunAndReturn(run func(context.Conte func (_m *MockRepositoriesServices) GetContents(ctx context.Context, owner string, repo string, path string, opts *github.RepositoryContentGetOptions) (*github.RepositoryContent, []*github.RepositoryContent, *github.Response, error) { ret := _m.Called(ctx, owner, repo, path, opts) + if len(ret) == 0 { + panic("no return value specified for GetContents") + } + var r0 *github.RepositoryContent var r1 []*github.RepositoryContent var r2 *github.Response @@ -301,6 +317,10 @@ func (_c *MockRepositoriesServices_GetContents_Call) RunAndReturn(run func(conte func (_m *MockRepositoriesServices) ListHooks(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) { ret := _m.Called(ctx, owner, repo, opts) + if len(ret) == 0 { + panic("no return value specified for ListHooks") + } + var r0 []*github.Hook var r1 *github.Response var r2 error diff --git a/mocks/gitlab_client/mocks/mock_CommitsServices.go b/mocks/gitlab_client/mocks/mock_CommitsServices.go index 61b67deb..4f16c6fb 100644 --- a/mocks/gitlab_client/mocks/mock_CommitsServices.go +++ b/mocks/gitlab_client/mocks/mock_CommitsServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package gitlab_client @@ -32,6 +32,10 @@ func (_m *MockCommitsServices) SetCommitStatus(pid interface{}, sha string, opt _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for SetCommitStatus") + } + var r0 *gitlab.CommitStatus var r1 *gitlab.Response var r2 error diff --git a/mocks/gitlab_client/mocks/mock_MergeRequestsServices.go b/mocks/gitlab_client/mocks/mock_MergeRequestsServices.go index a01086c6..f4eba9b3 100644 --- a/mocks/gitlab_client/mocks/mock_MergeRequestsServices.go +++ b/mocks/gitlab_client/mocks/mock_MergeRequestsServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package gitlab_client @@ -32,6 +32,10 @@ func (_m *MockMergeRequestsServices) GetMergeRequest(pid interface{}, mergeReque _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for GetMergeRequest") + } + var r0 *gitlab.MergeRequest var r1 *gitlab.Response var r2 error @@ -112,6 +116,10 @@ func (_m *MockMergeRequestsServices) GetMergeRequestChanges(pid interface{}, mer _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for GetMergeRequestChanges") + } + var r0 *gitlab.MergeRequest var r1 *gitlab.Response var r2 error @@ -192,6 +200,10 @@ func (_m *MockMergeRequestsServices) GetMergeRequestDiffVersions(pid interface{} _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for GetMergeRequestDiffVersions") + } + var r0 []*gitlab.MergeRequestDiffVersion var r1 *gitlab.Response var r2 error @@ -272,6 +284,10 @@ func (_m *MockMergeRequestsServices) ListMergeRequestDiffs(pid interface{}, merg _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for ListMergeRequestDiffs") + } + var r0 []*gitlab.MergeRequestDiff var r1 *gitlab.Response var r2 error @@ -352,6 +368,10 @@ func (_m *MockMergeRequestsServices) UpdateMergeRequest(pid interface{}, mergeRe _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for UpdateMergeRequest") + } + var r0 *gitlab.MergeRequest var r1 *gitlab.Response var r2 error diff --git a/mocks/gitlab_client/mocks/mock_NotesServices.go b/mocks/gitlab_client/mocks/mock_NotesServices.go index 629cc4c7..a06987fe 100644 --- a/mocks/gitlab_client/mocks/mock_NotesServices.go +++ b/mocks/gitlab_client/mocks/mock_NotesServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package gitlab_client @@ -32,6 +32,10 @@ func (_m *MockNotesServices) CreateMergeRequestNote(pid interface{}, mergeReques _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for CreateMergeRequestNote") + } + var r0 *gitlab.Note var r1 *gitlab.Response var r2 error @@ -112,6 +116,10 @@ func (_m *MockNotesServices) DeleteMergeRequestNote(pid interface{}, mergeReques _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for DeleteMergeRequestNote") + } + var r0 *gitlab.Response var r1 error if rf, ok := ret.Get(0).(func(interface{}, int, int, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)); ok { @@ -183,6 +191,10 @@ func (_m *MockNotesServices) ListMergeRequestNotes(pid interface{}, mergeRequest _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for ListMergeRequestNotes") + } + var r0 []*gitlab.Note var r1 *gitlab.Response var r2 error @@ -263,6 +275,10 @@ func (_m *MockNotesServices) UpdateMergeRequestNote(pid interface{}, mergeReques _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for UpdateMergeRequestNote") + } + var r0 *gitlab.Note var r1 *gitlab.Response var r2 error diff --git a/mocks/gitlab_client/mocks/mock_PipelinesServices.go b/mocks/gitlab_client/mocks/mock_PipelinesServices.go index d413db39..bc6c5386 100644 --- a/mocks/gitlab_client/mocks/mock_PipelinesServices.go +++ b/mocks/gitlab_client/mocks/mock_PipelinesServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package gitlab_client @@ -32,6 +32,10 @@ func (_m *MockPipelinesServices) ListProjectPipelines(pid interface{}, opt *gitl _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for ListProjectPipelines") + } + var r0 []*gitlab.PipelineInfo var r1 *gitlab.Response var r2 error diff --git a/mocks/gitlab_client/mocks/mock_ProjectsServices.go b/mocks/gitlab_client/mocks/mock_ProjectsServices.go index 7e3965e2..780d220f 100644 --- a/mocks/gitlab_client/mocks/mock_ProjectsServices.go +++ b/mocks/gitlab_client/mocks/mock_ProjectsServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package gitlab_client @@ -32,6 +32,10 @@ func (_m *MockProjectsServices) AddProjectHook(pid interface{}, opt *gitlab.AddP _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for AddProjectHook") + } + var r0 *gitlab.ProjectHook var r1 *gitlab.Response var r2 error @@ -111,6 +115,10 @@ func (_m *MockProjectsServices) EditProjectHook(pid interface{}, hook int, opt * _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for EditProjectHook") + } + var r0 *gitlab.ProjectHook var r1 *gitlab.Response var r2 error @@ -191,6 +199,10 @@ func (_m *MockProjectsServices) GetProject(pid interface{}, opt *gitlab.GetProje _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for GetProject") + } + var r0 *gitlab.Project var r1 *gitlab.Response var r2 error @@ -270,6 +282,10 @@ func (_m *MockProjectsServices) ListProjectHooks(pid interface{}, opt *gitlab.Li _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for ListProjectHooks") + } + var r0 []*gitlab.ProjectHook var r1 *gitlab.Response var r2 error diff --git a/mocks/gitlab_client/mocks/mock_RepositoryFilesServices.go b/mocks/gitlab_client/mocks/mock_RepositoryFilesServices.go index 1f0ec98d..9798dbc6 100644 --- a/mocks/gitlab_client/mocks/mock_RepositoryFilesServices.go +++ b/mocks/gitlab_client/mocks/mock_RepositoryFilesServices.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.37.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package gitlab_client @@ -32,6 +32,10 @@ func (_m *MockRepositoryFilesServices) GetRawFile(pid interface{}, fileName stri _ca = append(_ca, _va...) ret := _m.Called(_ca...) + if len(ret) == 0 { + panic("no return value specified for GetRawFile") + } + var r0 []byte var r1 *gitlab.Response var r2 error diff --git a/mocks/vcs/mocks/mock_Client.go b/mocks/vcs/mocks/mock_Client.go new file mode 100644 index 00000000..923a3eba --- /dev/null +++ b/mocks/vcs/mocks/mock_Client.go @@ -0,0 +1,710 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package vcs + +import ( + context "context" + http "net/http" + + mock "github.com/stretchr/testify/mock" + + msg "github.com/zapier/kubechecks/pkg/msg" + + pkg "github.com/zapier/kubechecks/pkg" + + vcs "github.com/zapier/kubechecks/pkg/vcs" +) + +// MockClient is an autogenerated mock type for the Client type +type MockClient struct { + mock.Mock +} + +type MockClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockClient) EXPECT() *MockClient_Expecter { + return &MockClient_Expecter{mock: &_m.Mock} +} + +// CommitStatus provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockClient) CommitStatus(_a0 context.Context, _a1 vcs.PullRequest, _a2 pkg.CommitState) error { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for CommitStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, vcs.PullRequest, pkg.CommitState) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_CommitStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommitStatus' +type MockClient_CommitStatus_Call struct { + *mock.Call +} + +// CommitStatus is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 vcs.PullRequest +// - _a2 pkg.CommitState +func (_e *MockClient_Expecter) CommitStatus(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockClient_CommitStatus_Call { + return &MockClient_CommitStatus_Call{Call: _e.mock.On("CommitStatus", _a0, _a1, _a2)} +} + +func (_c *MockClient_CommitStatus_Call) Run(run func(_a0 context.Context, _a1 vcs.PullRequest, _a2 pkg.CommitState)) *MockClient_CommitStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(vcs.PullRequest), args[2].(pkg.CommitState)) + }) + return _c +} + +func (_c *MockClient_CommitStatus_Call) Return(_a0 error) *MockClient_CommitStatus_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_CommitStatus_Call) RunAndReturn(run func(context.Context, vcs.PullRequest, pkg.CommitState) error) *MockClient_CommitStatus_Call { + _c.Call.Return(run) + return _c +} + +// CreateHook provides a mock function with given fields: ctx, repoName, webhookUrl, webhookSecret +func (_m *MockClient) CreateHook(ctx context.Context, repoName string, webhookUrl string, webhookSecret string) error { + ret := _m.Called(ctx, repoName, webhookUrl, webhookSecret) + + if len(ret) == 0 { + panic("no return value specified for CreateHook") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, repoName, webhookUrl, webhookSecret) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_CreateHook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateHook' +type MockClient_CreateHook_Call struct { + *mock.Call +} + +// CreateHook is a helper method to define mock.On call +// - ctx context.Context +// - repoName string +// - webhookUrl string +// - webhookSecret string +func (_e *MockClient_Expecter) CreateHook(ctx interface{}, repoName interface{}, webhookUrl interface{}, webhookSecret interface{}) *MockClient_CreateHook_Call { + return &MockClient_CreateHook_Call{Call: _e.mock.On("CreateHook", ctx, repoName, webhookUrl, webhookSecret)} +} + +func (_c *MockClient_CreateHook_Call) Run(run func(ctx context.Context, repoName string, webhookUrl string, webhookSecret string)) *MockClient_CreateHook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *MockClient_CreateHook_Call) Return(_a0 error) *MockClient_CreateHook_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_CreateHook_Call) RunAndReturn(run func(context.Context, string, string, string) error) *MockClient_CreateHook_Call { + _c.Call.Return(run) + return _c +} + +// Email provides a mock function with given fields: +func (_m *MockClient) Email() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Email") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockClient_Email_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Email' +type MockClient_Email_Call struct { + *mock.Call +} + +// Email is a helper method to define mock.On call +func (_e *MockClient_Expecter) Email() *MockClient_Email_Call { + return &MockClient_Email_Call{Call: _e.mock.On("Email")} +} + +func (_c *MockClient_Email_Call) Run(run func()) *MockClient_Email_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockClient_Email_Call) Return(_a0 string) *MockClient_Email_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_Email_Call) RunAndReturn(run func() string) *MockClient_Email_Call { + _c.Call.Return(run) + return _c +} + +// GetHookByUrl provides a mock function with given fields: ctx, repoName, webhookUrl +func (_m *MockClient) GetHookByUrl(ctx context.Context, repoName string, webhookUrl string) (*vcs.WebHookConfig, error) { + ret := _m.Called(ctx, repoName, webhookUrl) + + if len(ret) == 0 { + panic("no return value specified for GetHookByUrl") + } + + var r0 *vcs.WebHookConfig + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*vcs.WebHookConfig, error)); ok { + return rf(ctx, repoName, webhookUrl) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *vcs.WebHookConfig); ok { + r0 = rf(ctx, repoName, webhookUrl) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*vcs.WebHookConfig) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, repoName, webhookUrl) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_GetHookByUrl_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHookByUrl' +type MockClient_GetHookByUrl_Call struct { + *mock.Call +} + +// GetHookByUrl is a helper method to define mock.On call +// - ctx context.Context +// - repoName string +// - webhookUrl string +func (_e *MockClient_Expecter) GetHookByUrl(ctx interface{}, repoName interface{}, webhookUrl interface{}) *MockClient_GetHookByUrl_Call { + return &MockClient_GetHookByUrl_Call{Call: _e.mock.On("GetHookByUrl", ctx, repoName, webhookUrl)} +} + +func (_c *MockClient_GetHookByUrl_Call) Run(run func(ctx context.Context, repoName string, webhookUrl string)) *MockClient_GetHookByUrl_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockClient_GetHookByUrl_Call) Return(_a0 *vcs.WebHookConfig, _a1 error) *MockClient_GetHookByUrl_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_GetHookByUrl_Call) RunAndReturn(run func(context.Context, string, string) (*vcs.WebHookConfig, error)) *MockClient_GetHookByUrl_Call { + _c.Call.Return(run) + return _c +} + +// GetName provides a mock function with given fields: +func (_m *MockClient) GetName() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetName") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockClient_GetName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetName' +type MockClient_GetName_Call struct { + *mock.Call +} + +// GetName is a helper method to define mock.On call +func (_e *MockClient_Expecter) GetName() *MockClient_GetName_Call { + return &MockClient_GetName_Call{Call: _e.mock.On("GetName")} +} + +func (_c *MockClient_GetName_Call) Run(run func()) *MockClient_GetName_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockClient_GetName_Call) Return(_a0 string) *MockClient_GetName_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_GetName_Call) RunAndReturn(run func() string) *MockClient_GetName_Call { + _c.Call.Return(run) + return _c +} + +// LoadHook provides a mock function with given fields: ctx, repoAndId +func (_m *MockClient) LoadHook(ctx context.Context, repoAndId string) (vcs.PullRequest, error) { + ret := _m.Called(ctx, repoAndId) + + if len(ret) == 0 { + panic("no return value specified for LoadHook") + } + + var r0 vcs.PullRequest + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (vcs.PullRequest, error)); ok { + return rf(ctx, repoAndId) + } + if rf, ok := ret.Get(0).(func(context.Context, string) vcs.PullRequest); ok { + r0 = rf(ctx, repoAndId) + } else { + r0 = ret.Get(0).(vcs.PullRequest) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, repoAndId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_LoadHook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadHook' +type MockClient_LoadHook_Call struct { + *mock.Call +} + +// LoadHook is a helper method to define mock.On call +// - ctx context.Context +// - repoAndId string +func (_e *MockClient_Expecter) LoadHook(ctx interface{}, repoAndId interface{}) *MockClient_LoadHook_Call { + return &MockClient_LoadHook_Call{Call: _e.mock.On("LoadHook", ctx, repoAndId)} +} + +func (_c *MockClient_LoadHook_Call) Run(run func(ctx context.Context, repoAndId string)) *MockClient_LoadHook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockClient_LoadHook_Call) Return(_a0 vcs.PullRequest, _a1 error) *MockClient_LoadHook_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_LoadHook_Call) RunAndReturn(run func(context.Context, string) (vcs.PullRequest, error)) *MockClient_LoadHook_Call { + _c.Call.Return(run) + return _c +} + +// ParseHook provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockClient) ParseHook(_a0 context.Context, _a1 *http.Request, _a2 []byte) (vcs.PullRequest, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for ParseHook") + } + + var r0 vcs.PullRequest + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *http.Request, []byte) (vcs.PullRequest, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, *http.Request, []byte) vcs.PullRequest); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Get(0).(vcs.PullRequest) + } + + if rf, ok := ret.Get(1).(func(context.Context, *http.Request, []byte) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_ParseHook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ParseHook' +type MockClient_ParseHook_Call struct { + *mock.Call +} + +// ParseHook is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *http.Request +// - _a2 []byte +func (_e *MockClient_Expecter) ParseHook(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockClient_ParseHook_Call { + return &MockClient_ParseHook_Call{Call: _e.mock.On("ParseHook", _a0, _a1, _a2)} +} + +func (_c *MockClient_ParseHook_Call) Run(run func(_a0 context.Context, _a1 *http.Request, _a2 []byte)) *MockClient_ParseHook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*http.Request), args[2].([]byte)) + }) + return _c +} + +func (_c *MockClient_ParseHook_Call) Return(_a0 vcs.PullRequest, _a1 error) *MockClient_ParseHook_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_ParseHook_Call) RunAndReturn(run func(context.Context, *http.Request, []byte) (vcs.PullRequest, error)) *MockClient_ParseHook_Call { + _c.Call.Return(run) + return _c +} + +// PostMessage provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockClient) PostMessage(_a0 context.Context, _a1 vcs.PullRequest, _a2 string) (*msg.Message, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for PostMessage") + } + + var r0 *msg.Message + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, vcs.PullRequest, string) (*msg.Message, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, vcs.PullRequest, string) *msg.Message); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*msg.Message) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, vcs.PullRequest, string) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_PostMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PostMessage' +type MockClient_PostMessage_Call struct { + *mock.Call +} + +// PostMessage is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 vcs.PullRequest +// - _a2 string +func (_e *MockClient_Expecter) PostMessage(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockClient_PostMessage_Call { + return &MockClient_PostMessage_Call{Call: _e.mock.On("PostMessage", _a0, _a1, _a2)} +} + +func (_c *MockClient_PostMessage_Call) Run(run func(_a0 context.Context, _a1 vcs.PullRequest, _a2 string)) *MockClient_PostMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(vcs.PullRequest), args[2].(string)) + }) + return _c +} + +func (_c *MockClient_PostMessage_Call) Return(_a0 *msg.Message, _a1 error) *MockClient_PostMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_PostMessage_Call) RunAndReturn(run func(context.Context, vcs.PullRequest, string) (*msg.Message, error)) *MockClient_PostMessage_Call { + _c.Call.Return(run) + return _c +} + +// TidyOutdatedComments provides a mock function with given fields: _a0, _a1 +func (_m *MockClient) TidyOutdatedComments(_a0 context.Context, _a1 vcs.PullRequest) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for TidyOutdatedComments") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, vcs.PullRequest) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_TidyOutdatedComments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TidyOutdatedComments' +type MockClient_TidyOutdatedComments_Call struct { + *mock.Call +} + +// TidyOutdatedComments is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 vcs.PullRequest +func (_e *MockClient_Expecter) TidyOutdatedComments(_a0 interface{}, _a1 interface{}) *MockClient_TidyOutdatedComments_Call { + return &MockClient_TidyOutdatedComments_Call{Call: _e.mock.On("TidyOutdatedComments", _a0, _a1)} +} + +func (_c *MockClient_TidyOutdatedComments_Call) Run(run func(_a0 context.Context, _a1 vcs.PullRequest)) *MockClient_TidyOutdatedComments_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(vcs.PullRequest)) + }) + return _c +} + +func (_c *MockClient_TidyOutdatedComments_Call) Return(_a0 error) *MockClient_TidyOutdatedComments_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_TidyOutdatedComments_Call) RunAndReturn(run func(context.Context, vcs.PullRequest) error) *MockClient_TidyOutdatedComments_Call { + _c.Call.Return(run) + return _c +} + +// ToEmoji provides a mock function with given fields: _a0 +func (_m *MockClient) ToEmoji(_a0 pkg.CommitState) string { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for ToEmoji") + } + + var r0 string + if rf, ok := ret.Get(0).(func(pkg.CommitState) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockClient_ToEmoji_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ToEmoji' +type MockClient_ToEmoji_Call struct { + *mock.Call +} + +// ToEmoji is a helper method to define mock.On call +// - _a0 pkg.CommitState +func (_e *MockClient_Expecter) ToEmoji(_a0 interface{}) *MockClient_ToEmoji_Call { + return &MockClient_ToEmoji_Call{Call: _e.mock.On("ToEmoji", _a0)} +} + +func (_c *MockClient_ToEmoji_Call) Run(run func(_a0 pkg.CommitState)) *MockClient_ToEmoji_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(pkg.CommitState)) + }) + return _c +} + +func (_c *MockClient_ToEmoji_Call) Return(_a0 string) *MockClient_ToEmoji_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_ToEmoji_Call) RunAndReturn(run func(pkg.CommitState) string) *MockClient_ToEmoji_Call { + _c.Call.Return(run) + return _c +} + +// UpdateMessage provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockClient) UpdateMessage(_a0 context.Context, _a1 *msg.Message, _a2 string) error { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for UpdateMessage") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *msg.Message, string) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_UpdateMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateMessage' +type MockClient_UpdateMessage_Call struct { + *mock.Call +} + +// UpdateMessage is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *msg.Message +// - _a2 string +func (_e *MockClient_Expecter) UpdateMessage(_a0 interface{}, _a1 interface{}, _a2 interface{}) *MockClient_UpdateMessage_Call { + return &MockClient_UpdateMessage_Call{Call: _e.mock.On("UpdateMessage", _a0, _a1, _a2)} +} + +func (_c *MockClient_UpdateMessage_Call) Run(run func(_a0 context.Context, _a1 *msg.Message, _a2 string)) *MockClient_UpdateMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*msg.Message), args[2].(string)) + }) + return _c +} + +func (_c *MockClient_UpdateMessage_Call) Return(_a0 error) *MockClient_UpdateMessage_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_UpdateMessage_Call) RunAndReturn(run func(context.Context, *msg.Message, string) error) *MockClient_UpdateMessage_Call { + _c.Call.Return(run) + return _c +} + +// Username provides a mock function with given fields: +func (_m *MockClient) Username() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Username") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockClient_Username_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Username' +type MockClient_Username_Call struct { + *mock.Call +} + +// Username is a helper method to define mock.On call +func (_e *MockClient_Expecter) Username() *MockClient_Username_Call { + return &MockClient_Username_Call{Call: _e.mock.On("Username")} +} + +func (_c *MockClient_Username_Call) Run(run func()) *MockClient_Username_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockClient_Username_Call) Return(_a0 string) *MockClient_Username_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_Username_Call) RunAndReturn(run func() string) *MockClient_Username_Call { + _c.Call.Return(run) + return _c +} + +// VerifyHook provides a mock function with given fields: _a0, _a1 +func (_m *MockClient) VerifyHook(_a0 *http.Request, _a1 string) ([]byte, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for VerifyHook") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(*http.Request, string) ([]byte, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(*http.Request, string) []byte); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_VerifyHook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyHook' +type MockClient_VerifyHook_Call struct { + *mock.Call +} + +// VerifyHook is a helper method to define mock.On call +// - _a0 *http.Request +// - _a1 string +func (_e *MockClient_Expecter) VerifyHook(_a0 interface{}, _a1 interface{}) *MockClient_VerifyHook_Call { + return &MockClient_VerifyHook_Call{Call: _e.mock.On("VerifyHook", _a0, _a1)} +} + +func (_c *MockClient_VerifyHook_Call) Run(run func(_a0 *http.Request, _a1 string)) *MockClient_VerifyHook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*http.Request), args[1].(string)) + }) + return _c +} + +func (_c *MockClient_VerifyHook_Call) Return(_a0 []byte, _a1 error) *MockClient_VerifyHook_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_VerifyHook_Call) RunAndReturn(run func(*http.Request, string) ([]byte, error)) *MockClient_VerifyHook_Call { + _c.Call.Return(run) + return _c +} + +// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockClient(t interface { + mock.TestingT + Cleanup(func()) +}) *MockClient { + mock := &MockClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/affected_apps/argocd_matcher.go b/pkg/affected_apps/argocd_matcher.go index 0c1b7ded..935daeeb 100644 --- a/pkg/affected_apps/argocd_matcher.go +++ b/pkg/affected_apps/argocd_matcher.go @@ -6,7 +6,6 @@ import ( "github.com/rs/zerolog/log" "github.com/zapier/kubechecks/pkg/appdir" - "github.com/zapier/kubechecks/pkg/container" "github.com/zapier/kubechecks/pkg/git" ) @@ -15,7 +14,7 @@ type ArgocdMatcher struct { appSetsDirectory *appdir.AppSetDirectory } -func NewArgocdMatcher(vcsToArgoMap container.VcsToArgoMap, repo *git.Repo) (*ArgocdMatcher, error) { +func NewArgocdMatcher(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo) (*ArgocdMatcher, error) { repoApps := getArgocdApps(vcsToArgoMap, repo) kustomizeAppFiles := getKustomizeApps(vcsToArgoMap, repo, repo.Directory) @@ -41,7 +40,7 @@ func logCounts(repoApps *appdir.AppDirectory) { } } -func getKustomizeApps(vcsToArgoMap container.VcsToArgoMap, repo *git.Repo, repoPath string) *appdir.AppDirectory { +func getKustomizeApps(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo, repoPath string) *appdir.AppDirectory { log.Debug().Msgf("creating fs for %s", repoPath) fs := os.DirFS(repoPath) log.Debug().Msg("following kustomize apps") @@ -51,7 +50,7 @@ func getKustomizeApps(vcsToArgoMap container.VcsToArgoMap, repo *git.Repo, repoP return kustomizeAppFiles } -func getArgocdApps(vcsToArgoMap container.VcsToArgoMap, repo *git.Repo) *appdir.AppDirectory { +func getArgocdApps(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo) *appdir.AppDirectory { log.Debug().Msgf("looking for %s repos", repo.CloneURL) repoApps := vcsToArgoMap.GetAppsInRepo(repo.CloneURL) @@ -59,7 +58,7 @@ func getArgocdApps(vcsToArgoMap container.VcsToArgoMap, repo *git.Repo) *appdir. return repoApps } -func getArgocdAppSets(vcsToArgoMap container.VcsToArgoMap, repo *git.Repo) *appdir.AppSetDirectory { +func getArgocdAppSets(vcsToArgoMap appdir.VcsToArgoMap, repo *git.Repo) *appdir.AppSetDirectory { log.Debug().Msgf("looking for %s repos", repo.CloneURL) repoApps := vcsToArgoMap.GetAppSetsInRepo(repo.CloneURL) diff --git a/pkg/app_watcher/app_watcher.go b/pkg/app_watcher/app_watcher.go index adb39c26..db183dff 100644 --- a/pkg/app_watcher/app_watcher.go +++ b/pkg/app_watcher/app_watcher.go @@ -12,11 +12,11 @@ import ( informers "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions/application/v1alpha1" applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1" "github.com/rs/zerolog/log" + "github.com/zapier/kubechecks/pkg/appdir" + "github.com/zapier/kubechecks/pkg/container" "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" - "github.com/zapier/kubechecks/pkg/appdir" "github.com/zapier/kubechecks/pkg/config" ) @@ -34,16 +34,16 @@ type ApplicationWatcher struct { // - kubeCfg is the Kubernetes configuration. // - vcsToArgoMap is the mapping between VCS and Argo applications. // - cfg is the server configuration. -func NewApplicationWatcher(kubeCfg *rest.Config, vcsToArgoMap appdir.VcsToArgoMap, cfg config.ServerConfig) (*ApplicationWatcher, error) { - if kubeCfg == nil { +func NewApplicationWatcher(ctr container.Container) (*ApplicationWatcher, error) { + if ctr.KubeClientSet == nil { return nil, fmt.Errorf("kubeCfg cannot be nil") } ctrl := ApplicationWatcher{ - applicationClientset: appclientset.NewForConfigOrDie(kubeCfg), - vcsToArgoMap: vcsToArgoMap, + applicationClientset: appclientset.NewForConfigOrDie(ctr.KubeClientSet.Config()), + vcsToArgoMap: ctr.VcsToArgoMap, } - appInformer, appLister := ctrl.newApplicationInformerAndLister(time.Second*30, cfg) + appInformer, appLister := ctrl.newApplicationInformerAndLister(time.Second*30, ctr.Config) ctrl.appInformer = appInformer ctrl.appLister = appLister @@ -152,14 +152,14 @@ func canProcessApp(obj interface{}) (*appv1alpha1.Application, bool) { return nil, false } - for _, src := range app.Spec.Sources { + if src := app.Spec.Source; src != nil { if isGitRepo(src.RepoURL) { return app, true } } - if app.Spec.Source != nil { - if isGitRepo(app.Spec.Source.RepoURL) { + for _, src := range app.Spec.Sources { + if isGitRepo(src.RepoURL) { return app, true } } diff --git a/pkg/app_watcher/appset_watcher.go b/pkg/app_watcher/appset_watcher.go index cc90fed6..c98fedf6 100644 --- a/pkg/app_watcher/appset_watcher.go +++ b/pkg/app_watcher/appset_watcher.go @@ -13,8 +13,8 @@ import ( "github.com/rs/zerolog/log" "github.com/zapier/kubechecks/pkg/appdir" "github.com/zapier/kubechecks/pkg/config" + "github.com/zapier/kubechecks/pkg/container" "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" ) @@ -28,16 +28,16 @@ type ApplicationSetWatcher struct { } // NewApplicationSetWatcher creates new instance of ApplicationWatcher. -func NewApplicationSetWatcher(kubeCfg *rest.Config, vcsToArgoMap appdir.VcsToArgoMap, cfg config.ServerConfig) (*ApplicationSetWatcher, error) { - if kubeCfg == nil { +func NewApplicationSetWatcher(ctr container.Container) (*ApplicationSetWatcher, error) { + if ctr.KubeClientSet == nil { return nil, fmt.Errorf("kubeCfg cannot be nil") } ctrl := ApplicationSetWatcher{ - applicationClientset: appclientset.NewForConfigOrDie(kubeCfg), - vcsToArgoMap: vcsToArgoMap, + applicationClientset: appclientset.NewForConfigOrDie(ctr.KubeClientSet.Config()), + vcsToArgoMap: ctr.VcsToArgoMap, } - appInformer, appLister := ctrl.newApplicationSetInformerAndLister(time.Second*30, cfg) + appInformer, appLister := ctrl.newApplicationSetInformerAndLister(time.Second*30, ctr.Config) ctrl.appInformer = appInformer ctrl.appLister = appLister diff --git a/pkg/appdir/vcstoargomap.go b/pkg/appdir/vcstoargomap.go index 0becae94..eeafc070 100644 --- a/pkg/appdir/vcstoargomap.go +++ b/pkg/appdir/vcstoargomap.go @@ -79,37 +79,39 @@ func (v2a VcsToArgoMap) WalkKustomizeApps(cloneURL string, fs fs.FS) *AppDirecto return result } -func (v2a VcsToArgoMap) AddApp(app *v1alpha1.Application) { - if app.Spec.Source == nil { - log.Warn().Msgf("%s/%s: no source, skipping", app.Namespace, app.Name) - return +func (v2a VcsToArgoMap) processApp(app v1alpha1.Application, fn func(*AppDirectory)) { + + if src := app.Spec.Source; src != nil { + appDirectory := v2a.GetAppsInRepo(src.RepoURL) + fn(appDirectory) } - appDirectory := v2a.GetAppsInRepo(app.Spec.Source.RepoURL) - appDirectory.ProcessApp(*app) + for _, src := range app.Spec.Sources { + appDirectory := v2a.GetAppsInRepo(src.RepoURL) + fn(appDirectory) + } } -func (v2a VcsToArgoMap) UpdateApp(old *v1alpha1.Application, new *v1alpha1.Application) { - if new.Spec.Source == nil { - log.Warn().Msgf("%s/%s: no source, skipping", new.Namespace, new.Name) - return - } +func (v2a VcsToArgoMap) AddApp(app *v1alpha1.Application) { + v2a.processApp(*app, func(directory *AppDirectory) { + directory.AddApp(*app) + }) +} - oldAppDirectory := v2a.GetAppsInRepo(old.Spec.Source.RepoURL) - oldAppDirectory.RemoveApp(*old) +func (v2a VcsToArgoMap) UpdateApp(old *v1alpha1.Application, new *v1alpha1.Application) { + v2a.processApp(*old, func(directory *AppDirectory) { + directory.RemoveApp(*old) + }) - newAppDirectory := v2a.GetAppsInRepo(new.Spec.Source.RepoURL) - newAppDirectory.ProcessApp(*new) + v2a.processApp(*new, func(directory *AppDirectory) { + directory.AddApp(*new) + }) } func (v2a VcsToArgoMap) DeleteApp(app *v1alpha1.Application) { - if app.Spec.Source == nil { - log.Warn().Msgf("%s/%s: no source, skipping", app.Namespace, app.Name) - return - } - - oldAppDirectory := v2a.GetAppsInRepo(app.Spec.Source.RepoURL) - oldAppDirectory.RemoveApp(*app) + v2a.processApp(*app, func(directory *AppDirectory) { + directory.RemoveApp(*app) + }) } func (v2a VcsToArgoMap) GetVcsRepos() []string { diff --git a/pkg/argo_client/applications.go b/pkg/argo_client/applications.go index 8694a555..11acb80c 100644 --- a/pkg/argo_client/applications.go +++ b/pkg/argo_client/applications.go @@ -24,11 +24,11 @@ var ErrNoVersionFound = errors.New("no kubernetes version found") // GetApplicationByName takes a context and a name, then queries the Argo Application client to retrieve the Application with the specified name. // It returns the found Application and any error encountered during the process. // If successful, the Application client connection is closed before returning. -func (argo *ArgoClient) GetApplicationByName(ctx context.Context, name string) (*v1alpha1.Application, error) { +func (a *ArgoClient) GetApplicationByName(ctx context.Context, name string) (*v1alpha1.Application, error) { ctx, span := tracer.Start(ctx, "GetApplicationByName") defer span.End() - closer, appClient := argo.GetApplicationClient() + closer, appClient := a.GetApplicationClient() defer closer.Close() resp, err := appClient.Get(ctx, &application.ApplicationQuery{Name: &name}) @@ -43,7 +43,7 @@ func (argo *ArgoClient) GetApplicationByName(ctx context.Context, name string) ( // GetKubernetesVersionByApplication is a method on the ArgoClient struct that takes a context and an application name as parameters, // and returns the Kubernetes version of the destination cluster where the specified application is running. // It returns an error if the application or cluster information cannot be retrieved. -func (argo *ArgoClient) GetKubernetesVersionByApplication(ctx context.Context, app v1alpha1.Application) (string, error) { +func (a *ArgoClient) GetKubernetesVersionByApplication(ctx context.Context, app v1alpha1.Application) (string, error) { ctx, span := tracer.Start(ctx, "GetKubernetesVersionByApplicationName") defer span.End() @@ -58,7 +58,7 @@ func (argo *ArgoClient) GetKubernetesVersionByApplication(ctx context.Context, a } // Get cluster client - clusterCloser, clusterClient := argo.GetClusterClient() + clusterCloser, clusterClient := a.GetClusterClient() defer clusterCloser.Close() // Get cluster @@ -85,11 +85,11 @@ func (argo *ArgoClient) GetKubernetesVersionByApplication(ctx context.Context, a // GetApplicationsByLabels takes a context and a labelselector, then queries the Argo Application client to retrieve the Applications with the specified label. // It returns the found ApplicationList and any error encountered during the process. // If successful, the Application client connection is closed before returning. -func (argo *ArgoClient) GetApplicationsByLabels(ctx context.Context, labels string) (*v1alpha1.ApplicationList, error) { +func (a *ArgoClient) GetApplicationsByLabels(ctx context.Context, labels string) (*v1alpha1.ApplicationList, error) { ctx, span := tracer.Start(ctx, "GetApplicationsByLabels") defer span.End() - closer, appClient := argo.GetApplicationClient() + closer, appClient := a.GetApplicationClient() defer closer.Close() resp, err := appClient.List(ctx, &application.ApplicationQuery{Selector: &labels}) @@ -103,31 +103,31 @@ func (argo *ArgoClient) GetApplicationsByLabels(ctx context.Context, labels stri // GetApplicationsByAppset takes a context and an appset, then queries the Argo Application client to retrieve the Applications managed by the appset // It returns the found ApplicationList and any error encountered during the process. -func (argo *ArgoClient) GetApplicationsByAppset(ctx context.Context, name string) (*v1alpha1.ApplicationList, error) { +func (a *ArgoClient) GetApplicationsByAppset(ctx context.Context, name string) (*v1alpha1.ApplicationList, error) { appsetLabelSelector := "argocd.argoproj.io/application-set-name=" + name - return argo.GetApplicationsByLabels(ctx, appsetLabelSelector) + return a.GetApplicationsByLabels(ctx, appsetLabelSelector) } -func (argo *ArgoClient) GetApplications(ctx context.Context) (*v1alpha1.ApplicationList, error) { +func (a *ArgoClient) GetApplications(ctx context.Context) (*v1alpha1.ApplicationList, error) { ctx, span := tracer.Start(ctx, "GetApplications") defer span.End() - closer, appClient := argo.GetApplicationClient() + closer, appClient := a.GetApplicationClient() defer closer.Close() resp, err := appClient.List(ctx, new(application.ApplicationQuery)) if err != nil { telemetry.SetError(span, err, "Argo List All Applications error") - return nil, errors.Wrap(err, "failed to applications") + return nil, errors.Wrap(err, "failed to list applications") } return resp, nil } -func (argo *ArgoClient) GetApplicationSets(ctx context.Context) (*v1alpha1.ApplicationSetList, error) { +func (a *ArgoClient) GetApplicationSets(ctx context.Context) (*v1alpha1.ApplicationSetList, error) { ctx, span := tracer.Start(ctx, "GetApplications") defer span.End() - closer, appClient := argo.GetApplicationSetClient() + closer, appClient := a.GetApplicationSetClient() defer closer.Close() resp, err := appClient.List(ctx, new(applicationset.ApplicationSetListQuery)) diff --git a/pkg/argo_client/client.go b/pkg/argo_client/client.go index ac3af7c9..015201dd 100644 --- a/pkg/argo_client/client.go +++ b/pkg/argo_client/client.go @@ -1,14 +1,21 @@ package argo_client import ( + "crypto/tls" "io" - "sync" "github.com/argoproj/argo-cd/v2/pkg/apiclient" "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" "github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset" "github.com/argoproj/argo-cd/v2/pkg/apiclient/settings" + repoapiclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + client "github.com/zapier/kubechecks/pkg/kubernetes" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster" @@ -18,10 +25,17 @@ import ( type ArgoClient struct { client apiclient.Client - manifestsLock sync.Mutex + repoClient repoapiclient.RepoServerServiceClient + namespace string + k8s kubernetes.Interface + k8sConfig *rest.Config + sendFullRepository bool } -func NewArgoClient(cfg config.ServerConfig) (*ArgoClient, error) { +func NewArgoClient( + cfg config.ServerConfig, + k8s client.Interface, +) (*ArgoClient, error) { opts := &apiclient.ClientOptions{ ServerAddr: cfg.ArgoCDServerAddr, AuthToken: cfg.ArgoCDToken, @@ -42,38 +56,54 @@ func NewArgoClient(cfg config.ServerConfig) (*ArgoClient, error) { return nil, err } + log.Info().Msg("creating client") + tlsConfig := tls.Config{InsecureSkipVerify: cfg.ArgoCDRepositoryInsecure} + conn, err := grpc.NewClient(cfg.ArgoCDRepositoryEndpoint, + grpc.WithTransportCredentials( + credentials.NewTLS(&tlsConfig), + ), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to create client") + } + return &ArgoClient{ - client: argo, + repoClient: repoapiclient.NewRepoServerServiceClient(conn), + client: argo, + namespace: cfg.ArgoCDNamespace, + k8s: k8s.ClientSet(), + k8sConfig: k8s.Config(), + sendFullRepository: cfg.ArgoCDSendFullRepository, }, nil } // GetApplicationClient has related argocd diff code https://github.com/argoproj/argo-cd/blob/d3ff9757c460ae1a6a11e1231251b5d27aadcdd1/cmd/argocd/commands/app.go#L899 -func (argo *ArgoClient) GetApplicationClient() (io.Closer, application.ApplicationServiceClient) { - closer, appClient, err := argo.client.NewApplicationClient() +func (a *ArgoClient) GetApplicationClient() (io.Closer, application.ApplicationServiceClient) { + closer, appClient, err := a.client.NewApplicationClient() if err != nil { log.Fatal().Err(err).Msg("could not create ArgoCD Application Client") } return closer, appClient } -func (argo *ArgoClient) GetApplicationSetClient() (io.Closer, applicationset.ApplicationSetServiceClient) { - closer, appClient, err := argo.client.NewApplicationSetClient() +func (a *ArgoClient) GetApplicationSetClient() (io.Closer, applicationset.ApplicationSetServiceClient) { + closer, appClient, err := a.client.NewApplicationSetClient() if err != nil { log.Fatal().Err(err).Msg("could not create ArgoCD Application Set Client") } return closer, appClient } -func (argo *ArgoClient) GetSettingsClient() (io.Closer, settings.SettingsServiceClient) { - closer, appClient, err := argo.client.NewSettingsClient() +func (a *ArgoClient) GetSettingsClient() (io.Closer, settings.SettingsServiceClient) { + closer, appClient, err := a.client.NewSettingsClient() if err != nil { log.Fatal().Err(err).Msg("could not create ArgoCD Settings Client") } return closer, appClient } -func (argo *ArgoClient) GetClusterClient() (io.Closer, cluster.ClusterServiceClient) { - closer, clusterClient, err := argo.client.NewClusterClient() +func (a *ArgoClient) GetClusterClient() (io.Closer, cluster.ClusterServiceClient) { + closer, clusterClient, err := a.client.NewClusterClient() if err != nil { log.Fatal().Err(err).Msg("could not create ArgoCD Cluster Client") } diff --git a/pkg/argo_client/manifests.go b/pkg/argo_client/manifests.go index 0587e223..27cd4ba4 100644 --- a/pkg/argo_client/manifests.go +++ b/pkg/argo_client/manifests.go @@ -1,31 +1,40 @@ package argo_client import ( + "bufio" "context" "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" "time" "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster" + "github.com/argoproj/argo-cd/v2/pkg/apiclient/project" "github.com/argoproj/argo-cd/v2/pkg/apiclient/settings" - argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" repoapiclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient" - "github.com/argoproj/argo-cd/v2/reposerver/repository" - "github.com/argoproj/argo-cd/v2/util/git" - "github.com/ghodss/yaml" + "github.com/argoproj/argo-cd/v2/util/argo" + "github.com/argoproj/argo-cd/v2/util/db" + argosettings "github.com/argoproj/argo-cd/v2/util/settings" + "github.com/argoproj/argo-cd/v2/util/tgzstream" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "k8s.io/apimachinery/pkg/api/resource" - - "github.com/zapier/kubechecks/telemetry" + "github.com/zapier/kubechecks/pkg" + "github.com/zapier/kubechecks/pkg/git" + "github.com/zapier/kubechecks/pkg/vcs" ) -func (argo *ArgoClient) GetManifestsLocal(ctx context.Context, name, tempRepoDir, changedAppFilePath string, app argoappv1.Application) ([]string, error) { - var err error +type getRepo func(ctx context.Context, cloneURL string, branchName string) (*git.Repo, error) - ctx, span := tracer.Start(ctx, "GetManifestsLocal") +func (a *ArgoClient) GetManifests(ctx context.Context, name string, app v1alpha1.Application, pullRequest vcs.PullRequest, getRepo getRepo) ([]string, error) { + ctx, span := tracer.Start(ctx, "GetManifests") defer span.End() - log.Debug().Str("name", name).Msg("GetManifestsLocal") + log.Debug().Str("name", name).Msg("GetManifests") start := time.Now() defer func() { @@ -33,84 +42,436 @@ func (argo *ArgoClient) GetManifestsLocal(ctx context.Context, name, tempRepoDir getManifestsDuration.WithLabelValues(name).Observe(duration.Seconds()) }() - clusterCloser, clusterClient := argo.GetClusterClient() - defer clusterCloser.Close() + contentRefs, refs := preprocessSources(&app, pullRequest) - settingsCloser, settingsClient := argo.GetSettingsClient() - defer settingsCloser.Close() + var manifests []string + for _, source := range contentRefs { + moreManifests, err := a.generateManifests(ctx, app, source, refs, pullRequest, getRepo) + if err != nil { + return nil, errors.Wrap(err, "failed to generate manifests") + } + manifests = append(manifests, moreManifests...) + } + + getManifestsSuccess.WithLabelValues(name).Inc() + return manifests, nil +} + +// preprocessSources splits the content sources from the ref sources, and transforms source refs that point at the pull +// request's base into refs that point at the pull request's head. This is necessary to generate manifests based on what +// the world will look like _after_ the branch gets merged in. +func preprocessSources(app *v1alpha1.Application, pullRequest vcs.PullRequest) ([]v1alpha1.ApplicationSource, []v1alpha1.ApplicationSource) { + if !app.Spec.HasMultipleSources() { + return []v1alpha1.ApplicationSource{app.Spec.GetSource()}, nil + } + + // collect all ref sources, map by name + var contentSources []v1alpha1.ApplicationSource + var refSources []v1alpha1.ApplicationSource + + for _, source := range app.Spec.Sources { + if source.Ref == "" { + contentSources = append(contentSources, source) + continue + } + + /* + This is to make sure that the respository server understands where to pull the values.yaml file from. + + Or put differently: + + | PR Repo | PR Base | PR Target | Ref Repo | Ref Target | | + | --------- | ----------- | --------- | --------- | ---------- | ----------------------------------------------------------------------------------------------- | + | repo1.git | new-feature | main | repo1.git | main | need to change main to new-feature for preview, as the base will become the target after merge. | + | repo1.git | new-feature | main | repo2.git | main | no change, ref source refers to a different repository unaffected by the pull request | + | repo1.git | new-feature | main | repo1.git | staging | no change, ref source refers to a different branch than the pull request | + */ + if pkg.AreSameRepos(source.RepoURL, pullRequest.CloneURL) { + if source.TargetRevision == pullRequest.BaseRef { + source.TargetRevision = pullRequest.HeadRef + } + } + + refSources = append(refSources, source) + } + + return contentSources, refSources +} + +// generateManifests generates an Application along with all of its files, and sends it to the ArgoCD +// Repository service to be transformed into raw kubernetes manifests. This allows us to take advantage of server +// configuration and credentials. +func (a *ArgoClient) generateManifests(ctx context.Context, app v1alpha1.Application, source v1alpha1.ApplicationSource, refs []v1alpha1.ApplicationSource, pullRequest vcs.PullRequest, getRepo func(ctx context.Context, cloneURL string, branchName string) (*git.Repo, error)) ([]string, error) { + // The GenerateManifestWithFiles has some non-obvious rules due to assumptions that it makes: + // 1. first source must be a non-ref source + // 2. there must be one and only one non-ref source + // 3. ref sources that match the pull requests' repo and target branch need to have their target branch swapped to the head branch of the pull request + + clusterCloser, clusterClient := a.GetClusterClient() + defer clusterCloser.Close() - log.Debug(). - Str("clusterName", app.Spec.Destination.Name). - Str("clusterServer", app.Spec.Destination.Server). - Msg("getting cluster") cluster, err := clusterClient.Get(ctx, &cluster.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server}) if err != nil { - telemetry.SetError(span, err, "Argo Get Cluster") - getManifestsFailed.WithLabelValues(name).Inc() + getManifestsFailed.WithLabelValues(app.Name).Inc() return nil, errors.Wrap(err, "failed to get cluster") } + settingsCloser, settingsClient := a.GetSettingsClient() + defer settingsCloser.Close() + + log.Info().Msg("get settings") argoSettings, err := settingsClient.Get(ctx, &settings.SettingsQuery{}) if err != nil { - telemetry.SetError(span, err, "Argo Get Settings") - getManifestsFailed.WithLabelValues(name).Inc() + getManifestsFailed.WithLabelValues(app.Name).Inc() return nil, errors.Wrap(err, "failed to get settings") } - log.Debug().Str("name", name).Msg("generating diff for application...") - res, err := argo.generateManifests(ctx, fmt.Sprintf("%s/%s", tempRepoDir, changedAppFilePath), tempRepoDir, app, argoSettings, cluster) + settingsMgr := argosettings.NewSettingsManager(ctx, a.k8s, a.namespace) + argoDB := db.NewDB(a.namespace, settingsMgr, a.k8s) + + repoTarget := source.TargetRevision + if pkg.AreSameRepos(source.RepoURL, pullRequest.CloneURL) && areSameTargetRef(source.TargetRevision, pullRequest.BaseRef) { + repoTarget = pullRequest.HeadRef + } + + log.Info().Msg("get repo") + repo, err := getRepo(ctx, source.RepoURL, repoTarget) if err != nil { - telemetry.SetError(span, err, "Generate Manifests") - return nil, errors.Wrap(err, "failed to generate manifests") + return nil, errors.Wrap(err, "failed to get repo") } - if res.Manifests == nil { - return nil, nil + var packageDir string + if a.sendFullRepository { + log.Info().Msg("sending full repository") + packageDir = repo.Directory + } else { + log.Info().Msg("packaging app") + packageDir, err = packageApp(ctx, source, refs, repo, getRepo) + if err != nil { + return nil, errors.Wrap(err, "failed to package application") + } + } + + log.Info().Msg("compressing files") + f, filesWritten, checksum, err := tgzstream.CompressFiles(packageDir, []string{"*"}, []string{".git"}) + if err != nil { + return nil, fmt.Errorf("failed to compress files: %w", err) + } + log.Info().Msgf("%d files compressed", filesWritten) + //if filesWritten == 0 { + // return nil, fmt.Errorf("no files to send") + //} + + closer, projectClient, err := a.client.NewProjectClient() + if err != nil { + return nil, errors.Wrap(err, "failed to get project client") + } + defer closer.Close() + + proj, err := projectClient.Get(ctx, &project.ProjectQuery{Name: app.Spec.Project}) + if err != nil { + return nil, fmt.Errorf("error getting app project: %w", err) + } + + helmRepos, err := argoDB.ListHelmRepositories(ctx) + if err != nil { + return nil, fmt.Errorf("error listing helm repositories: %w", err) + } + permittedHelmRepos, err := argo.GetPermittedRepos(proj, helmRepos) + if err != nil { + return nil, fmt.Errorf("error retrieving permitted repos: %w", err) + } + helmRepositoryCredentials, err := argoDB.GetAllHelmRepositoryCredentials(ctx) + if err != nil { + return nil, fmt.Errorf("error getting helm repository credentials: %w", err) + } + helmOptions, err := settingsMgr.GetHelmSettings() + if err != nil { + return nil, fmt.Errorf("error getting helm settings: %w", err) + } + permittedHelmCredentials, err := argo.GetPermittedReposCredentials(proj, helmRepositoryCredentials) + if err != nil { + return nil, fmt.Errorf("error getting permitted repos credentials: %w", err) + } + enabledSourceTypes, err := settingsMgr.GetEnabledSourceTypes() + if err != nil { + return nil, fmt.Errorf("error getting settings enabled source types: %w", err) + } + + refSources, err := argo.GetRefSources(context.Background(), app.Spec.Sources, app.Spec.Project, argoDB.GetRepository, []string{}, false) + if err != nil { + return nil, fmt.Errorf("failed to get ref sources: %w", err) + } + + app.Spec.Sources = append([]v1alpha1.ApplicationSource{source}, refs...) + + q := repoapiclient.ManifestRequest{ + Repo: &v1alpha1.Repository{Repo: source.RepoURL}, + Revision: source.TargetRevision, + AppLabelKey: argoSettings.AppLabelKey, + AppName: app.Name, + Namespace: app.Spec.Destination.Namespace, + ApplicationSource: &source, + Repos: permittedHelmRepos, + KustomizeOptions: argoSettings.KustomizeOptions, + KubeVersion: cluster.Info.ServerVersion, + ApiVersions: cluster.Info.APIVersions, + HelmRepoCreds: permittedHelmCredentials, + HelmOptions: helmOptions, + TrackingMethod: argoSettings.TrackingMethod, + EnabledSourceTypes: enabledSourceTypes, + ProjectName: proj.Name, + ProjectSourceRepos: proj.Spec.SourceRepos, + HasMultipleSources: app.Spec.HasMultipleSources(), + RefSources: refSources, + } + + log.Info().Msg("generating manifest with files") + stream, err := a.repoClient.GenerateManifestWithFiles(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get manifests with files") } - getManifestsSuccess.WithLabelValues(name).Inc() - return res.Manifests, nil -} -func (argo *ArgoClient) generateManifests( - ctx context.Context, appPath, tempRepoDir string, app argoappv1.Application, argoSettings *settings.Settings, cluster *argoappv1.Cluster, -) (*repoapiclient.ManifestResponse, error) { - argo.manifestsLock.Lock() - defer argo.manifestsLock.Unlock() - - source := app.Spec.GetSource() - - return repository.GenerateManifests( - ctx, - appPath, - tempRepoDir, - source.TargetRevision, - &repoapiclient.ManifestRequest{ - Repo: &argoappv1.Repository{Repo: source.RepoURL}, - AppLabelKey: argoSettings.AppLabelKey, - AppName: app.Name, - Namespace: app.Spec.Destination.Namespace, - ApplicationSource: &source, - KustomizeOptions: argoSettings.KustomizeOptions, - KubeVersion: cluster.Info.ServerVersion, - ApiVersions: cluster.Info.APIVersions, - TrackingMethod: argoSettings.TrackingMethod, + log.Info().Msg("sending request") + if err := stream.Send(&repoapiclient.ManifestRequestWithFiles{ + Part: &repoapiclient.ManifestRequestWithFiles_Request{ + Request: &q, }, - true, - new(git.NoopCredsStore), - resource.MustParse("0"), - nil, - ) + }); err != nil { + return nil, errors.Wrap(err, "failed to send request") + } + + log.Info().Msg("sending metadata") + if err := stream.Send(&repoapiclient.ManifestRequestWithFiles{ + Part: &repoapiclient.ManifestRequestWithFiles_Metadata{ + Metadata: &repoapiclient.ManifestFileMetadata{ + Checksum: checksum, + }, + }, + }); err != nil { + return nil, errors.Wrap(err, "failed to send metadata") + } + + log.Info().Msg("sending file") + err = sendFile(ctx, stream, f) + if err != nil { + return nil, fmt.Errorf("failed to send manifest stream file: %w", err) + } + + log.Info().Msg("receiving repsonse") + response, err := stream.CloseAndRecv() + if err != nil { + return nil, errors.Wrap(err, "failed to get response") + } + + log.Info().Msg("done!") + return response.Manifests, nil } -func ConvertJsonToYamlManifests(jsonManifests []string) []string { - var manifests []string - for _, manifest := range jsonManifests { - ret, err := yaml.JSONToYAML([]byte(manifest)) +func copyFile(srcpath, dstpath string) error { + dstdir := filepath.Dir(dstpath) + if err := os.MkdirAll(dstdir, 0o777); err != nil { + return errors.Wrap(err, "failed to make directories") + } + + r, err := os.Open(srcpath) + if err != nil { + return err + } + defer r.Close() // ignore error: file was opened read-only. + + w, err := os.Create(dstpath) + if err != nil { + return err + } + + defer func() { + // Report the error, if any, from Close, but do so + // only if there isn't already an outgoing error. + if c := w.Close(); err == nil { + err = c + } + }() + + _, err = io.Copy(w, r) + return err +} + +func packageApp(ctx context.Context, source v1alpha1.ApplicationSource, refs []v1alpha1.ApplicationSource, repo *git.Repo, getRepo getRepo) (string, error) { + tempDir, err := os.MkdirTemp("", "package-*") + if err != nil { + return "", errors.Wrap(err, "failed to make temp dir") + } + + tempAppDir := filepath.Join(tempDir, source.Path) + appPath := filepath.Join(repo.Directory, source.Path) + + // copy app files to the temp dir + if err = filepath.Walk(appPath, func(path string, info fs.FileInfo, err error) error { if err != nil { - log.Warn().Err(err).Msg("Failed to format manifest") - continue + return err + } + + if info.IsDir() { + return nil + } + + relPath, err := filepath.Rel(appPath, path) + if err != nil { + return errors.Wrapf(err, "failed to calculate rel between %q and %q", appPath, path) + } + src := path + dst := filepath.Join(tempAppDir, relPath) + if err := copyFile(src, dst); err != nil { + return errors.Wrapf(err, "failed to %s => %s", src, dst) + } + return nil + }); err != nil { + return "", errors.Wrap(err, "failed to copy files") + } + + if source.Helm != nil { + refsByName := make(map[string]v1alpha1.ApplicationSource) + for _, ref := range refs { + refsByName[ref.Ref] = ref + } + + for index, valueFile := range source.Helm.ValueFiles { + if strings.HasPrefix(valueFile, "$") { + relPath, err := processValueReference(ctx, source, valueFile, refsByName, repo, getRepo, tempDir, tempAppDir) + if err != nil { + return "", err + } + + source.Helm.ValueFiles[index] = relPath + continue + } + + if strings.Contains(valueFile, "://") { + continue + } + + relPath, err := filepath.Rel(source.Path, valueFile) + if err != nil { + return "", errors.Wrap(err, "failed to calculate relative path") + } + + if !strings.HasPrefix(relPath, "../") { + continue // this values file is already copied + } + + src := filepath.Join(appPath, valueFile) + dst := filepath.Join(tempAppDir, valueFile) + if err = copyFile(src, dst); err != nil { + if !ignoreValuesFileCopyError(source, valueFile, err) { + return "", errors.Wrapf(err, "failed to copy file: %q", valueFile) + } + } } - manifests = append(manifests, fmt.Sprintf("---\n%s", string(ret))) } - return manifests + + return tempDir, nil +} + +func processValueReference( + ctx context.Context, + source v1alpha1.ApplicationSource, + valueFile string, + refsByName map[string]v1alpha1.ApplicationSource, + repo *git.Repo, + getRepo getRepo, + tempDir, tempAppDir string, +) (string, error) { + refName, refPath, err := splitRefFromPath(valueFile) + if err != nil { + return "", errors.Wrap(err, "failed to parse value file") + } + + ref, ok := refsByName[refName] + if !ok { + return "", errors.Wrap(err, "value file points at missing ref") + } + + refRepo := repo + if !pkg.AreSameRepos(ref.RepoURL, repo.CloneURL) { + refRepo, err = getRepo(ctx, ref.RepoURL, ref.TargetRevision) + if err != nil { + return "", errors.Wrapf(err, "failed to clone repo: %q", ref.RepoURL) + } + } + + src := filepath.Join(refRepo.Directory, refPath) + dst := filepath.Join(tempDir, ".refs", refName, refPath) + if err = copyFile(src, dst); err != nil { + if !ignoreValuesFileCopyError(source, valueFile, err) { + return "", errors.Wrapf(err, "failed to copy referenced value file: %q", valueFile) + } + } + + relPath, err := filepath.Rel(tempAppDir, dst) + if err != nil { + return "", errors.Wrap(err, "failed to find a relative path") + } + return relPath, nil +} + +func ignoreValuesFileCopyError(source v1alpha1.ApplicationSource, valueFile string, err error) bool { + if errors.Is(err, os.ErrNotExist) && source.Helm.IgnoreMissingValueFiles { + log.Debug().Str("valueFile", valueFile).Msg("ignore missing values file, because source.Helm.IgnoreMissingValueFiles is true") + return true + } + + return false +} + +var valueRef = regexp.MustCompile(`^\$([^/]+)/(.*)$`) +var ErrInvalidSourceRef = errors.New("invalid value ref") + +func splitRefFromPath(file string) (string, string, error) { + match := valueRef.FindStringSubmatch(file) + if match == nil { + return "", "", ErrInvalidSourceRef + } + + return match[1], match[2], nil +} + +type sender interface { + Send(*repoapiclient.ManifestRequestWithFiles) error +} + +func sendFile(ctx context.Context, sender sender, file *os.File) error { + reader := bufio.NewReader(file) + chunk := make([]byte, 1024) + for { + if ctx != nil { + if err := ctx.Err(); err != nil { + return fmt.Errorf("client stream context error: %w", err) + } + } + n, err := reader.Read(chunk) + if n > 0 { + fr := &repoapiclient.ManifestRequestWithFiles{ + Part: &repoapiclient.ManifestRequestWithFiles_Chunk{ + Chunk: &repoapiclient.ManifestFileChunk{ + Chunk: chunk[:n], + }, + }, + } + if e := sender.Send(fr); e != nil { + return fmt.Errorf("error sending stream: %w", e) + } + } + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("buffer reader error: %w", err) + } + } + return nil +} + +func areSameTargetRef(ref1, ref2 string) bool { + return ref1 == ref2 } diff --git a/pkg/argo_client/manifests_test.go b/pkg/argo_client/manifests_test.go new file mode 100644 index 00000000..662d324d --- /dev/null +++ b/pkg/argo_client/manifests_test.go @@ -0,0 +1,471 @@ +package argo_client + +import ( + "context" + "crypto/md5" + "encoding/hex" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zapier/kubechecks/pkg" + "github.com/zapier/kubechecks/pkg/git" + "github.com/zapier/kubechecks/pkg/vcs" +) + +func TestAreSameTargetRef(t *testing.T) { + testcases := map[string]struct { + ref1, ref2 string + expected bool + }{ + "same": {"one", "one", true}, + "different": {"one", "two", false}, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + actual := areSameTargetRef(tc.ref1, tc.ref2) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestSplitRefFromPath(t *testing.T) { + testcases := map[string]struct { + input string + refName, path string + err error + }{ + "simple": { + "$values/charts/prometheus/values.yaml", "values", "charts/prometheus/values.yaml", nil, + }, + "too-short": { + "$values", "", "", ErrInvalidSourceRef, + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + ref, path, err := splitRefFromPath(tc.input) + if tc.err != nil { + assert.EqualError(t, err, tc.err.Error()) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tc.refName, ref) + assert.Equal(t, tc.path, path) + }) + } +} + +func TestPreprocessSources(t *testing.T) { + t.Run("one source", func(t *testing.T) { + app := &v1alpha1.Application{ + Spec: v1alpha1.ApplicationSpec{ + Source: &v1alpha1.ApplicationSource{}, + }, + } + pr := vcs.PullRequest{} + + sources, refs := preprocessSources(app, pr) + assert.Len(t, sources, 1) + assert.Len(t, refs, 0) + }) + + t.Run("one multisource", func(t *testing.T) { + app := &v1alpha1.Application{ + Spec: v1alpha1.ApplicationSpec{ + Sources: []v1alpha1.ApplicationSource{{}}, + }, + } + pr := vcs.PullRequest{} + + sources, refs := preprocessSources(app, pr) + assert.Len(t, sources, 1) + assert.Len(t, refs, 0) + }) + + t.Run("one source, one ref, needs targetrev transform", func(t *testing.T) { + app := &v1alpha1.Application{ + Spec: v1alpha1.ApplicationSpec{ + Sources: []v1alpha1.ApplicationSource{ + { + Ref: "", + RepoURL: "git@github.com:argoproj/argo-cd.git", + TargetRevision: "main", + }, + { + Ref: "test-ref", + RepoURL: "https://github.com/argoproj/argo-cd.git", + TargetRevision: "main", + }, + }, + }, + } + + pr := vcs.PullRequest{ + CloneURL: "git@github.com:argoproj/argo-cd.git", + BaseRef: "main", + HeadRef: "test-ref", + } + + sources, refs := preprocessSources(app, pr) + require.Len(t, sources, 1) + assert.Equal(t, "main", sources[0].TargetRevision) + require.Len(t, refs, 1) + assert.Equal(t, "test-ref", refs[0].TargetRevision) + }) + + t.Run("one source, one ref, no targetrev transform", func(t *testing.T) { + app := &v1alpha1.Application{ + Spec: v1alpha1.ApplicationSpec{ + Sources: []v1alpha1.ApplicationSource{ + { + Ref: "", + RepoURL: "git@github.com:argoproj/argo-cd.git", + TargetRevision: "main", + }, + { + Ref: "test-ref", + RepoURL: "https://github.com/argoproj/argo-cd.git", + TargetRevision: "staging", + }, + }, + }, + } + + pr := vcs.PullRequest{ + CloneURL: "git@github.com:argoproj/argo-cd.git", + BaseRef: "main", + HeadRef: "test-ref", + } + + sources, refs := preprocessSources(app, pr) + require.Len(t, sources, 1) + assert.Equal(t, "main", sources[0].TargetRevision) + require.Len(t, refs, 1) + assert.Equal(t, "staging", refs[0].TargetRevision) + }) +} + +func TestCopyFile(t *testing.T) { + t.Run("simple", func(t *testing.T) { + tempSourcePath := filepath.Join(t.TempDir(), "tempsrc1.txt") + err := os.WriteFile(tempSourcePath, []byte("hello world"), 0o600) + require.NoError(t, err) + + tempDestinationPath := filepath.Join(t.TempDir(), "subdir", "tempdest1.txt") + err = copyFile(tempSourcePath, tempDestinationPath) + require.NoError(t, err) + + data, err := os.ReadFile(tempDestinationPath) + require.NoError(t, err) + + assert.Equal(t, []byte("hello world"), data) + }) +} + +type repoTarget struct { + repo, target string +} + +type repoTargetPath struct { + repo, target, path string +} + +func TestPackageApp(t *testing.T) { + testCases := map[string]struct { + app v1alpha1.Application + pullRequest vcs.PullRequest + filesByRepo map[repoTarget]set[string] + expectedFiles map[string]repoTargetPath + }{ + "unused-paths-are-ignored": { + app: v1alpha1.Application{ + Spec: v1alpha1.ApplicationSpec{ + Source: &v1alpha1.ApplicationSource{ + RepoURL: "git@github.com:testuser/testrepo.git", + Path: "app1/", + TargetRevision: "main", + }, + }, + }, + filesByRepo: map[repoTarget]set[string]{ + repoTarget{"git@github.com:testuser/testrepo.git", "main"}: newSet[string]( + "app1/Chart.yaml", + "app1/values.yaml", + "app2/Chart.yaml", + "app2/values.yaml", + ), + }, + expectedFiles: map[string]repoTargetPath{ + "app1/Chart.yaml": {"git@github.com:testuser/testrepo.git", "main", "app1/Chart.yaml"}, + "app1/values.yaml": {"git@github.com:testuser/testrepo.git", "main", "app1/values.yaml"}, + }, + }, + + "missing-values-can-be-accpetable": { + pullRequest: vcs.PullRequest{ + CloneURL: "git@github.com:testuser/testrepo.git", + BaseRef: "main", + HeadRef: "update-code", + }, + + app: v1alpha1.Application{ + Spec: v1alpha1.ApplicationSpec{ + Sources: []v1alpha1.ApplicationSource{ + { + RepoURL: "git@github.com:testuser/testrepo.git", + Path: "app1/", + TargetRevision: "main", + Helm: &v1alpha1.ApplicationSourceHelm{ + IgnoreMissingValueFiles: true, + ValueFiles: []string{ + "./values.yaml", + "missing.yaml", + "$staging/base.yaml", + "$staging/missing.yaml", + }, + }, + }, + { + Ref: "staging", + RepoURL: "git@github.com:testuser/otherrepo.git", + TargetRevision: "main", + }, + }, + }, + }, + + filesByRepo: map[repoTarget]set[string]{ + repoTarget{"git@github.com:testuser/testrepo.git", "main"}: newSet[string]( + "app1/Chart.yaml", + "app1/values.yaml", + "app2/Chart.yaml", + "app2/values.yaml", + ), + + repoTarget{"git@github.com:testuser/otherrepo.git", "main"}: newSet[string]( + "base.yaml", + ), + }, + + expectedFiles: map[string]repoTargetPath{ + "app1/Chart.yaml": {"git@github.com:testuser/testrepo.git", "main", "app1/Chart.yaml"}, + "app1/values.yaml": {"git@github.com:testuser/testrepo.git", "main", "app1/values.yaml"}, + ".refs/staging/base.yaml": {"git@github.com:testuser/otherrepo.git", "main", "base.yaml"}, + }, + }, + + "refs-are-copied": { + pullRequest: vcs.PullRequest{ + CloneURL: "git@github.com:testuser/testrepo.git", + BaseRef: "main", + HeadRef: "update-code", + }, + app: v1alpha1.Application{ + Spec: v1alpha1.ApplicationSpec{ + Sources: []v1alpha1.ApplicationSource{ + { + RepoURL: "git@github.com:testuser/testrepo.git", + Path: "app1/", + TargetRevision: "main", + Helm: &v1alpha1.ApplicationSourceHelm{ + ValueFiles: []string{ + "./values.yaml", + "./staging.yaml", + "$staging/base.yaml", + }, + }, + }, + { + Ref: "staging", + RepoURL: "git@github.com:testuser/otherrepo.git", + TargetRevision: "main", + }, + }, + }, + }, + filesByRepo: map[repoTarget]set[string]{ + repoTarget{"git@github.com:testuser/testrepo.git", "main"}: newSet[string]( + "app1/Chart.yaml", + "app1/values.yaml", + "app1/staging.yaml", + ), + repoTarget{"git@github.com:testuser/otherrepo.git", "main"}: newSet[string]( + "base.yaml", + ), + }, + expectedFiles: map[string]repoTargetPath{ + "app1/Chart.yaml": {"git@github.com:testuser/testrepo.git", "main", "app1/Chart.yaml"}, + "app1/values.yaml": {"git@github.com:testuser/testrepo.git", "main", "app1/values.yaml"}, + "app1/staging.yaml": {"git@github.com:testuser/testrepo.git", "main", "app1/staging.yaml"}, + ".refs/staging/base.yaml": {"git@github.com:testuser/otherrepo.git", "main", "base.yaml"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + var err error + ctx := context.Background() + + // write garbage content for files in fake repos, and + // store the tempdirs as repos + repoDirs, fileContentByRepo := createTestRepos(t, tc.filesByRepo) + + // split sources from refs + sources, refs := preprocessSources(&tc.app, tc.pullRequest) + require.Len(t, sources, 1) + source := sources[0] + + // get repos from the map, but nowhere else + getRepo := func(ctx context.Context, cloneURL, branchName string) (*git.Repo, error) { + repoHash := hash(t, repoTarget{cloneURL, branchName}) + repo, ok := repoDirs[repoHash] + if !ok { + return nil, errors.New("repo not found") + } + return repo, nil + } + + // get the source repo, which was created above + repo, err := getRepo(ctx, source.RepoURL, source.TargetRevision) + require.NoError(t, err) + + // FUNCTION UNDER TEST: package the app + path, err := packageApp(ctx, source, refs, repo, getRepo) + require.NoError(t, err) + + // ensure that only the expected files were copied + actualFiles := makeRelPathFilesSet(t, path) + expectedFilesSet := makeExpectedFilesSet(t, tc.expectedFiles) + extraCopiedFiles := actualFiles.Minus(expectedFilesSet) + assert.Empty(t, extraCopiedFiles, "extra files have been copied") + missingCopiedFiles := expectedFilesSet.Minus(actualFiles) + assert.Empty(t, missingCopiedFiles, "files that should have been packaged are missing") + + // verify that the correct files were written + for file, config := range tc.expectedFiles { + fullfile := filepath.Join(path, file) + actual, err := os.ReadFile(fullfile) + expected := fileContentByRepo[config] + if assert.NoError(t, err) { + assert.Equal(t, expected, string(actual)) + } + } + }) + } +} + +func makeExpectedFilesSet(t *testing.T, files map[string]repoTargetPath) set[string] { + t.Helper() + + result := newSet[string]() + + for path := range files { + result.Add(path) + } + + return result +} + +func createTestRepos(t *testing.T, filesByRepo map[repoTarget]set[string]) (map[string]*git.Repo, map[repoTargetPath]string) { + repoDirs := make(map[string]*git.Repo) + fileContents := make(map[repoTargetPath]string) + + var err error + + for cloneURL, files := range filesByRepo { + repoHash := hash(t, cloneURL) + tempDir := filepath.Join(t.TempDir(), repoHash) + repoDirs[repoHash] = &git.Repo{ + BranchName: cloneURL.target, + CloneURL: cloneURL.repo, + Directory: tempDir, + } + + for file := range files { + fullfilepath := filepath.Join(tempDir, file) + + // ensure the directories exist + filedir := filepath.Dir(fullfilepath) + err = os.MkdirAll(filedir, 0o755) + require.NoError(t, err) + + // generate and store content + fileContent := uuid.NewString() + fileContents[repoTargetPath{cloneURL.repo, cloneURL.target, file}] = fileContent + + // write the file to disk + err = os.WriteFile(fullfilepath, []byte(fileContent), 0o600) + require.NoError(t, err) + } + } + + return repoDirs, fileContents +} + +func makeRelPathFilesSet(t *testing.T, path string) set[string] { + files := newSet[string]() + err := filepath.Walk(path, func(fullPath string, info fs.FileInfo, err error) error { + require.NoError(t, err) + + if info.IsDir() { + return nil + } + + relPath, err := filepath.Rel(path, fullPath) + require.NoError(t, err) + + files.Add(relPath) + return nil + }) + require.NoError(t, err) + return files +} + +func hash(t *testing.T, repo repoTarget) string { + t.Helper() + + url, err := pkg.Canonicalize(repo.repo) + require.NoError(t, err) + + data := md5.Sum([]byte(url.Host + url.Path + repo.target)) + return hex.EncodeToString(data[:]) +} + +type set[T comparable] map[T]struct{} + +func newSet[T comparable](items ...T) set[T] { + result := make(set[T]) + for _, item := range items { + result.Add(item) + } + return result +} + +func (s set[T]) Add(value T) { + s[value] = struct{}{} +} + +func (s set[T]) Remove(value T) { + delete(s, value) +} + +func (s set[T]) Minus(other set[T]) set[T] { + result := newSet[T]() + for k := range s { + result.Add(k) + } + for k := range other { + result.Remove(k) + } + return result +} diff --git a/pkg/checks/diff/diff.go b/pkg/checks/diff/diff.go index 8d84d46c..4896dd66 100644 --- a/pkg/checks/diff/diff.go +++ b/pkg/checks/diff/diff.go @@ -259,18 +259,25 @@ func getArgoSettings(ctx context.Context, request checks.Request) (*settings.Set var nilApp = argoappv1.Application{} func isApp(item objKeyLiveTarget, manifests []byte) (argoappv1.Application, bool) { + logger := log.With(). + Str("kind", item.key.Kind). + Str("name", item.key.Name). + Str("namespace", item.key.Namespace). + Str("group", item.key.Group). + Logger() + if strings.ToLower(item.key.Group) != "argoproj.io" { - log.Debug().Str("group", item.key.Group).Msg("group is not correct") + logger.Debug().Msg("group is not correct") return nilApp, false } if strings.ToLower(item.key.Kind) != "application" { - log.Debug().Str("kind", item.key.Kind).Msg("kind is not correct") + logger.Debug().Msg("kind is not correct") return nilApp, false } var app argoappv1.Application if err := json.Unmarshal(manifests, &app); err != nil { - log.Warn().Err(err).Msg("failed to deserialize application") + logger.Warn().Err(err).Msg("failed to deserialize application") return nilApp, false } @@ -346,8 +353,9 @@ type resourceInfoProvider struct { namespacedByGk map[schema.GroupKind]bool } -// Infer if obj is namespaced or not from corresponding live objects list. If corresponding live object has namespace then target object is also namespaced. -// If live object is missing then it does not matter if target is namespaced or not. +// IsNamespaced infers if obj is namespaced or not from corresponding live objects list. If corresponding live object +// has namespace then target object is also namespaced. If live object is missing then it does not matter if target is +// namespaced or not. func (p *resourceInfoProvider) IsNamespaced(gk schema.GroupKind) (bool, error) { return p.namespacedByGk[gk], nil } diff --git a/pkg/checks/kubeconform/check.go b/pkg/checks/kubeconform/check.go index 2198470b..5dcfdb02 100644 --- a/pkg/checks/kubeconform/check.go +++ b/pkg/checks/kubeconform/check.go @@ -8,8 +8,5 @@ import ( ) func Check(ctx context.Context, request checks.Request) (msg.Result, error) { - return argoCdAppValidate( - ctx, request.Container, request.AppName, request.KubernetesVersion, request.Repo.Directory, - request.YamlManifests, - ) + return argoCdAppValidate(ctx, request.Container, request.AppName, request.KubernetesVersion, request.YamlManifests) } diff --git a/pkg/checks/kubeconform/validate.go b/pkg/checks/kubeconform/validate.go index 71439ef3..efba18e2 100644 --- a/pkg/checks/kubeconform/validate.go +++ b/pkg/checks/kubeconform/validate.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "path/filepath" "strings" "github.com/pkg/errors" @@ -20,7 +19,7 @@ import ( var tracer = otel.Tracer("pkg/checks/kubeconform") -func getSchemaLocations(ctx context.Context, ctr container.Container, tempRepoPath string) []string { +func getSchemaLocations(ctr container.Container) []string { cfg := ctr.Config locations := []string{ @@ -29,28 +28,13 @@ func getSchemaLocations(ctx context.Context, ctr container.Container, tempRepoPa } // schemas configured globally - for _, schemasLocation := range cfg.SchemasLocations { - if strings.HasPrefix(schemasLocation, "http://") || strings.HasPrefix(schemasLocation, "https://") { - locations = append(locations, schemasLocation) - } else { - if !filepath.IsAbs(schemasLocation) { - schemasLocation = filepath.Join(tempRepoPath, schemasLocation) - } - - if _, err := os.Stat(schemasLocation); err != nil { - log.Warn(). - Err(err). - Str("path", schemasLocation). - Msg("schemas location is invalid, skipping") - } else { - locations = append(locations, schemasLocation) - } - } - } + locations = append(locations, cfg.SchemasLocations...) for index := range locations { location := locations[index] + oldLocation := location if location == "default" || strings.Contains(location, "{{") { + log.Debug().Str("location", location).Msg("location requires no processing to be valid") continue } @@ -60,12 +44,14 @@ func getSchemaLocations(ctx context.Context, ctr container.Container, tempRepoPa location += "{{ .NormalizedKubernetesVersion }}/{{ .ResourceKind }}{{ .KindSuffix }}.json" locations[index] = location + + log.Debug().Str("old", oldLocation).Str("new", location).Msg("processed schema location") } return locations } -func argoCdAppValidate(ctx context.Context, ctr container.Container, appName, targetKubernetesVersion, tempRepoPath string, appManifests []string) (msg.Result, error) { +func argoCdAppValidate(ctx context.Context, ctr container.Container, appName, targetKubernetesVersion string, appManifests []string) (msg.Result, error) { _, span := tracer.Start(ctx, "ArgoCdAppValidate") defer span.End() @@ -92,7 +78,7 @@ func argoCdAppValidate(ctx context.Context, ctr container.Container, appName, ta var ( outputString []string - schemaLocations = getSchemaLocations(ctx, ctr, tempRepoPath) + schemaLocations = getSchemaLocations(ctr) ) log.Debug().Msgf("cache location: %s", vOpts.Cache) diff --git a/pkg/checks/kubeconform/validate_test.go b/pkg/checks/kubeconform/validate_test.go index bd502b01..b68324cc 100644 --- a/pkg/checks/kubeconform/validate_test.go +++ b/pkg/checks/kubeconform/validate_test.go @@ -1,7 +1,6 @@ package kubeconform import ( - "context" "fmt" "os" "strings" @@ -16,9 +15,8 @@ import ( ) func TestDefaultGetSchemaLocations(t *testing.T) { - ctx := context.TODO() ctr := container.Container{} - schemaLocations := getSchemaLocations(ctx, ctr, "/some/other/path") + schemaLocations := getSchemaLocations(ctr) // default schema location is "./schemas" assert.Len(t, schemaLocations, 1) @@ -26,7 +24,6 @@ func TestDefaultGetSchemaLocations(t *testing.T) { } func TestGetRemoteSchemaLocations(t *testing.T) { - ctx := context.TODO() ctr := container.Container{} if os.Getenv("CI") == "" { @@ -39,7 +36,7 @@ func TestGetRemoteSchemaLocations(t *testing.T) { // t.Setenv("KUBECHECKS_SCHEMAS_LOCATION", fixture.URL) // doesn't work because viper needs to initialize from root, which doesn't happen viper.Set("schemas-location", []string{fixture.URL}) - schemaLocations := getSchemaLocations(ctx, ctr, "/some/other/path") + schemaLocations := getSchemaLocations(ctr) hasTmpDirPrefix := strings.HasPrefix(schemaLocations[0], "/tmp/schemas") assert.Equal(t, hasTmpDirPrefix, true, "invalid schemas location. Schema location should have prefix /tmp/schemas but has %s", schemaLocations[0]) } diff --git a/pkg/config/config.go b/pkg/config/config.go index cc09f634..552bdf7d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -17,15 +17,18 @@ import ( type ServerConfig struct { // argocd - ArgoCDServerAddr string `mapstructure:"argocd-api-server-addr"` - ArgoCDToken string `mapstructure:"argocd-api-token"` - ArgoCDPathPrefix string `mapstructure:"argocd-api-path-prefix"` - ArgoCDInsecure bool `mapstructure:"argocd-api-insecure"` - ArgoCDNamespace string `mapstructure:"argocd-api-namespace"` - ArgoCDPlainText bool `mapstructure:"argocd-api-plaintext"` - KubernetesConfig string `mapstructure:"kubernetes-config"` - KubernetesType string `mapstructure:"kubernetes-type"` - KubernetesClusterID string `mapstructure:"kubernetes-clusterid"` + ArgoCDServerAddr string `mapstructure:"argocd-api-server-addr"` + ArgoCDToken string `mapstructure:"argocd-api-token"` + ArgoCDPathPrefix string `mapstructure:"argocd-api-path-prefix"` + ArgoCDInsecure bool `mapstructure:"argocd-api-insecure"` + ArgoCDNamespace string `mapstructure:"argocd-api-namespace"` + ArgoCDPlainText bool `mapstructure:"argocd-api-plaintext"` + ArgoCDRepositoryEndpoint string `mapstructure:"argocd-repository-endpoint"` + ArgoCDRepositoryInsecure bool `mapstructure:"argocd-repository-insecure"` + ArgoCDSendFullRepository bool `mapstructure:"argocd-send-full-repository"` + KubernetesConfig string `mapstructure:"kubernetes-config"` + KubernetesType string `mapstructure:"kubernetes-type"` + KubernetesClusterID string `mapstructure:"kubernetes-clusterid"` // otel EnableOtel bool `mapstructure:"otel-enabled"` diff --git a/pkg/container/main.go b/pkg/container/main.go index a330af3f..4a281a43 100644 --- a/pkg/container/main.go +++ b/pkg/container/main.go @@ -2,13 +2,14 @@ package container import ( "context" - "io/fs" + "fmt" - "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" client "github.com/zapier/kubechecks/pkg/kubernetes" + "github.com/zapier/kubechecks/pkg/vcs/github_client" + "github.com/zapier/kubechecks/pkg/vcs/gitlab_client" - "github.com/zapier/kubechecks/pkg" - "github.com/zapier/kubechecks/pkg/app_watcher" "github.com/zapier/kubechecks/pkg/appdir" "github.com/zapier/kubechecks/pkg/argo_client" "github.com/zapier/kubechecks/pkg/config" @@ -17,35 +18,107 @@ import ( ) type Container struct { - ApplicationWatcher *app_watcher.ApplicationWatcher - ApplicationSetWatcher *app_watcher.ApplicationSetWatcher - ArgoClient *argo_client.ArgoClient + ArgoClient *argo_client.ArgoClient Config config.ServerConfig RepoManager *git.RepoManager VcsClient vcs.Client - VcsToArgoMap VcsToArgoMap + VcsToArgoMap appdir.VcsToArgoMap KubeClientSet client.Interface } -type VcsToArgoMap interface { - AddApp(*v1alpha1.Application) - AddAppSet(*v1alpha1.ApplicationSet) - UpdateApp(old, new *v1alpha1.Application) - UpdateAppSet(old *v1alpha1.ApplicationSet, new *v1alpha1.ApplicationSet) - DeleteApp(*v1alpha1.Application) - DeleteAppSet(app *v1alpha1.ApplicationSet) - GetVcsRepos() []string - GetAppsInRepo(string) *appdir.AppDirectory - GetAppSetsInRepo(string) *appdir.AppSetDirectory - GetMap() map[pkg.RepoURL]*appdir.AppDirectory - WalkKustomizeApps(cloneURL string, fs fs.FS) *appdir.AppDirectory -} - type ReposCache interface { Clone(ctx context.Context, repoUrl string) (string, error) CloneWithBranch(ctx context.Context, repoUrl, targetBranch string) (string, error) } + +func New(ctx context.Context, cfg config.ServerConfig) (Container, error) { + var err error + + var ctr = Container{ + Config: cfg, + RepoManager: git.NewRepoManager(cfg), + } + + // create vcs client + switch cfg.VcsType { + case "gitlab": + ctr.VcsClient, err = gitlab_client.CreateGitlabClient(cfg) + case "github": + ctr.VcsClient, err = github_client.CreateGithubClient(cfg) + default: + err = fmt.Errorf("unknown vcs-type: %q", cfg.VcsType) + } + if err != nil { + return ctr, errors.Wrap(err, "failed to create vcs client") + } + var kubeClient client.Interface + + switch cfg.KubernetesType { + // TODO: expand with other cluster types + case client.ClusterTypeLOCAL: + kubeClient, err = client.New(&client.NewClientInput{ + KubernetesConfigPath: cfg.KubernetesConfig, + ClusterType: cfg.KubernetesType, + }) + if err != nil { + return ctr, errors.Wrap(err, "failed to create kube client") + } + case client.ClusterTypeEKS: + kubeClient, err = client.New(&client.NewClientInput{ + KubernetesConfigPath: cfg.KubernetesConfig, + ClusterType: cfg.KubernetesType, + }, + client.EKSClientOption(ctx, cfg.KubernetesClusterID), + ) + if err != nil { + return ctr, errors.Wrap(err, "failed to create kube client") + } + } + ctr.KubeClientSet = kubeClient + // create argo client + if ctr.ArgoClient, err = argo_client.NewArgoClient(cfg, kubeClient); err != nil { + return ctr, errors.Wrap(err, "failed to create argo client") + } + + // create vcs to argo map + vcsToArgoMap := appdir.NewVcsToArgoMap(ctr.VcsClient.Username()) + ctr.VcsToArgoMap = vcsToArgoMap + + if cfg.MonitorAllApplications { + if err = buildAppsMap(ctx, ctr.ArgoClient, ctr.VcsToArgoMap); err != nil { + log.Fatal().Err(err).Msg("failed to build apps map") + } + + if err = buildAppSetsMap(ctx, ctr.ArgoClient, ctr.VcsToArgoMap); err != nil { + log.Fatal().Err(err).Msg("failed to build appsets map") + } + } + + return ctr, nil +} + +func buildAppsMap(ctx context.Context, argoClient *argo_client.ArgoClient, result appdir.VcsToArgoMap) error { + apps, err := argoClient.GetApplications(ctx) + if err != nil { + return errors.Wrap(err, "failed to list applications") + } + for _, app := range apps.Items { + result.AddApp(&app) + } + return nil +} + +func buildAppSetsMap(ctx context.Context, argoClient *argo_client.ArgoClient, result appdir.VcsToArgoMap) error { + appSets, err := argoClient.GetApplicationSets(ctx) + if err != nil { + return errors.Wrap(err, "failed to list application sets") + } + for _, appSet := range appSets.Items { + result.AddAppSet(&appSet) + } + return nil +} diff --git a/pkg/events/check.go b/pkg/events/check.go index a6b96ebd..94ea4e81 100644 --- a/pkg/events/check.go +++ b/pkg/events/check.go @@ -42,7 +42,7 @@ type CheckEvent struct { repoManager repoManager processors []checks.ProcessorEntry repoLock sync.Mutex - clonedRepos map[string]*git.Repo + clonedRepos map[repoKey]*git.Repo addedAppsSet map[string]v1alpha1.Application addedAppsSetLock sync.Mutex @@ -82,7 +82,7 @@ func NewCheckEvent(pullRequest vcs.PullRequest, ctr container.Container, repoMan addedAppsSet: make(map[string]v1alpha1.Application), appChannel: make(chan *v1alpha1.Application, ctr.Config.MaxQueueSize), ctr: ctr, - clonedRepos: make(map[string]*git.Repo), + clonedRepos: make(map[repoKey]*git.Repo), processors: processors, pullRequest: pullRequest, repoManager: repoManager, @@ -160,15 +160,14 @@ func canonicalize(cloneURL string) (pkg.RepoURL, error) { return parsed, nil } -func generateRepoKey(cloneURL pkg.RepoURL, branchName string) string { - return fmt.Sprintf("%s|||%s", cloneURL.CloneURL(""), branchName) -} +type repoKey string -type hasUsername interface { - Username() string +func generateRepoKey(cloneURL pkg.RepoURL, branchName string) repoKey { + key := fmt.Sprintf("%s|||%s", cloneURL.CloneURL(""), branchName) + return repoKey(key) } -func (ce *CheckEvent) getRepo(ctx context.Context, vcsClient hasUsername, cloneURL, branchName string) (*git.Repo, error) { +func (ce *CheckEvent) getRepo(ctx context.Context, cloneURL, branchName string) (*git.Repo, error) { var ( err error repo *git.Repo @@ -181,7 +180,7 @@ func (ce *CheckEvent) getRepo(ctx context.Context, vcsClient hasUsername, cloneU if err != nil { return nil, errors.Wrap(err, "failed to parse clone url") } - cloneURL = parsed.CloneURL(vcsClient.Username()) + cloneURL = parsed.CloneURL(ce.ctr.VcsClient.Username()) branchName = strings.TrimSpace(branchName) if branchName == "" { @@ -227,6 +226,22 @@ func (ce *CheckEvent) getRepo(ctx context.Context, vcsClient hasUsername, cloneU return repo, nil } +func (ce *CheckEvent) mergeIntoTarget(ctx context.Context, repo *git.Repo, branch string) error { + if err := repo.MergeIntoTarget(ctx, fmt.Sprintf("origin/%s", branch)); err != nil { + return errors.Wrap(err, "failed to merge into target") + } + + parsed, err := canonicalize(repo.CloneURL) + if err != nil { + return errors.Wrap(err, "failed to canonicalize url") + } + + reposKey := generateRepoKey(parsed, branch) + ce.clonedRepos[reposKey] = repo + + return nil +} + func (ce *CheckEvent) Process(ctx context.Context) error { start := time.Now() @@ -234,13 +249,13 @@ func (ce *CheckEvent) Process(ctx context.Context) error { defer span.End() // Clone the repo's BaseRef (main, etc.) locally into the temp dir we just made - repo, err := ce.getRepo(ctx, ce.ctr.VcsClient, ce.pullRequest.CloneURL, ce.pullRequest.BaseRef) + repo, err := ce.getRepo(ctx, ce.pullRequest.CloneURL, ce.pullRequest.BaseRef) if err != nil { return errors.Wrap(err, "failed to clone repo") } // Merge the most recent changes into the branch we just cloned - if err = repo.MergeIntoTarget(ctx, ce.pullRequest.SHA); err != nil { + if err = ce.mergeIntoTarget(ctx, repo, ce.pullRequest.HeadRef); err != nil { return errors.Wrap(err, "failed to merge into target") } @@ -275,11 +290,12 @@ func (ce *CheckEvent) Process(ctx context.Context) error { for num := 0; num <= ce.ctr.Config.MaxConcurrenctChecks; num++ { w := worker{ - appChannel: ce.appChannel, - ctr: ce.ctr, - logger: ce.logger.With().Int("workerID", num).Logger(), - processors: ce.processors, - vcsNote: ce.vcsNote, + appChannel: ce.appChannel, + ctr: ce.ctr, + logger: ce.logger.With().Int("workerID", num).Logger(), + pullRequest: ce.pullRequest, + processors: ce.processors, + vcsNote: ce.vcsNote, done: ce.wg.Done, getRepo: ce.getRepo, diff --git a/pkg/events/check_test.go b/pkg/events/check_test.go index ff7505b4..0e6ffeeb 100644 --- a/pkg/events/check_test.go +++ b/pkg/events/check_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" affectedappsmocks "github.com/zapier/kubechecks/mocks/affected_apps/mocks" generatorsmocks "github.com/zapier/kubechecks/mocks/generator/mocks" + vcsmocks "github.com/zapier/kubechecks/mocks/vcs/mocks" "github.com/zapier/kubechecks/pkg/affected_apps" "github.com/zapier/kubechecks/pkg/checks" "github.com/zapier/kubechecks/pkg/config" @@ -65,12 +66,6 @@ func TestCleanupGetManifestsError(t *testing.T) { } } -type mockVcsClient struct{} - -func (m mockVcsClient) Username() string { - return "username" -} - func TestCheckEventGetRepo(t *testing.T) { cloneURL := "https://github.com/zapier/kubechecks.git" canonical, err := canonicalize(cloneURL) @@ -80,12 +75,16 @@ func TestCheckEventGetRepo(t *testing.T) { ctx := context.TODO() t.Run("empty branch name", func(t *testing.T) { + vcsClient := new(vcsmocks.MockClient) + vcsClient.EXPECT().Username().Return("username") + ce := CheckEvent{ - clonedRepos: make(map[string]*git.Repo), + clonedRepos: make(map[repoKey]*git.Repo), repoManager: git.NewRepoManager(cfg), + ctr: container.Container{VcsClient: vcsClient}, } - repo, err := ce.getRepo(ctx, mockVcsClient{}, cloneURL, "") + repo, err := ce.getRepo(ctx, cloneURL, "") require.NoError(t, err) assert.Equal(t, "main", repo.BranchName) assert.Len(t, ce.clonedRepos, 2) @@ -94,12 +93,16 @@ func TestCheckEventGetRepo(t *testing.T) { }) t.Run("branch is HEAD", func(t *testing.T) { + vcsClient := new(vcsmocks.MockClient) + vcsClient.EXPECT().Username().Return("username") + ce := CheckEvent{ - clonedRepos: make(map[string]*git.Repo), + clonedRepos: make(map[repoKey]*git.Repo), repoManager: git.NewRepoManager(cfg), + ctr: container.Container{VcsClient: vcsClient}, } - repo, err := ce.getRepo(ctx, mockVcsClient{}, cloneURL, "HEAD") + repo, err := ce.getRepo(ctx, cloneURL, "HEAD") require.NoError(t, err) assert.Equal(t, "main", repo.BranchName) assert.Len(t, ce.clonedRepos, 2) @@ -108,12 +111,16 @@ func TestCheckEventGetRepo(t *testing.T) { }) t.Run("branch is the same as HEAD", func(t *testing.T) { + vcsClient := new(vcsmocks.MockClient) + vcsClient.EXPECT().Username().Return("username") + ce := CheckEvent{ - clonedRepos: make(map[string]*git.Repo), + clonedRepos: make(map[repoKey]*git.Repo), repoManager: git.NewRepoManager(cfg), + ctr: container.Container{VcsClient: vcsClient}, } - repo, err := ce.getRepo(ctx, mockVcsClient{}, cloneURL, "main") + repo, err := ce.getRepo(ctx, cloneURL, "main") require.NoError(t, err) assert.Equal(t, "main", repo.BranchName) assert.Len(t, ce.clonedRepos, 2) @@ -122,12 +129,16 @@ func TestCheckEventGetRepo(t *testing.T) { }) t.Run("branch is not the same as HEAD", func(t *testing.T) { + vcsClient := new(vcsmocks.MockClient) + vcsClient.EXPECT().Username().Return("username") + ce := CheckEvent{ - clonedRepos: make(map[string]*git.Repo), + clonedRepos: make(map[repoKey]*git.Repo), repoManager: git.NewRepoManager(cfg), + ctr: container.Container{VcsClient: vcsClient}, } - repo, err := ce.getRepo(ctx, mockVcsClient{}, cloneURL, "gh-pages") + repo, err := ce.getRepo(ctx, cloneURL, "gh-pages") require.NoError(t, err) assert.Equal(t, "gh-pages", repo.BranchName) assert.Len(t, ce.clonedRepos, 1) @@ -145,7 +156,7 @@ func TestCheckEvent_GenerateListOfAffectedApps(t *testing.T) { ctr container.Container repoManager repoManager processors []checks.ProcessorEntry - clonedRepos map[string]*git.Repo + clonedRepos map[repoKey]*git.Repo addedAppsSet map[string]v1alpha1.Application appsSent int32 appChannel chan *v1alpha1.Application @@ -308,6 +319,7 @@ func MockGenerator(methodName string, returns []interface{}) generator.AppsGener return mockClient } + func MockInitMatcherFn() MatcherFn { return func(ce *CheckEvent, repo *git.Repo) error { return nil diff --git a/pkg/events/runner.go b/pkg/events/runner.go index 29c25242..f845d88f 100644 --- a/pkg/events/runner.go +++ b/pkg/events/runner.go @@ -11,7 +11,6 @@ import ( "github.com/zapier/kubechecks/pkg" "github.com/zapier/kubechecks/pkg/checks" "github.com/zapier/kubechecks/pkg/container" - "github.com/zapier/kubechecks/pkg/git" "github.com/zapier/kubechecks/pkg/msg" "github.com/zapier/kubechecks/telemetry" ) @@ -23,9 +22,13 @@ type Runner struct { } func newRunner( - ctr container.Container, app v1alpha1.Application, repo *git.Repo, - appName, k8sVersion string, jsonManifests, yamlManifests []string, - logger zerolog.Logger, note *msg.Message, queueApp, removeApp func(application v1alpha1.Application), + ctr container.Container, + app v1alpha1.Application, + appName, k8sVersion string, + jsonManifests, yamlManifests []string, + logger zerolog.Logger, + note *msg.Message, + queueApp, removeApp func(application v1alpha1.Application), ) *Runner { return &Runner{ Request: checks.Request{ @@ -38,7 +41,6 @@ func newRunner( Note: note, QueueApp: queueApp, RemoveApp: removeApp, - Repo: repo, YamlManifests: yamlManifests, }, } diff --git a/pkg/events/worker.go b/pkg/events/worker.go index 58f0e47b..7840ce9f 100644 --- a/pkg/events/worker.go +++ b/pkg/events/worker.go @@ -3,15 +3,18 @@ package events import ( "context" "fmt" + "runtime/debug" "sync/atomic" + "github.com/ghodss/yaml" + "github.com/rs/zerolog/log" + "github.com/zapier/kubechecks/pkg/vcs" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/rs/zerolog" "github.com/zapier/kubechecks/pkg" - "github.com/zapier/kubechecks/pkg/argo_client" "github.com/zapier/kubechecks/pkg/checks" "github.com/zapier/kubechecks/pkg/container" "github.com/zapier/kubechecks/pkg/git" @@ -20,14 +23,15 @@ import ( ) type worker struct { - appChannel chan *v1alpha1.Application - ctr container.Container - logger zerolog.Logger - processors []checks.ProcessorEntry - vcsNote *msg.Message + appChannel chan *v1alpha1.Application + ctr container.Container + logger zerolog.Logger + processors []checks.ProcessorEntry + pullRequest vcs.PullRequest + vcsNote *msg.Message done func() - getRepo func(ctx context.Context, vcsClient hasUsername, cloneURL, branchName string) (*git.Repo, error) + getRepo func(ctx context.Context, cloneURL, branchName string) (*git.Repo, error) queueApp, removeApp func(application v1alpha1.Application) } @@ -54,87 +58,69 @@ func (w *worker) processApp(ctx context.Context, app v1alpha1.Application) { var ( err error - appName = app.Name - appSrc = app.Spec.Source - appPath = appSrc.Path - appRepoUrl = appSrc.RepoURL + appName = app.Name - logger = w.logger.With(). - Str("app_name", appName). - Str("app_path", appPath). - Logger() + rootLogger = w.logger.With(). + Str("app_name", appName). + Logger() ) ctx, span := tracer.Start(ctx, "processApp", trace.WithAttributes( attribute.String("app", appName), - attribute.String("dir", appPath), )) defer span.End() atomic.AddInt32(&inFlight, 1) defer atomic.AddInt32(&inFlight, -1) - logger.Info().Msg("Processing app") + rootLogger.Info().Msg("Processing app") // Build a new section for this app in the parent comment w.vcsNote.AddNewApp(ctx, appName) defer func() { - if err := recover(); err != nil { + if r := recover(); r != nil { desc := fmt.Sprintf("panic while checking %s", appName) - w.logger.Error().Str("app", appName).Msgf("panic while running check") + w.logger.Error().Any("error", r). + Str("app", appName).Msgf("panic while running check") + println(string(debug.Stack())) - telemetry.SetError(span, fmt.Errorf("%v", err), "panic while running check") + telemetry.SetError(span, fmt.Errorf("%v", r), "panic while running check") result := msg.Result{ State: pkg.StatePanic, Summary: desc, - Details: fmt.Sprintf(errorCommentFormat, desc, err), + Details: fmt.Sprintf(errorCommentFormat, desc, r), } w.vcsNote.AddToAppMessage(ctx, appName, result) } }() - repo, err := w.getRepo(ctx, w.ctr.VcsClient, appRepoUrl, appSrc.TargetRevision) + rootLogger.Debug().Msg("Getting manifests") + jsonManifests, err := w.ctr.ArgoClient.GetManifests(ctx, appName, app, w.pullRequest, w.getRepo) if err != nil { - logger.Error().Err(err).Msg("Unable to clone repository") - w.vcsNote.AddToAppMessage(ctx, appName, msg.Result{ - State: pkg.StateError, - Summary: "failed to clone repo", - Details: fmt.Sprintf("Clone URL: `%s`\nTarget Revision: `%s`\n```\n%s\n```", appRepoUrl, appSrc.TargetRevision, err.Error()), - }) - return - } - repoPath := repo.Directory - - logger.Debug().Str("repo_path", repoPath).Msg("Getting manifests") - jsonManifests, err := w.ctr.ArgoClient.GetManifestsLocal(ctx, appName, repoPath, appPath, app) - if err != nil { - logger.Error().Err(err).Msg("Unable to get manifests") + rootLogger.Error().Err(err).Msg("Unable to get manifests") w.vcsNote.AddToAppMessage(ctx, appName, msg.Result{ State: pkg.StateError, Summary: "Unable to get manifests", - Details: fmt.Sprintf("```\n%s\n```", cleanupGetManifestsError(err, repo.Directory)), + Details: fmt.Sprintf("```\n%s\n```", err), }) return } // Argo diff logic wants unformatted manifests but everything else wants them as YAML, so we prepare both - yamlManifests := argo_client.ConvertJsonToYamlManifests(jsonManifests) - logger.Trace().Msgf("Manifests:\n%+v\n", yamlManifests) + yamlManifests := convertJsonToYamlManifests(jsonManifests) + rootLogger.Trace().Msgf("Manifests:\n%+v\n", yamlManifests) k8sVersion, err := w.ctr.ArgoClient.GetKubernetesVersionByApplication(ctx, app) if err != nil { - logger.Error().Err(err).Msg("Error retrieving the Kubernetes version") + rootLogger.Error().Err(err).Msg("Error retrieving the Kubernetes version") k8sVersion = w.ctr.Config.FallbackK8sVersion } else { k8sVersion = fmt.Sprintf("%s.0", k8sVersion) - logger.Info().Msgf("Kubernetes version: %s", k8sVersion) + rootLogger.Info().Msgf("Kubernetes version: %s", k8sVersion) } - runner := newRunner( - w.ctr, app, repo, appName, k8sVersion, jsonManifests, yamlManifests, logger, w.vcsNote, - w.queueApp, w.removeApp, - ) + runner := newRunner(w.ctr, app, appName, k8sVersion, jsonManifests, yamlManifests, rootLogger, w.vcsNote, w.queueApp, w.removeApp) for _, processor := range w.processors { runner.Run(ctx, processor.Name, processor.Processor, processor.WorstState) @@ -142,3 +128,16 @@ func (w *worker) processApp(ctx context.Context, app v1alpha1.Application) { runner.Wait() } + +func convertJsonToYamlManifests(jsonManifests []string) []string { + var manifests []string + for _, manifest := range jsonManifests { + ret, err := yaml.JSONToYAML([]byte(manifest)) + if err != nil { + log.Warn().Err(err).Msg("Failed to format manifest") + continue + } + manifests = append(manifests, fmt.Sprintf("---\n%s", string(ret))) + } + return manifests +} diff --git a/pkg/events/worker_test.go b/pkg/events/worker_test.go new file mode 100644 index 00000000..9cea817a --- /dev/null +++ b/pkg/events/worker_test.go @@ -0,0 +1,34 @@ +package events + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConvertJsonToYamlManifests(t *testing.T) { + testcases := map[string]struct { + input, expected []string + }{ + "empty": { + input: []string{}, + expected: nil, + }, + "easy json": { + input: []string{ + `{"hello": "world"}`, + }, + expected: []string{ + `--- +hello: world +`, + }, + }, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + actual := convertJsonToYamlManifests(tc.input) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/pkg/git/repo.go b/pkg/git/repo.go index 05652154..ae9a40ca 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -81,6 +81,7 @@ func (r *Repo) Clone(ctx context.Context) error { } } + log.Info().Msg("repo has been cloned") return nil } @@ -107,22 +108,22 @@ func (r *Repo) GetRemoteHead() (string, error) { return branchName, nil } -func (r *Repo) MergeIntoTarget(ctx context.Context, sha string) error { +func (r *Repo) MergeIntoTarget(ctx context.Context, ref string) error { // Merge the last commit into a tmp branch off of the target branch _, span := tracer.Start(ctx, "Repo - RepoMergeIntoTarget", trace.WithAttributes( attribute.String("branch_name", r.BranchName), attribute.String("clone_url", r.CloneURL), attribute.String("directory", r.Directory), - attribute.String("sha", sha), + attribute.String("sha", ref), )) defer span.End() - cmd := r.execCommand("git", "merge", sha) + cmd := r.execCommand("git", "merge", ref) out, err := cmd.CombinedOutput() if err != nil { telemetry.SetError(span, err, "merge commit into branch") - log.Error().Err(err).Msgf("unable to merge %s, %s", sha, out) + log.Error().Err(err).Msgf("unable to merge %s, %s", ref, out) return err } diff --git a/pkg/repoUrl.go b/pkg/repoUrl.go index cfc5a677..6ee8c188 100644 --- a/pkg/repoUrl.go +++ b/pkg/repoUrl.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/chainguard-dev/git-urls" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" ) type RepoURL struct { @@ -41,3 +43,28 @@ func NormalizeRepoUrl(s string) (RepoURL, url.Values, error) { Path: r.Path, }, r.Query(), nil } + +func Canonicalize(cloneURL string) (RepoURL, error) { + parsed, _, err := NormalizeRepoUrl(cloneURL) + if err != nil { + return RepoURL{}, errors.Wrap(err, "failed to parse clone url") + } + + return parsed, nil +} + +func AreSameRepos(url1, url2 string) bool { + repo1, err := Canonicalize(url1) + if err != nil { + log.Warn().Msgf("failed to canonicalize %q", url1) + return false + } + + repo2, err := Canonicalize(url2) + if err != nil { + log.Warn().Msgf("failed to canonicalize %q", url2) + return false + } + + return repo1 == repo2 +} diff --git a/pkg/repoUrl_test.go b/pkg/repoUrl_test.go index a4bc786e..9ac46b6c 100644 --- a/pkg/repoUrl_test.go +++ b/pkg/repoUrl_test.go @@ -70,3 +70,23 @@ func TestNormalizeStrings(t *testing.T) { }) } } + +func TestAreSameRepos(t *testing.T) { + testcases := map[string]struct { + input1, input2 string + expected bool + }{ + "empty": {"", "", true}, + "empty1": {"", "blah", false}, + "empty2": {"blah", "", false}, + "git-to-git": {"git@github.com:zapier/kubechecks.git", "git@github.com:zapier/kubechecks.git", true}, + "no-git-suffix-to-git": {"git@github.com:zapier/kubechecks", "git@github.com:zapier/kubechecks.git", true}, + "https-to-git": {"https://github.com/zapier/kubechecks", "git@github.com:zapier/kubechecks.git", true}, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + actual := AreSameRepos(tc.input1, tc.input2) + assert.Equal(t, tc.expected, actual) + }) + } +}