Skip to content

Commit

Permalink
Nudging now works for private image repositories
Browse files Browse the repository at this point in the history
KFLUXBUGS-1609

Signed-off-by: Robert Cerven <[email protected]>
  • Loading branch information
rcerven committed Oct 8, 2024
1 parent 4431762 commit 9fdf660
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 26 deletions.
175 changes: 172 additions & 3 deletions controllers/component_dependency_update_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"regexp"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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...)
Expand Down Expand Up @@ -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 {

Expand Down
70 changes: 47 additions & 23 deletions controllers/renovate_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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{}

Expand Down Expand Up @@ -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)
}
Expand All @@ -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{}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)
Expand Down

0 comments on commit 9fdf660

Please sign in to comment.