diff --git a/controllers/component_dependency_update_controller.go b/controllers/component_dependency_update_controller.go index b0b6c8e1..fa89856a 100644 --- a/controllers/component_dependency_update_controller.go +++ b/controllers/component_dependency_update_controller.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "encoding/base64" "encoding/json" "fmt" "regexp" @@ -96,6 +97,20 @@ type BuildResult struct { Component *applicationapi.Component } +type RepositoryCredentials struct { + SecretName string + RepoName string + UserName string + Password string +} + +type RepositoryConfigAuth struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Email string `json:"email,omitempty"` + Auth string `json:"auth,omitempty"` +} + // SetupController creates a new Integration reconciler and adds it to the Manager. func (r *ComponentDependencyUpdateReconciler) SetupWithManager(manager ctrl.Manager) error { return setupControllerWithManager(manager, r) @@ -372,16 +387,31 @@ func (r *ComponentDependencyUpdateReconciler) handleCompletedBuild(ctx context.C componentsToUpdate = append(componentsToUpdate, comp) } } - var nudgeErr error + updatedOutputImage := updatedComponent.Spec.ContainerImage + imageRepositoryHost := strings.Split(updatedOutputImage, "/")[0] + + imageRepositoryUsername, imageRepositoryPassword, err := r.getImageRepositoryCredentials(ctx, pipelineRun.Namespace, updatedOutputImage) + if err != nil { + // when we can't find credential for repository, remove pipeline finalizer and return error + _, errRemoveFinalizer := r.removePipelineFinalizer(ctx, pipelineRun, patch) + if errRemoveFinalizer != nil { + return ctrl.Result{}, errRemoveFinalizer + } + + return ctrl.Result{}, err + } + + var nudgeErr error var targets []updateTarget - newTargets := r.ComponentDependenciesUpdater.GetUpdateTargetsGithubApp(ctx, componentsToUpdate) + + newTargets := r.ComponentDependenciesUpdater.GetUpdateTargetsGithubApp(ctx, componentsToUpdate, imageRepositoryHost, imageRepositoryUsername, imageRepositoryPassword) log.Info("found new targets for GitHub app", "targets", len(newTargets)) if len(newTargets) > 0 { targets = append(targets, newTargets...) } - newTargets = r.ComponentDependenciesUpdater.GetUpdateTargetsBasicAuth(ctx, componentsToUpdate) + newTargets = r.ComponentDependenciesUpdater.GetUpdateTargetsBasicAuth(ctx, componentsToUpdate, imageRepositoryHost, imageRepositoryUsername, imageRepositoryPassword) log.Info("found new targets for basic auth", "targets", len(newTargets)) if len(newTargets) > 0 { targets = append(targets, newTargets...) @@ -509,6 +539,145 @@ func (r *ComponentDependencyUpdateReconciler) removePipelineFinalizer(ctx contex return ctrl.Result{}, nil } +func (r *ComponentDependencyUpdateReconciler) getImageRepositoryCredentials(ctx context.Context, namespace, updatedOutputImage string) (string, string, error) { + log := ctrllog.FromContext(ctx) + + // get service account and gather linked secrets + pipelinesServiceAccount := &corev1.ServiceAccount{} + err := r.Client.Get(ctx, types.NamespacedName{Name: buildPipelineServiceAccountName, Namespace: namespace}, pipelinesServiceAccount) + if err != nil { + log.Error(err, fmt.Sprintf("Failed to read service account %s in namespace %s", buildPipelineServiceAccountName, namespace), l.Action, l.ActionView) + return "", "", err + } + + linkedSecretNames := []string{} + for _, secret := range pipelinesServiceAccount.Secrets { + linkedSecretNames = append(linkedSecretNames, secret.Name) + } + + if len(linkedSecretNames) == 0 { + log.Error(fmt.Errorf("No secrets linked to service account %s in namespace %s", buildPipelineServiceAccountName, namespace), "no linked secrets") + return "", "", err + } + log.Info("secrets linked to service account", "count", len(linkedSecretNames)) + + // get all docker config json secrets + allImageRepoSecrets := &corev1.SecretList{} + opts := client.ListOption(&client.MatchingFields{"type": string(corev1.SecretTypeDockerConfigJson)}) + + if err := r.Client.List(ctx, allImageRepoSecrets, client.InNamespace(namespace), opts); err != nil { + return "", "", fmt.Errorf("failed to list secrets of type %s in %s namespace: %w", corev1.SecretTypeDockerConfigJson, namespace, err) + } + log.Info("found docker config secrets secrets", "count", len(allImageRepoSecrets.Items)) + + type DockerConfigJson struct { + ConfigAuths map[string]RepositoryConfigAuth `json:"auths"` + } + + filteredSecretsData := []RepositoryCredentials{} + for _, secret := range allImageRepoSecrets.Items { + isSecretLinked := false + + for _, linkedSecret := range linkedSecretNames { + if secret.Name == linkedSecret { + isSecretLinked = true + break + } + } + if !isSecretLinked { + continue + } + + dockerConfigObject := &DockerConfigJson{} + if err = json.Unmarshal(secret.Data[corev1.DockerConfigJsonKey], dockerConfigObject); err != nil { + log.Error(err, fmt.Sprintf("unable to parse docker json config in the secret %s", secret.Name)) + continue + } + + for repoName, repoAuth := range dockerConfigObject.ConfigAuths { + if repoAuth.Username != "" && repoAuth.Password != "" { + filteredSecretsData = append(filteredSecretsData, RepositoryCredentials{SecretName: secret.Name, RepoName: repoName, UserName: repoAuth.Username, Password: repoAuth.Password}) + } else { + if repoAuth.Auth == "" { + log.Error(fmt.Errorf("password and username and auth are empty in auth config for repository %s", repoName), "no valid auth") + continue + } else { + decodedAuth, err := base64.StdEncoding.DecodeString(repoAuth.Auth) + if err != nil { + log.Error(err, fmt.Sprintf("unable to decode docker config json auth for repository %s in the secret %s", repoName, secret.Name)) + continue + } + authParts := strings.Split(string(decodedAuth), ":") + filteredSecretsData = append(filteredSecretsData, RepositoryCredentials{SecretName: secret.Name, RepoName: repoName, UserName: authParts[0], Password: authParts[1]}) + } + } + } + } + + imageRepositoryUsername, imageRepositoryPassword, err := r.GetCredentialsForImageRepository(ctx, updatedOutputImage, filteredSecretsData) + if err != nil { + log.Error(err, fmt.Sprintf("unable to find credential for repository %s", updatedOutputImage)) + return "", "", err + } + return imageRepositoryUsername, imageRepositoryPassword, nil +} + +// GetCredentialsForImageRepository This method returns credentials for image repository +func (r ComponentDependencyUpdateReconciler) GetCredentialsForImageRepository(ctx context.Context, outputImage string, imageRepoSecrets []RepositoryCredentials) (string, string, error) { + log := ctrllog.FromContext(ctx) + + repoPath := outputImage + if strings.Contains(outputImage, "@") { + repoPath = strings.Split(outputImage, "@")[0] + } else { + if strings.Contains(outputImage, ":") { + repoPath = strings.Split(outputImage, ":")[0] + } + } + repoPath = strings.TrimSuffix(repoPath, "/") + + username := "" + password := "" + // first check for credential which matches full repository path + for _, credential := range imageRepoSecrets { + credentialRepo := strings.TrimSuffix(credential.RepoName, "/") + if repoPath == credentialRepo { + log.Info("found full match of repository in auth", "repo", repoPath, "secretName", credential.SecretName) + username = credential.UserName + password = credential.Password + break + } + } + if username != "" && password != "" { + return username, password, nil + + } + + // check for partial match, get the most complete match + // if there is multiple secrets for registry and some partial match, upload sbom would fail anyway, because it chooses them randomly (until cosign fixes it) + repoParts := strings.Split(repoPath, "/") + for { + repoParts = repoParts[:len(repoParts)-1] + if len(repoParts) == 0 { + break + } + partialRepo := strings.Join(repoParts, "/") + + for _, credential := range imageRepoSecrets { + credentialRepo := strings.TrimSuffix(credential.RepoName, "/") + if partialRepo == credentialRepo { + log.Info("partial match found of repository in auth", "repo", partialRepo, "secretName", credential.SecretName) + username = credential.UserName + password = credential.Password + return username, password, nil + } + } + } + + log.Info("no credentials found for repo", "repo", repoPath) + return "", "", fmt.Errorf("No credentials found for repository %s ", repoPath) +} + func IsBuildPushPipelineRun(object client.Object) bool { if pipelineRun, ok := object.(*tektonapi.PipelineRun); ok { diff --git a/controllers/renovate_util.go b/controllers/renovate_util.go index 7bd18df1..3f0852e8 100644 --- a/controllers/renovate_util.go +++ b/controllers/renovate_util.go @@ -42,13 +42,16 @@ type renovateRepository struct { // UpdateTarget represents a target source code repository to be executed by Renovate with credentials and repositories type updateTarget struct { - ComponentName string - GitProvider string - Username string - GitAuthor string - Token string - Endpoint string - Repositories []renovateRepository + ComponentName string + GitProvider string + Username string + GitAuthor string + Token string + Endpoint string + Repositories []renovateRepository + ImageRepositoryHost string + ImageRepositoryUsername string + ImageRepositoryPassword string } type ComponentDependenciesUpdater struct { @@ -65,7 +68,7 @@ func NewComponentDependenciesUpdater(client client.Client, scheme *runtime.Schem } // GetUpdateTargetsBasicAuth This method returns targets for components based on basic auth -func (u ComponentDependenciesUpdater) GetUpdateTargetsBasicAuth(ctx context.Context, componentList []v1alpha1.Component) []updateTarget { +func (u ComponentDependenciesUpdater) GetUpdateTargetsBasicAuth(ctx context.Context, componentList []v1alpha1.Component, imageRepositoryHost, imageRepositoryUsername, imageRepositoryPassword string) []updateTarget { log := logger.FromContext(ctx) targetsToUpdate := []updateTarget{} @@ -125,13 +128,16 @@ func (u ComponentDependenciesUpdater) GetUpdateTargetsBasicAuth(ctx context.Cont } targetsToUpdate = append(targetsToUpdate, updateTarget{ - ComponentName: component.Name, - GitProvider: gitProvider, - Username: username, - GitAuthor: fmt.Sprintf("%s <123456+%s[bot]@users.noreply.%s>", username, username, scmComponent.RepositoryHost()), - Token: creds.Password, - Endpoint: git.BuildAPIEndpoint(gitProvider).APIEndpoint(scmComponent.RepositoryHost()), - Repositories: repositories, + ComponentName: component.Name, + GitProvider: gitProvider, + Username: username, + GitAuthor: fmt.Sprintf("%s <123456+%s[bot]@users.noreply.%s>", username, username, scmComponent.RepositoryHost()), + Token: creds.Password, + Endpoint: git.BuildAPIEndpoint(gitProvider).APIEndpoint(scmComponent.RepositoryHost()), + Repositories: repositories, + ImageRepositoryHost: imageRepositoryHost, + ImageRepositoryUsername: imageRepositoryUsername, + ImageRepositoryPassword: imageRepositoryPassword, }) log.Info("component to update for basic auth", "component", component.Name, "repositories", repositories) } @@ -140,7 +146,7 @@ func (u ComponentDependenciesUpdater) GetUpdateTargetsBasicAuth(ctx context.Cont } // GetUpdateTargetsGithubApp This method returns targets for components based on github app -func (u ComponentDependenciesUpdater) GetUpdateTargetsGithubApp(ctx context.Context, componentList []v1alpha1.Component) []updateTarget { +func (u ComponentDependenciesUpdater) GetUpdateTargetsGithubApp(ctx context.Context, componentList []v1alpha1.Component, imageRepositoryHost, imageRepositoryUsername, imageRepositoryPassword string) []updateTarget { log := logger.FromContext(ctx) // Check if GitHub Application is used, if not then skip pacSecret := corev1.Secret{} @@ -215,14 +221,18 @@ func (u ComponentDependenciesUpdater) GetUpdateTargetsGithubApp(ctx context.Cont log.Info("no repositories found in the installation", "ComponentName", component.Name, "ComponentNamespace", component.Namespace) continue } + targetsToUpdate = append(targetsToUpdate, updateTarget{ - ComponentName: component.Name, - GitProvider: gitProvider, - Username: fmt.Sprintf("%s[bot]", slug), - GitAuthor: fmt.Sprintf("%s <123456+%s[bot]@users.noreply.github.com>", slug, slug), - Token: githubAppInstallation.Token, - Endpoint: git.BuildAPIEndpoint("github").APIEndpoint("github.com"), - Repositories: repositories, + ComponentName: component.Name, + GitProvider: gitProvider, + Username: fmt.Sprintf("%s[bot]", slug), + GitAuthor: fmt.Sprintf("%s <123456+%s[bot]@users.noreply.github.com>", slug, slug), + Token: githubAppInstallation.Token, + Endpoint: git.BuildAPIEndpoint("github").APIEndpoint("github.com"), + Repositories: repositories, + ImageRepositoryHost: imageRepositoryHost, + ImageRepositoryUsername: imageRepositoryUsername, + ImageRepositoryPassword: imageRepositoryPassword, }) log.Info("component to update for installations", "component", component.Name, "repositories", repositories) } @@ -287,12 +297,20 @@ func generateRenovateConfigForNudge(target updateTarget, buildResult *BuildResul followTag: "{{.BuiltImageTag}}" } ], + hostRules: [ + { + "matchHost": "{{.ImageRepositoryHost}}", + "username": "{{.ImageRepositoryUsername}}", + "password": "{{.ImageRepositoryPassword}}" + } + ], forkProcessing: "enabled", extends: [":gitSignOff"], dependencyDashboard: false } {{end}} ` + data := struct { GitProvider string Username string @@ -305,6 +323,9 @@ func generateRenovateConfigForNudge(target updateTarget, buildResult *BuildResul Digest string DistributionRepositories []string FileMatches string + ImageRepositoryHost string + ImageRepositoryUsername string + ImageRepositoryPassword string }{ GitProvider: target.GitProvider, Username: target.Username, @@ -317,6 +338,9 @@ func generateRenovateConfigForNudge(target updateTarget, buildResult *BuildResul Digest: buildResult.Digest, DistributionRepositories: buildResult.DistributionRepositories, FileMatches: string(fileMatch), + ImageRepositoryHost: target.ImageRepositoryHost, + ImageRepositoryUsername: target.ImageRepositoryUsername, + ImageRepositoryPassword: target.ImageRepositoryPassword, } configTemplate, err := template.New("renovate").Parse(body)