diff --git a/controllers/component_build_controller_common.go b/controllers/component_build_controller_common.go deleted file mode 100644 index 93d93745..00000000 --- a/controllers/component_build_controller_common.go +++ /dev/null @@ -1,218 +0,0 @@ -/* -Copyright 2023 Red Hat, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - appstudiov1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" - "github.com/konflux-ci/build-service/pkg/boerrors" - . "github.com/konflux-ci/build-service/pkg/common" - l "github.com/konflux-ci/build-service/pkg/logs" - tektonapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - "gopkg.in/yaml.v2" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - ctrllog "sigs.k8s.io/controller-runtime/pkg/log" -) - -type BuildPipeline struct { - Name string `json:"name,omitempty"` - Bundle string `json:"bundle,omitempty"` -} - -type pipelineConfig struct { - DefaultPipelineName string `yaml:"default-pipeline-name"` - Pipelines []BuildPipeline `yaml:"pipelines"` -} - -// SetDefaultBuildPipelineComponentAnnotation sets default build pipeline to component pipeline annotation -func (r *ComponentBuildReconciler) SetDefaultBuildPipelineComponentAnnotation(ctx context.Context, component *appstudiov1alpha1.Component) error { - log := ctrllog.FromContext(ctx) - pipelinesConfigMap := &corev1.ConfigMap{} - - if err := r.Client.Get(ctx, types.NamespacedName{Name: buildPipelineConfigMapResourceName, Namespace: BuildServiceNamespaceName}, pipelinesConfigMap); err != nil { - if errors.IsNotFound(err) { - return boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotDefined, err) - } - return err - } - - buildPipelineData := &pipelineConfig{} - if err := yaml.Unmarshal([]byte(pipelinesConfigMap.Data[buildPipelineConfigName]), buildPipelineData); err != nil { - return boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotValid, err) - } - - pipelineAnnotation := fmt.Sprintf("{\"name\":\"%s\",\"bundle\":\"%s\"}", buildPipelineData.DefaultPipelineName, "latest") - if component.Annotations == nil { - component.Annotations = make(map[string]string) - } - component.Annotations[defaultBuildPipelineAnnotation] = pipelineAnnotation - - if err := r.Client.Update(ctx, component); err != nil { - log.Error(err, fmt.Sprintf("failed to update component with default pipeline annotation %s", defaultBuildPipelineAnnotation)) - return err - } - log.Info(fmt.Sprintf("updated component with default pipeline annotation %s", defaultBuildPipelineAnnotation)) - return nil -} - -// GetBuildPipelineFromComponentAnnotation parses pipeline annotation on component and returns build pipeline -func (r *ComponentBuildReconciler) GetBuildPipelineFromComponentAnnotation(ctx context.Context, component *appstudiov1alpha1.Component) (*tektonapi.PipelineRef, error) { - buildPipeline, err := readBuildPipelineAnnotation(component) - if err != nil { - return nil, err - } - if buildPipeline == nil { - err := fmt.Errorf("missing or empty pipeline annotation: %s, will add default one to the component", component.Annotations[defaultBuildPipelineAnnotation]) - return nil, boerrors.NewBuildOpError(boerrors.EMissingPipelineAnnotation, err) - } - if buildPipeline.Bundle == "" || buildPipeline.Name == "" { - err = fmt.Errorf("missing name or bundle in pipeline annotation: name=%s bundle=%s", buildPipeline.Name, buildPipeline.Bundle) - return nil, boerrors.NewBuildOpError(boerrors.EWrongPipelineAnnotation, err) - } - finalBundle := buildPipeline.Bundle - - if buildPipeline.Bundle == "latest" { - pipelinesConfigMap := &corev1.ConfigMap{} - if err := r.Client.Get(ctx, types.NamespacedName{Name: buildPipelineConfigMapResourceName, Namespace: BuildServiceNamespaceName}, pipelinesConfigMap); err != nil { - if errors.IsNotFound(err) { - return nil, boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotDefined, err) - } - return nil, err - } - - buildPipelineData := &pipelineConfig{} - if err := yaml.Unmarshal([]byte(pipelinesConfigMap.Data[buildPipelineConfigName]), buildPipelineData); err != nil { - return nil, boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotValid, err) - } - - for _, pipeline := range buildPipelineData.Pipelines { - if pipeline.Name == buildPipeline.Name { - finalBundle = pipeline.Bundle - break - } - } - - // requested pipeline was not found in configMap - if finalBundle == "latest" { - err = fmt.Errorf("invalid pipeline name in pipeline annotation: name=%s", buildPipeline.Name) - return nil, boerrors.NewBuildOpError(boerrors.EBuildPipelineInvalid, err) - } - } - - pipelineRef := &tektonapi.PipelineRef{ - ResolverRef: tektonapi.ResolverRef{ - Resolver: "bundles", - Params: []tektonapi.Param{ - {Name: "name", Value: *tektonapi.NewStructuredValues(buildPipeline.Name)}, - {Name: "bundle", Value: *tektonapi.NewStructuredValues(finalBundle)}, - {Name: "kind", Value: *tektonapi.NewStructuredValues("pipeline")}, - }, - }, - } - return pipelineRef, nil -} - -func readBuildPipelineAnnotation(component *appstudiov1alpha1.Component) (*BuildPipeline, error) { - if component.Annotations == nil { - return nil, nil - } - - requestedPipeline, requestedPipelineExists := component.Annotations[defaultBuildPipelineAnnotation] - if requestedPipelineExists && requestedPipeline != "" { - buildPipeline := &BuildPipeline{} - buildPipelineBytes := []byte(requestedPipeline) - - if err := json.Unmarshal(buildPipelineBytes, buildPipeline); err != nil { - return nil, boerrors.NewBuildOpError(boerrors.EFailedToParsePipelineAnnotation, err) - } - return buildPipeline, nil - } - return nil, nil -} - -func (r *ComponentBuildReconciler) ensurePipelineServiceAccount(ctx context.Context, namespace string) (*corev1.ServiceAccount, error) { - log := ctrllog.FromContext(ctx) - - pipelinesServiceAccount := &corev1.ServiceAccount{} - err := r.Client.Get(ctx, types.NamespacedName{Name: buildPipelineServiceAccountName, Namespace: namespace}, pipelinesServiceAccount) - if err != nil { - if !errors.IsNotFound(err) { - log.Error(err, fmt.Sprintf("Failed to read service account %s in namespace %s", buildPipelineServiceAccountName, namespace), l.Action, l.ActionView) - return nil, err - } - // Create service account for the build pipeline - buildPipelineSA := corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: buildPipelineServiceAccountName, - Namespace: namespace, - }, - } - if err := r.Client.Create(ctx, &buildPipelineSA); err != nil { - log.Error(err, fmt.Sprintf("Failed to create service account %s in namespace %s", buildPipelineServiceAccountName, namespace), l.Action, l.ActionAdd) - return nil, err - } - return r.ensurePipelineServiceAccount(ctx, namespace) - } - return pipelinesServiceAccount, nil -} - -func getContainerImageRepositoryForComponent(component *appstudiov1alpha1.Component) string { - if component.Spec.ContainerImage != "" { - return getContainerImageRepository(component.Spec.ContainerImage) - } - imageRepo, _, err := getComponentImageRepoAndSecretNameFromImageAnnotation(component) - if err == nil && imageRepo != "" { - return imageRepo - } - return "" -} - -// getContainerImageRepository removes tag or SHA has from container image reference -func getContainerImageRepository(image string) string { - if strings.Contains(image, "@") { - // registry.io/user/image@sha256:586ab...d59a - return strings.Split(image, "@")[0] - } - // registry.io/user/image:tag - return strings.Split(image, ":")[0] -} - -// getComponentImageRepoAndSecretNameFromImageAnnotation parses image.redhat.com/image annotation -// for image repository and secret name to access it. -// If image.redhat.com/image is not set, the procedure returns empty values. -func getComponentImageRepoAndSecretNameFromImageAnnotation(component *appstudiov1alpha1.Component) (string, string, error) { - type RepositoryInfo struct { - Image string `json:"image"` - Secret string `json:"secret"` - } - - var repoInfo RepositoryInfo - if imageRepoDataJson, exists := component.Annotations[ImageRepoAnnotationName]; exists { - if err := json.Unmarshal([]byte(imageRepoDataJson), &repoInfo); err != nil { - return "", "", boerrors.NewBuildOpError(boerrors.EFailedToParseImageAnnotation, err) - } - return repoInfo.Image, repoInfo.Secret, nil - } - return "", "", nil -} diff --git a/controllers/component_build_controller_pac.go b/controllers/component_build_controller_pac.go index 6094a717..ae17f3a9 100644 --- a/controllers/component_build_controller_pac.go +++ b/controllers/component_build_controller_pac.go @@ -18,41 +18,23 @@ package controllers import ( "context" - "crypto/rand" "crypto/tls" - "encoding/hex" "fmt" "net/http" "net/url" "os" - "path/filepath" - "regexp" "strconv" "strings" - "github.com/go-logr/logr" - "github.com/google/go-containerregistry/pkg/authn" appstudiov1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" - pacv1alpha1 "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1" routev1 "github.com/openshift/api/route/v1" - tektonapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" - tektonapi_v1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" - oci "github.com/tektoncd/pipeline/pkg/remote/oci" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/strings/slices" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/yaml" "github.com/konflux-ci/build-service/pkg/boerrors" . "github.com/konflux-ci/build-service/pkg/common" - "github.com/konflux-ci/build-service/pkg/git" gp "github.com/konflux-ci/build-service/pkg/git/gitprovider" "github.com/konflux-ci/build-service/pkg/git/gitproviderfactory" l "github.com/konflux-ci/build-service/pkg/logs" @@ -184,55 +166,48 @@ func getHttpClient() *http.Client { // #nosec G402 // dev instances need insecur return client } -// ensureIncomingSecret is ensuring that incoming secret for PaC trigger exists -// if secret doesn't exists it will create it and also add repository as owner -// Returns: -// pointer to secret object -// bool which indicates if reconcile is required (which is required when we just created secret) -func (r *ComponentBuildReconciler) ensureIncomingSecret(ctx context.Context, component *appstudiov1alpha1.Component) (*corev1.Secret, bool, error) { - log := ctrllog.FromContext(ctx) - - repository, err := r.findPaCRepositoryForComponent(ctx, component) - if err != nil { - return nil, false, err - } +// validatePaCConfiguration detects checks that all required fields is set for whatever method is used. +func validatePaCConfiguration(gitProvider string, pacSecret corev1.Secret) error { + if IsPaCApplicationConfigured(gitProvider, pacSecret.Data) { + if gitProvider == "github" { + // GitHub application + err := checkMandatoryFieldsNotEmpty(pacSecret.Data, []string{PipelinesAsCodeGithubAppIdKey, PipelinesAsCodeGithubPrivateKey}) + if err != nil { + return err + } - incomingSecretName := fmt.Sprintf("%s%s", repository.Name, pacIncomingSecretNameSuffix) - incomingSecretPassword := generatePaCWebhookSecretString() - incomingSecretData := map[string]string{ - pacIncomingSecretKey: incomingSecretPassword, - } + // validate content of the fields + if _, e := strconv.ParseInt(string(pacSecret.Data[PipelinesAsCodeGithubAppIdKey]), 10, 64); e != nil { + return fmt.Errorf(" Pipelines as Code: failed to parse GitHub application ID. Cause: %w", e) + } - secret := corev1.Secret{} - if err := r.Client.Get(ctx, types.NamespacedName{Namespace: component.Namespace, Name: incomingSecretName}, &secret); err != nil { - if !errors.IsNotFound(err) { - log.Error(err, "failed to get incoming secret", l.Action, l.ActionView) - return nil, false, err - } - // Create incoming secret - secret = corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: incomingSecretName, - Namespace: component.Namespace, - }, - Type: corev1.SecretTypeOpaque, - StringData: incomingSecretData, + privateKey := strings.TrimSpace(string(pacSecret.Data[PipelinesAsCodeGithubPrivateKey])) + if !strings.HasPrefix(privateKey, "-----BEGIN RSA PRIVATE KEY-----") || + !strings.HasSuffix(privateKey, "-----END RSA PRIVATE KEY-----") { + return fmt.Errorf(" Pipelines as Code secret: GitHub application private key is invalid") + } + return nil } + return fmt.Errorf(fmt.Sprintf("There is no applications for %s", gitProvider)) + } - if err := controllerutil.SetOwnerReference(repository, &secret, r.Scheme); err != nil { - log.Error(err, "failed to set owner for incoming secret") - return nil, false, err - } + switch pacSecret.Type { + case corev1.SecretTypeSSHAuth: + return checkMandatoryFieldsNotEmpty(pacSecret.Data, []string{"ssh-privatekey"}) + case corev1.SecretTypeBasicAuth, corev1.SecretTypeOpaque: + return checkMandatoryFieldsNotEmpty(pacSecret.Data, []string{"password"}) + default: + return fmt.Errorf("git secret: unsupported secret type: %s", pacSecret.Type) + } +} - if err := r.Client.Create(ctx, &secret); err != nil { - log.Error(err, "failed to create incoming secret", l.Action, l.ActionAdd) - return nil, false, err +func checkMandatoryFieldsNotEmpty(config map[string][]byte, mandatoryFields []string) error { + for _, field := range mandatoryFields { + if len(config[field]) == 0 { + return fmt.Errorf("git secret: %s field is not configured", field) } - - log.Info("incoming secret created") - return &secret, true, nil } - return &secret, false, nil + return nil } func (r *ComponentBuildReconciler) TriggerPaCBuild(ctx context.Context, component *appstudiov1alpha1.Component) (bool, error) { @@ -335,105 +310,6 @@ func (r *ComponentBuildReconciler) TriggerPaCBuild(ctx context.Context, componen return false, nil } -// cleanupPaCRepositoryIncomingsAndSecret is cleaning up incomings in Repository -// for unprovisioned component, and also removes incoming secret when no longer required -func (r *ComponentBuildReconciler) cleanupPaCRepositoryIncomingsAndSecret(ctx context.Context, component *appstudiov1alpha1.Component, baseBranch string) error { - log := ctrllog.FromContext(ctx) - - // check if more components are using same repo with PaC enabled for incomings removal from repository - incomingsRepoTargetBranchCount := 0 - incomingsRepoAllBranchesCount := 0 - componentList := &appstudiov1alpha1.ComponentList{} - if err := r.Client.List(ctx, componentList, &client.ListOptions{Namespace: component.Namespace}); err != nil { - log.Error(err, "failed to list Components", l.Action, l.ActionView) - return err - } - buildStatus := &BuildStatus{} - for _, comp := range componentList.Items { - if comp.Spec.Source.GitSource.URL == component.Spec.Source.GitSource.URL { - buildStatus = readBuildStatus(component) - if buildStatus.PaC != nil && buildStatus.PaC.State == "enabled" { - incomingsRepoAllBranchesCount += 1 - - // revision can be empty and then use default branch - if comp.Spec.Source.GitSource.Revision == component.Spec.Source.GitSource.Revision || comp.Spec.Source.GitSource.Revision == baseBranch { - incomingsRepoTargetBranchCount += 1 - } - } - } - } - - repository, err := r.findPaCRepositoryForComponent(ctx, component) - if err != nil { - return err - } - - // repository is used to construct incoming secret name - if repository == nil { - return nil - } - - incomingSecretName := fmt.Sprintf("%s%s", repository.Name, pacIncomingSecretNameSuffix) - incomingUpdated := false - // update first in case there is multiple incoming entries, and it will be converted to incomings with just 1 entry - _ = updateIncoming(repository, incomingSecretName, pacIncomingSecretKey, baseBranch) - - if len((*repository.Spec.Incomings)[0].Targets) > 1 { - // incoming contains target from the current component only - if slices.Contains((*repository.Spec.Incomings)[0].Targets, baseBranch) && incomingsRepoTargetBranchCount <= 1 { - newTargets := []string{} - for _, target := range (*repository.Spec.Incomings)[0].Targets { - if target != baseBranch { - newTargets = append(newTargets, target) - } - } - (*repository.Spec.Incomings)[0].Targets = newTargets - incomingUpdated = true - } - // remove secret from incomings if just current component is using incomings in repository - if incomingsRepoAllBranchesCount <= 1 && incomingsRepoTargetBranchCount <= 1 { - (*repository.Spec.Incomings)[0].Secret = pacv1alpha1.Secret{} - incomingUpdated = true - } - - } else { - // incomings has just 1 target and that target is from the current component only - if (*repository.Spec.Incomings)[0].Targets[0] == baseBranch && incomingsRepoTargetBranchCount <= 1 { - repository.Spec.Incomings = nil - incomingUpdated = true - } - } - - if incomingUpdated { - if err := r.Client.Update(ctx, repository); err != nil { - log.Error(err, "failed to update existing PaC repository with incomings", "PaCRepositoryName", repository.Name) - return err - } - log.Info("Removed incomings from the PaC repository", "PaCRepositoryName", repository.Name, l.Action, l.ActionUpdate) - } - - // remove incoming secret if just current component is using incomings in repository - if incomingsRepoAllBranchesCount <= 1 && incomingsRepoTargetBranchCount <= 1 { - secret := &corev1.Secret{} - if err := r.Client.Get(ctx, types.NamespacedName{Namespace: component.Namespace, Name: incomingSecretName}, secret); err != nil { - if !errors.IsNotFound(err) { - log.Error(err, "failed to get incoming secret", l.Action, l.ActionView) - return err - } - log.Info("incoming secret doesn't exist anymore, removal isn't required") - } else { - if err := r.Client.Delete(ctx, secret); err != nil { - if !errors.IsNotFound(err) { - log.Error(err, "failed to remove incoming secret", l.Action, l.ActionView) - return err - } - } - log.Info("incoming secret removed") - } - } - return nil -} - // UndoPaCProvisionForComponent creates merge request that removes Pipelines as Code configuration from component source repository. // Deletes PaC webhook if used. // In case of any errors just logs them and does not block Component deletion. @@ -491,122 +367,6 @@ func (r *ComponentBuildReconciler) UndoPaCProvisionForComponent(ctx context.Cont return mrUrl, nil } -func (r *ComponentBuildReconciler) lookupPaCSecret(ctx context.Context, component *appstudiov1alpha1.Component, gitProvider string) (*corev1.Secret, error) { - log := ctrllog.FromContext(ctx) - - scmComponent, err := git.NewScmComponent(gitProvider, component.Spec.Source.GitSource.URL, component.Spec.Source.GitSource.Revision, component.Name, component.Namespace) - if err != nil { - return nil, err - } - // find the best matching secret, starting from SSH type - secret, err := r.CredentialProvider.LookupSecret(ctx, scmComponent, corev1.SecretTypeSSHAuth) - if err != nil && !boerrors.IsBuildOpError(err, boerrors.EComponentGitSecretMissing) { - log.Error(err, "failed to get Pipelines as Code SSH secret", "scmComponent", scmComponent) - return nil, err - } - if secret != nil { - return secret, nil - } - // find the best matching secret, starting from BasicAuth type - secret, err = r.CredentialProvider.LookupSecret(ctx, scmComponent, corev1.SecretTypeBasicAuth) - if err != nil && !boerrors.IsBuildOpError(err, boerrors.EComponentGitSecretMissing) { - log.Error(err, "failed to get Pipelines as Code BasicAuth secret", "scmComponent", scmComponent) - return nil, err - } - if secret != nil { - return secret, nil - } - - // No SCM secrets found in the component namespace, fall back to the global configuration - if gitProvider == "github" { - return r.lookupGHAppSecret(ctx) - } else { - return nil, boerrors.NewBuildOpError(boerrors.EPaCSecretNotFound, fmt.Errorf("no matching Pipelines as Code secrets found in %s namespace", component.Namespace)) - } - -} - -func (r *ComponentBuildReconciler) lookupGHAppSecret(ctx context.Context) (*corev1.Secret, error) { - pacSecret := &corev1.Secret{} - globalPaCSecretKey := types.NamespacedName{Namespace: BuildServiceNamespaceName, Name: PipelinesAsCodeGitHubAppSecretName} - if err := r.Client.Get(ctx, globalPaCSecretKey, pacSecret); err != nil { - if !errors.IsNotFound(err) { - r.EventRecorder.Event(pacSecret, "Warning", "ErrorReadingPaCSecret", err.Error()) - return nil, fmt.Errorf("failed to get Pipelines as Code secret in %s namespace: %w", globalPaCSecretKey.Namespace, err) - } - - r.EventRecorder.Event(pacSecret, "Warning", "PaCSecretNotFound", err.Error()) - // Do not trigger a new reconcile. The PaC secret must be created first. - return nil, boerrors.NewBuildOpError(boerrors.EPaCSecretNotFound, fmt.Errorf(" Pipelines as Code secret not found in %s ", globalPaCSecretKey.Namespace)) - } - return pacSecret, nil -} - -// Returns webhook secret for given component. -// Generates the webhook secret and saves it in the k8s secret if it doesn't exist. -func (r *ComponentBuildReconciler) ensureWebhookSecret(ctx context.Context, component *appstudiov1alpha1.Component) (string, error) { - log := ctrllog.FromContext(ctx) - - webhookSecretsSecret := &corev1.Secret{} - if err := r.Client.Get(ctx, types.NamespacedName{Name: pipelinesAsCodeWebhooksSecretName, Namespace: component.GetNamespace()}, webhookSecretsSecret); err != nil { - if errors.IsNotFound(err) { - webhookSecretsSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: pipelinesAsCodeWebhooksSecretName, - Namespace: component.GetNamespace(), - Labels: map[string]string{ - PartOfLabelName: PartOfAppStudioLabelValue, - }, - }, - } - if err := r.Client.Create(ctx, webhookSecretsSecret); err != nil { - log.Error(err, "failed to create webhooks secrets secret", l.Action, l.ActionAdd) - return "", err - } - return r.ensureWebhookSecret(ctx, component) - } - - log.Error(err, "failed to get webhook secrets secret", l.Action, l.ActionView) - return "", err - } - - componentWebhookSecretKey := getWebhookSecretKeyForComponent(*component) - if _, exists := webhookSecretsSecret.Data[componentWebhookSecretKey]; exists { - // The webhook secret already exists. Use single secret for the same repository. - return string(webhookSecretsSecret.Data[componentWebhookSecretKey]), nil - } - - webhookSecretString := generatePaCWebhookSecretString() - - if webhookSecretsSecret.Data == nil { - webhookSecretsSecret.Data = make(map[string][]byte) - } - webhookSecretsSecret.Data[componentWebhookSecretKey] = []byte(webhookSecretString) - if err := r.Client.Update(ctx, webhookSecretsSecret); err != nil { - log.Error(err, "failed to update webhook secrets secret", l.Action, l.ActionUpdate) - return "", err - } - - return webhookSecretString, nil -} - -func getWebhookSecretKeyForComponent(component appstudiov1alpha1.Component) string { - gitRepoUrl := strings.TrimSuffix(component.Spec.Source.GitSource.URL, ".git") - - notAllowedCharRegex, _ := regexp.Compile("[^-._a-zA-Z0-9]{1}") - return notAllowedCharRegex.ReplaceAllString(gitRepoUrl, "_") -} - -// generatePaCWebhookSecretString generates string alike openssl rand -hex 20 -func generatePaCWebhookSecretString() string { - length := 20 // in bytes - tokenBytes := make([]byte, length) - if _, err := rand.Read(tokenBytes); err != nil { - panic("Failed to read from random generator") - } - return hex.EncodeToString(tokenBytes) -} - // getPaCWebhookTargetUrl returns URL to which events from git repository should be sent. func (r *ComponentBuildReconciler) getPaCWebhookTargetUrl(ctx context.Context, repositoryURL string) (string, error) { webhookTargetUrl := os.Getenv(pipelinesAsCodeRouteEnvVar) @@ -650,302 +410,6 @@ func (r *ComponentBuildReconciler) getPaCRoutePublicUrl(ctx context.Context) (st return "https://" + pacWebhookRoute.Spec.Host, nil } -// validatePaCConfiguration detects checks that all required fields is set for whatever method is used. -func validatePaCConfiguration(gitProvider string, pacSecret corev1.Secret) error { - if IsPaCApplicationConfigured(gitProvider, pacSecret.Data) { - if gitProvider == "github" { - // GitHub application - err := checkMandatoryFieldsNotEmpty(pacSecret.Data, []string{PipelinesAsCodeGithubAppIdKey, PipelinesAsCodeGithubPrivateKey}) - if err != nil { - return err - } - - // validate content of the fields - if _, e := strconv.ParseInt(string(pacSecret.Data[PipelinesAsCodeGithubAppIdKey]), 10, 64); e != nil { - return fmt.Errorf(" Pipelines as Code: failed to parse GitHub application ID. Cause: %w", e) - } - - privateKey := strings.TrimSpace(string(pacSecret.Data[PipelinesAsCodeGithubPrivateKey])) - if !strings.HasPrefix(privateKey, "-----BEGIN RSA PRIVATE KEY-----") || - !strings.HasSuffix(privateKey, "-----END RSA PRIVATE KEY-----") { - return fmt.Errorf(" Pipelines as Code secret: GitHub application private key is invalid") - } - return nil - } - return fmt.Errorf(fmt.Sprintf("There is no applications for %s", gitProvider)) - } - - switch pacSecret.Type { - case corev1.SecretTypeSSHAuth: - return checkMandatoryFieldsNotEmpty(pacSecret.Data, []string{"ssh-privatekey"}) - case corev1.SecretTypeBasicAuth, corev1.SecretTypeOpaque: - return checkMandatoryFieldsNotEmpty(pacSecret.Data, []string{"password"}) - default: - return fmt.Errorf("git secret: unsupported secret type: %s", pacSecret.Type) - } -} - -func checkMandatoryFieldsNotEmpty(config map[string][]byte, mandatoryFields []string) error { - for _, field := range mandatoryFields { - if len(config[field]) == 0 { - return fmt.Errorf("git secret: %s field is not configured", field) - } - } - return nil -} - -// pacRepoAddParamWorkspaceName adds custom parameter workspace name to a PaC repository. -// Existing parameter will be overridden. -func pacRepoAddParamWorkspaceName(log logr.Logger, repository *pacv1alpha1.Repository, workspaceName string) { - var params []pacv1alpha1.Params - // Before pipelines-as-code gets upgraded for application-service to the - // version supporting custom parameters, this check must be taken. - if repository.Spec.Params == nil { - params = make([]pacv1alpha1.Params, 0) - } else { - params = *repository.Spec.Params - } - found := -1 - for i, param := range params { - if param.Name == pacCustomParamAppstudioWorkspace { - found = i - break - } - } - workspaceParam := pacv1alpha1.Params{ - Name: pacCustomParamAppstudioWorkspace, - Value: workspaceName, - } - if found >= 0 { - params[found] = workspaceParam - } else { - params = append(params, workspaceParam) - } - repository.Spec.Params = ¶ms -} - -func (r *ComponentBuildReconciler) getNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { - ns := &corev1.Namespace{} - err := r.Client.Get(ctx, types.NamespacedName{Name: name}, ns) - if err == nil { - return ns, nil - } else { - return nil, err - } -} - -func (r *ComponentBuildReconciler) ensurePaCRepository(ctx context.Context, component *appstudiov1alpha1.Component, pacConfig *corev1.Secret) error { - log := ctrllog.FromContext(ctx) - - // Check multi component git repository scenario. - // It's not possible to determine multi component git repository scenario by context directory field, - // therefore it's required to do the check for all components. - // For example, there are several dockerfiles in the same git repository - // and each of them builds separate component from the common codebase. - // Another scenario is component per branch. - repository, err := r.findPaCRepositoryForComponent(ctx, component) - if err != nil { - return err - } - if repository != nil { - pacRepositoryOwnersNumber := len(repository.OwnerReferences) - if err := controllerutil.SetOwnerReference(component, repository, r.Scheme); err != nil { - log.Error(err, "failed to add owner reference to existing PaC repository", "PaCRepositoryName", repository.Name) - return err - } - if len(repository.OwnerReferences) > pacRepositoryOwnersNumber { - if err := r.Client.Update(ctx, repository); err != nil { - log.Error(err, "failed to update existing PaC repository with component owner reference", "PaCRepositoryName", repository.Name) - return err - } - log.Info("Added current component to owners of the PaC repository", "PaCRepositoryName", repository.Name, l.Action, l.ActionUpdate) - } else { - log.Info("Using existing PaC Repository object for the component", "PaCRepositoryName", repository.Name) - } - return nil - } - - // This is the first Component that does PaC provision for the git repository - repository, err = generatePACRepository(*component, pacConfig) - if err != nil { - return err - } - - ns, err := r.getNamespace(ctx, component.GetNamespace()) - if err != nil { - log.Error(err, "failed to get the component namespace for setting custom parameter.") - return err - } - if val, ok := ns.Labels[appstudioWorkspaceNameLabel]; ok { - pacRepoAddParamWorkspaceName(log, repository, val) - } - - existingRepository := &pacv1alpha1.Repository{} - if err := r.Client.Get(ctx, types.NamespacedName{Name: repository.Name, Namespace: repository.Namespace}, existingRepository); err != nil { - if errors.IsNotFound(err) { - if err := controllerutil.SetOwnerReference(component, repository, r.Scheme); err != nil { - return err - } - if err := r.Client.Create(ctx, repository); err != nil { - if strings.Contains(err.Error(), "repository already exist with url") { - // PaC admission webhook denied creation of the PaC repository, - // because PaC repository object that references the same git repository already exists. - log.Info("An attempt to create second PaC Repository for the same git repository", "GitRepository", repository.Spec.URL, l.Action, l.ActionAdd, l.Audit, "true") - return boerrors.NewBuildOpError(boerrors.EPaCDuplicateRepository, err) - } - - if strings.Contains(err.Error(), "denied the request: failed to validate url error") { - // PaC admission webhook denied creation of the PaC repository, - // because PaC repository object that references not allowed repository url. - - log.Info("An attempt to create PaC Repository for not allowed repository url", "GitRepository", repository.Spec.URL, l.Action, l.ActionAdd, l.Audit, "true") - return boerrors.NewBuildOpError(boerrors.EPaCNotAllowedRepositoryUrl, err) - } - - log.Error(err, "failed to create Component PaC repository object", l.Action, l.ActionAdd) - return err - } - log.Info("Created PaC Repository object for the component") - - } else { - log.Error(err, "failed to get Component PaC repository object", l.Action, l.ActionView) - return err - } - } - return nil -} - -// findPaCRepositoryForComponent searches for existing matching PaC repository object for given component. -// The search makes sense only in the same namespace. -func (r *ComponentBuildReconciler) findPaCRepositoryForComponent(ctx context.Context, component *appstudiov1alpha1.Component) (*pacv1alpha1.Repository, error) { - log := ctrllog.FromContext(ctx) - - pacRepositoriesList := &pacv1alpha1.RepositoryList{} - err := r.Client.List(ctx, pacRepositoriesList, &client.ListOptions{Namespace: component.Namespace}) - if err != nil { - log.Error(err, "failed to list PaC repositories") - return nil, err - } - - gitUrl := strings.TrimSuffix(strings.TrimSuffix(component.Spec.Source.GitSource.URL, ".git"), "/") - for _, pacRepository := range pacRepositoriesList.Items { - if pacRepository.Spec.URL == gitUrl { - return &pacRepository, nil - } - } - return nil, nil -} - -// generatePACRepository creates configuration of Pipelines as Code repository object. -func generatePACRepository(component appstudiov1alpha1.Component, config *corev1.Secret) (*pacv1alpha1.Repository, error) { - gitProvider, err := getGitProvider(component) - if err != nil { - return nil, err - } - - isAppUsed := IsPaCApplicationConfigured(gitProvider, config.Data) - - var gitProviderConfig *pacv1alpha1.GitProvider = nil - if !isAppUsed { - // Webhook is used - gitProviderConfig = &pacv1alpha1.GitProvider{ - Secret: &pacv1alpha1.Secret{ - Name: config.Name, - Key: corev1.BasicAuthPasswordKey, // basic-auth secret type expected - }, - WebhookSecret: &pacv1alpha1.Secret{ - Name: pipelinesAsCodeWebhooksSecretName, - Key: getWebhookSecretKeyForComponent(component), - }, - } - - if gitProvider == "gitlab" { - if providerUrl, configured := component.Annotations[GitProviderAnnotationURL]; configured { - gitProviderConfig.URL = providerUrl - } else { - // Get git provider URL from source URL. - u, err := url.Parse(component.Spec.Source.GitSource.URL) - if err != nil { - return nil, err - } - gitProviderConfig.URL = u.Scheme + "://" + u.Host - } - } - } - - if url, ok := component.Annotations[GitProviderAnnotationURL]; ok { - if gitProviderConfig == nil { - gitProviderConfig = &pacv1alpha1.GitProvider{} - } - gitProviderConfig.URL = url - } - - repository := &pacv1alpha1.Repository{ - TypeMeta: metav1.TypeMeta{ - Kind: "Repository", - APIVersion: "pipelinesascode.tekton.dev/v1alpha1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: component.Name, - Namespace: component.Namespace, - }, - Spec: pacv1alpha1.RepositorySpec{ - URL: strings.TrimSuffix(strings.TrimSuffix(component.Spec.Source.GitSource.URL, ".git"), "/"), - GitProvider: gitProviderConfig, - }, - } - - return repository, nil -} - -// generatePaCPipelineRunConfigs generates PipelineRun YAML configs for given component. -// The generated PipelineRun Yaml content are returned in byte string and in the order of push and pull request. -func (r *ComponentBuildReconciler) generatePaCPipelineRunConfigs(ctx context.Context, component *appstudiov1alpha1.Component, gitClient gp.GitProviderClient, pacTargetBranch string) ([]byte, []byte, error) { - log := ctrllog.FromContext(ctx) - - var pipelineName string - var pipelineBundle string - var pipelineRef *tektonapi.PipelineRef - var err error - - // no need to check error because it would fail already in Reconcile - pipelineRef, _ = r.GetBuildPipelineFromComponentAnnotation(ctx, component) - pipelineName, pipelineBundle, err = getPipelineNameAndBundle(pipelineRef) - if err != nil { - return nil, nil, err - } - log.Info(fmt.Sprintf("Selected %s pipeline from %s bundle for %s component", - pipelineName, pipelineBundle, component.Name), - l.Audit, "true") - - // Get pipeline from the bundle to be expanded to the PipelineRun - pipelineSpec, err := retrievePipelineSpec(ctx, pipelineBundle, pipelineName) - if err != nil { - r.EventRecorder.Event(component, "Warning", "ErrorGettingPipelineFromBundle", err.Error()) - return nil, nil, err - } - - pipelineRunOnPush, err := generatePaCPipelineRunForComponent(component, pipelineSpec, pacTargetBranch, gitClient, false) - if err != nil { - return nil, nil, err - } - pipelineRunOnPushYaml, err := yaml.Marshal(pipelineRunOnPush) - if err != nil { - return nil, nil, err - } - - pipelineRunOnPR, err := generatePaCPipelineRunForComponent(component, pipelineSpec, pacTargetBranch, gitClient, true) - if err != nil { - return nil, nil, err - } - pipelineRunOnPRYaml, err := yaml.Marshal(pipelineRunOnPR) - if err != nil { - return nil, nil, err - } - - return pipelineRunOnPushYaml, pipelineRunOnPRYaml, nil -} - func generateMergeRequestSourceBranch(component *appstudiov1alpha1.Component) string { return fmt.Sprintf("%s%s", pacMergeRequestSourceBranchPrefix, component.Name) } @@ -1135,269 +599,6 @@ func (r *ComponentBuildReconciler) UnconfigureRepositoryForPaC(ctx context.Conte return baseBranch, prUrl, action_done, err } -// generatePaCPipelineRunForComponent returns pipeline run definition to build component source with. -// Generated pipeline run contains placeholders that are expanded by Pipeline-as-Code. -func generatePaCPipelineRunForComponent( - component *appstudiov1alpha1.Component, - pipelineSpec *tektonapi.PipelineSpec, - pacTargetBranch string, - gitClient gp.GitProviderClient, - onPull bool) (*tektonapi.PipelineRun, error) { - - if pacTargetBranch == "" { - return nil, fmt.Errorf("target branch can't be empty for generating PaC PipelineRun for: %v", component) - } - pipelineCelExpression, err := generateCelExpressionForPipeline(component, gitClient, pacTargetBranch, onPull) - if err != nil { - return nil, fmt.Errorf("failed to generate cel expression for pipeline: %w", err) - } - repoUrl := component.Spec.Source.GitSource.URL - - annotations := map[string]string{ - "pipelinesascode.tekton.dev/max-keep-runs": "3", - "build.appstudio.redhat.com/target_branch": "{{target_branch}}", - pacCelExpressionAnnotationName: pipelineCelExpression, - gitCommitShaAnnotationName: "{{revision}}", - gitRepoAtShaAnnotationName: gitClient.GetBrowseRepositoryAtShaLink(repoUrl, "{{revision}}"), - } - labels := map[string]string{ - ApplicationNameLabelName: component.Spec.Application, - ComponentNameLabelName: component.Name, - "pipelines.appstudio.openshift.io/type": "build", - } - - imageRepo := getContainerImageRepositoryForComponent(component) - - var pipelineName string - var proposedImage string - if onPull { - annotations["build.appstudio.redhat.com/pull_request_number"] = "{{pull_request_number}}" - pipelineName = component.Name + pipelineRunOnPRSuffix - proposedImage = imageRepo + ":on-pr-{{revision}}" - } else { - pipelineName = component.Name + pipelineRunOnPushSuffix - proposedImage = imageRepo + ":{{revision}}" - } - - params := []tektonapi.Param{ - {Name: "git-url", Value: tektonapi.ParamValue{Type: "string", StringVal: "{{source_url}}"}}, - {Name: "revision", Value: tektonapi.ParamValue{Type: "string", StringVal: "{{revision}}"}}, - {Name: "output-image", Value: tektonapi.ParamValue{Type: "string", StringVal: proposedImage}}, - } - if onPull { - prImageExpiration := os.Getenv(PipelineRunOnPRExpirationEnvVar) - if prImageExpiration == "" { - prImageExpiration = PipelineRunOnPRExpirationDefault - } - params = append(params, tektonapi.Param{Name: "image-expires-after", Value: tektonapi.ParamValue{Type: "string", StringVal: prImageExpiration}}) - } - - if component.Spec.Source.GitSource.DockerfileURL != "" { - params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: component.Spec.Source.GitSource.DockerfileURL}}) - } else { - params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: "Dockerfile"}}) - } - pathContext := getPathContext(component.Spec.Source.GitSource.Context, "") - if pathContext != "" { - params = append(params, tektonapi.Param{Name: "path-context", Value: tektonapi.ParamValue{Type: "string", StringVal: pathContext}}) - } - - pipelineRunWorkspaces := createWorkspaceBinding(pipelineSpec.Workspaces) - - pipelineRun := &tektonapi.PipelineRun{ - TypeMeta: metav1.TypeMeta{ - Kind: "PipelineRun", - APIVersion: "tekton.dev/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: pipelineName, - Namespace: component.Namespace, - Labels: labels, - Annotations: annotations, - }, - Spec: tektonapi.PipelineRunSpec{ - PipelineSpec: pipelineSpec, - Params: params, - Workspaces: pipelineRunWorkspaces, - }, - } - - return pipelineRun, nil -} - -// generateCelExpressionForPipeline generates value for pipelinesascode.tekton.dev/on-cel-expression annotation -// in order to have better flexibility with git events filtering. -// Examples of returned values: -// event == "push" && target_branch == "main" -// event == "pull_request" && target_branch == "my-branch" && ( "component-src-dir/***".pathChanged() || ".tekton/pipeline.yaml".pathChanged() || "dockerfiles/my-component/Dockerfile".pathChanged() ) -func generateCelExpressionForPipeline(component *appstudiov1alpha1.Component, gitClient gp.GitProviderClient, targetBranch string, onPull bool) (string, error) { - eventType := "push" - if onPull { - eventType = "pull_request" - } - eventCondition := fmt.Sprintf(`event == "%s"`, eventType) - - targetBranchCondition := fmt.Sprintf(`target_branch == "%s"`, targetBranch) - - // Set path changed event filtering only for Components that are stored within a directory of the git repository. - // Also, we have to rebuild everything on push events, so applying the filter only to pull request pipeline. - pathChangedSuffix := "" - if onPull && component.Spec.Source.GitSource.Context != "" && component.Spec.Source.GitSource.Context != "/" && component.Spec.Source.GitSource.Context != "./" && component.Spec.Source.GitSource.Context != "." { - contextDir := component.Spec.Source.GitSource.Context - if !strings.HasSuffix(contextDir, "/") { - contextDir += "/" - } - - // If a Dockerfile is defined for the Component, - // we should rebuild the Component if the Dockerfile has been changed. - dockerfilePathChangedSuffix := "" - dockerfile := component.Spec.Source.GitSource.DockerfileURL - if dockerfile != "" { - // Ignore dockerfile that is not stored in the same git repository but downloaded by an URL. - if !strings.Contains(dockerfile, "://") { - // dockerfile could be relative to the context directory or repository root. - // To avoid unessesary builds, it's required to pass absolute path to the Dockerfile. - repoUrl := component.Spec.Source.GitSource.URL - branch := component.Spec.Source.GitSource.Revision - dockerfilePath := contextDir + dockerfile - isDockerfileInContextDir, err := gitClient.IsFileExist(repoUrl, branch, dockerfilePath) - if err != nil { - return "", err - } - // If the Dockerfile is inside context directory, no changes to event filter needed. - if !isDockerfileInContextDir { - // Pipelines as Code doesn't match path if it starts from / - dockerfileAbsolutePath := strings.TrimPrefix(dockerfile, "/") - dockerfilePathChangedSuffix = fmt.Sprintf(`|| "%s".pathChanged() `, dockerfileAbsolutePath) - } - } - } - - pullPipelineFileName := component.Name + "-" + pipelineRunOnPRFilename - pathChangedSuffix = fmt.Sprintf(` && ( "%s***".pathChanged() || ".tekton/%s".pathChanged() %s)`, contextDir, pullPipelineFileName, dockerfilePathChangedSuffix) - } - - return fmt.Sprintf("%s && %s%s", eventCondition, targetBranchCondition, pathChangedSuffix), nil -} - -func createWorkspaceBinding(pipelineWorkspaces []tektonapi.PipelineWorkspaceDeclaration) []tektonapi.WorkspaceBinding { - pipelineRunWorkspaces := []tektonapi.WorkspaceBinding{} - for _, workspace := range pipelineWorkspaces { - switch workspace.Name { - case "workspace": - pipelineRunWorkspaces = append(pipelineRunWorkspaces, - tektonapi.WorkspaceBinding{ - Name: workspace.Name, - VolumeClaimTemplate: generateVolumeClaimTemplate(), - }) - case "git-auth": - pipelineRunWorkspaces = append(pipelineRunWorkspaces, - tektonapi.WorkspaceBinding{ - Name: workspace.Name, - Secret: &corev1.SecretVolumeSource{SecretName: "{{ git_auth_secret }}"}, - }) - } - } - return pipelineRunWorkspaces -} - -// retrievePipelineSpec retrieves pipeline definition with given name from the given bundle. -func retrievePipelineSpec(ctx context.Context, bundleUri, pipelineName string) (*tektonapi.PipelineSpec, error) { - log := ctrllog.FromContext(ctx) - - var obj runtime.Object - var err error - resolver := oci.NewResolver(bundleUri, authn.DefaultKeychain) - - if obj, _, err = resolver.Get(ctx, "pipeline", pipelineName); err != nil { - return nil, err - } - - var pipelineSpec tektonapi.PipelineSpec - - if v1beta1Pipeline, ok := obj.(tektonapi_v1beta1.PipelineObject); ok { - v1beta1PipelineSpec := v1beta1Pipeline.PipelineSpec() - log.Info("Converting from v1beta1 to v1", "PipelineName", pipelineName, "Bundle", bundleUri) - err := v1beta1PipelineSpec.ConvertTo(ctx, &pipelineSpec, &metav1.ObjectMeta{}) - if err != nil { - return nil, boerrors.NewBuildOpError( - boerrors.EPipelineConversionFailed, - fmt.Errorf("pipeline %s from bundle %s: failed to convert from v1beta1 to v1: %w", pipelineName, bundleUri, err), - ) - } - } else if v1Pipeline, ok := obj.(*tektonapi.Pipeline); ok { - pipelineSpec = v1Pipeline.PipelineSpec() - } else { - return nil, boerrors.NewBuildOpError( - boerrors.EPipelineRetrievalFailed, - fmt.Errorf("failed to extract pipeline %s from bundle %s", pipelineName, bundleUri), - ) - } - - return &pipelineSpec, nil -} - -// updateIncoming updates incomings in repository, adds new incoming for provided branch with incoming secret -// if repository contains multiple incoming entries, it will merge them to one, and combine Targets and add incoming secret to incoming -// if repository contains one incoming entry, it will add new target and add incoming secret to incoming -// if repository doesn't have any incoming entry, it will add new incoming entry with target and add incoming secret to incoming -// Returns bool, indicating if incomings in repository was updated or not -func updateIncoming(repository *pacv1alpha1.Repository, incomingSecretName string, pacIncomingSecretKey string, targetBranch string) bool { - foundSecretName := false - foundTarget := false - multiple_incomings := false - all_targets := []string{} - - if repository.Spec.Incomings != nil { - if len(*repository.Spec.Incomings) > 1 { - multiple_incomings = true - } - - for idx, key := range *repository.Spec.Incomings { - if multiple_incomings { // for multiple incomings gather all targets - for _, target := range key.Targets { - all_targets = append(all_targets, target) - if target == targetBranch { - foundTarget = true - } - } - } else { // for single incoming add target & secret if missing - for _, target := range key.Targets { - if target == targetBranch { - foundTarget = true - break - } - } - // add missing target branch - if !foundTarget { - (*repository.Spec.Incomings)[idx].Targets = append((*repository.Spec.Incomings)[idx].Targets, targetBranch) - } - - if key.Secret.Name == incomingSecretName { - foundSecretName = true - } else { - (*repository.Spec.Incomings)[idx].Secret = pacv1alpha1.Secret{Name: incomingSecretName, Key: pacIncomingSecretKey} - } - } - } - - // combine multiple incomings into one and add secret - if multiple_incomings { - if !foundTarget { - all_targets = append(all_targets, targetBranch) - } - incoming := []pacv1alpha1.Incoming{{Type: "webhook-url", Secret: pacv1alpha1.Secret{Name: incomingSecretName, Key: pacIncomingSecretKey}, Targets: all_targets}} - repository.Spec.Incomings = &incoming - } - } else { - // create incomings when missing - incoming := []pacv1alpha1.Incoming{{Type: "webhook-url", Secret: pacv1alpha1.Secret{Name: incomingSecretName, Key: pacIncomingSecretKey}, Targets: []string{targetBranch}}} - repository.Spec.Incomings = &incoming - } - - return multiple_incomings || !(foundSecretName && foundTarget) -} - // getGitProvider returns git provider name based on the repository url, e.g. github, gitlab, etc or git-privider annotation func getGitProvider(component appstudiov1alpha1.Component) (string, error) { allowedGitProviders := map[string]bool{"github": true, "gitlab": true, "bitbucket": true} @@ -1445,59 +646,3 @@ func getGitProvider(component appstudiov1alpha1.Component) (string, error) { return gitProvider, err } - -func generateVolumeClaimTemplate() *corev1.PersistentVolumeClaim { - return &corev1.PersistentVolumeClaim{ - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{ - "ReadWriteOnce", - }, - Resources: corev1.VolumeResourceRequirements{ - Requests: corev1.ResourceList{ - "storage": resource.MustParse("1Gi"), - }, - }, - }, - } -} - -func getPathContext(gitContext, dockerfileContext string) string { - if gitContext == "" && dockerfileContext == "" { - return "" - } - separator := string(filepath.Separator) - path := filepath.Join(gitContext, dockerfileContext) - path = filepath.Clean(path) - path = strings.TrimPrefix(path, separator) - return path -} - -func getPipelineNameAndBundle(pipelineRef *tektonapi.PipelineRef) (string, string, error) { - if pipelineRef.Resolver != "" && pipelineRef.Resolver != "bundles" { - return "", "", boerrors.NewBuildOpError( - boerrors.EUnsupportedPipelineRef, - fmt.Errorf("unsupported Tekton resolver %q", pipelineRef.Resolver), - ) - } - - name := pipelineRef.Name - var bundle string - - for _, param := range pipelineRef.Params { - switch param.Name { - case "name": - name = param.Value.StringVal - case "bundle": - bundle = param.Value.StringVal - } - } - - if name == "" || bundle == "" { - return "", "", boerrors.NewBuildOpError( - boerrors.EMissingParamsForBundleResolver, - fmt.Errorf("missing name or bundle in pipelineRef: name=%s bundle=%s", name, bundle), - ) - } - - return name, bundle, nil -} diff --git a/controllers/component_build_controller_pac_repository.go b/controllers/component_build_controller_pac_repository.go new file mode 100644 index 00000000..b8a569bb --- /dev/null +++ b/controllers/component_build_controller_pac_repository.go @@ -0,0 +1,404 @@ +/* +Copyright 2021-2024 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/go-logr/logr" + appstudiov1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" + pacv1alpha1 "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/strings/slices" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/konflux-ci/build-service/pkg/boerrors" + . "github.com/konflux-ci/build-service/pkg/common" + l "github.com/konflux-ci/build-service/pkg/logs" +) + +// findPaCRepositoryForComponent searches for existing matching PaC repository object for given component. +// The search makes sense only in the same namespace. +func (r *ComponentBuildReconciler) findPaCRepositoryForComponent(ctx context.Context, component *appstudiov1alpha1.Component) (*pacv1alpha1.Repository, error) { + log := ctrllog.FromContext(ctx) + + pacRepositoriesList := &pacv1alpha1.RepositoryList{} + err := r.Client.List(ctx, pacRepositoriesList, &client.ListOptions{Namespace: component.Namespace}) + if err != nil { + log.Error(err, "failed to list PaC repositories") + return nil, err + } + + gitUrl := strings.TrimSuffix(strings.TrimSuffix(component.Spec.Source.GitSource.URL, ".git"), "/") + for _, pacRepository := range pacRepositoriesList.Items { + if pacRepository.Spec.URL == gitUrl { + return &pacRepository, nil + } + } + return nil, nil +} + +func (r *ComponentBuildReconciler) ensurePaCRepository(ctx context.Context, component *appstudiov1alpha1.Component, pacConfig *corev1.Secret) error { + log := ctrllog.FromContext(ctx) + + // Check multi component git repository scenario. + // It's not possible to determine multi component git repository scenario by context directory field, + // therefore it's required to do the check for all components. + // For example, there are several dockerfiles in the same git repository + // and each of them builds separate component from the common codebase. + // Another scenario is component per branch. + repository, err := r.findPaCRepositoryForComponent(ctx, component) + if err != nil { + return err + } + if repository != nil { + pacRepositoryOwnersNumber := len(repository.OwnerReferences) + if err := controllerutil.SetOwnerReference(component, repository, r.Scheme); err != nil { + log.Error(err, "failed to add owner reference to existing PaC repository", "PaCRepositoryName", repository.Name) + return err + } + if len(repository.OwnerReferences) > pacRepositoryOwnersNumber { + if err := r.Client.Update(ctx, repository); err != nil { + log.Error(err, "failed to update existing PaC repository with component owner reference", "PaCRepositoryName", repository.Name) + return err + } + log.Info("Added current component to owners of the PaC repository", "PaCRepositoryName", repository.Name, l.Action, l.ActionUpdate) + } else { + log.Info("Using existing PaC Repository object for the component", "PaCRepositoryName", repository.Name) + } + return nil + } + + // This is the first Component that does PaC provision for the git repository + repository, err = generatePACRepository(*component, pacConfig) + if err != nil { + return err + } + + ns, err := r.getNamespace(ctx, component.GetNamespace()) + if err != nil { + log.Error(err, "failed to get the component namespace for setting custom parameter.") + return err + } + if val, ok := ns.Labels[appstudioWorkspaceNameLabel]; ok { + pacRepoAddParamWorkspaceName(log, repository, val) + } + + existingRepository := &pacv1alpha1.Repository{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: repository.Name, Namespace: repository.Namespace}, existingRepository); err != nil { + if errors.IsNotFound(err) { + if err := controllerutil.SetOwnerReference(component, repository, r.Scheme); err != nil { + return err + } + if err := r.Client.Create(ctx, repository); err != nil { + if strings.Contains(err.Error(), "repository already exist with url") { + // PaC admission webhook denied creation of the PaC repository, + // because PaC repository object that references the same git repository already exists. + log.Info("An attempt to create second PaC Repository for the same git repository", "GitRepository", repository.Spec.URL, l.Action, l.ActionAdd, l.Audit, "true") + return boerrors.NewBuildOpError(boerrors.EPaCDuplicateRepository, err) + } + + if strings.Contains(err.Error(), "denied the request: failed to validate url error") { + // PaC admission webhook denied creation of the PaC repository, + // because PaC repository object that references not allowed repository url. + + log.Info("An attempt to create PaC Repository for not allowed repository url", "GitRepository", repository.Spec.URL, l.Action, l.ActionAdd, l.Audit, "true") + return boerrors.NewBuildOpError(boerrors.EPaCNotAllowedRepositoryUrl, err) + } + + log.Error(err, "failed to create Component PaC repository object", l.Action, l.ActionAdd) + return err + } + log.Info("Created PaC Repository object for the component") + + } else { + log.Error(err, "failed to get Component PaC repository object", l.Action, l.ActionView) + return err + } + } + return nil +} + +func (r *ComponentBuildReconciler) getNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { + ns := &corev1.Namespace{} + err := r.Client.Get(ctx, types.NamespacedName{Name: name}, ns) + if err == nil { + return ns, nil + } else { + return nil, err + } +} + +// pacRepoAddParamWorkspaceName adds custom parameter workspace name to a PaC repository. +// Existing parameter will be overridden. +func pacRepoAddParamWorkspaceName(log logr.Logger, repository *pacv1alpha1.Repository, workspaceName string) { + var params []pacv1alpha1.Params + // Before pipelines-as-code gets upgraded for application-service to the + // version supporting custom parameters, this check must be taken. + if repository.Spec.Params == nil { + params = make([]pacv1alpha1.Params, 0) + } else { + params = *repository.Spec.Params + } + found := -1 + for i, param := range params { + if param.Name == pacCustomParamAppstudioWorkspace { + found = i + break + } + } + workspaceParam := pacv1alpha1.Params{ + Name: pacCustomParamAppstudioWorkspace, + Value: workspaceName, + } + if found >= 0 { + params[found] = workspaceParam + } else { + params = append(params, workspaceParam) + } + repository.Spec.Params = ¶ms +} + +// generatePACRepository creates configuration of Pipelines as Code repository object. +func generatePACRepository(component appstudiov1alpha1.Component, config *corev1.Secret) (*pacv1alpha1.Repository, error) { + gitProvider, err := getGitProvider(component) + if err != nil { + return nil, err + } + + isAppUsed := IsPaCApplicationConfigured(gitProvider, config.Data) + + var gitProviderConfig *pacv1alpha1.GitProvider = nil + if !isAppUsed { + // Webhook is used + gitProviderConfig = &pacv1alpha1.GitProvider{ + Secret: &pacv1alpha1.Secret{ + Name: config.Name, + Key: corev1.BasicAuthPasswordKey, // basic-auth secret type expected + }, + WebhookSecret: &pacv1alpha1.Secret{ + Name: pipelinesAsCodeWebhooksSecretName, + Key: getWebhookSecretKeyForComponent(component), + }, + } + + if gitProvider == "gitlab" { + if providerUrl, configured := component.Annotations[GitProviderAnnotationURL]; configured { + gitProviderConfig.URL = providerUrl + } else { + // Get git provider URL from source URL. + u, err := url.Parse(component.Spec.Source.GitSource.URL) + if err != nil { + return nil, err + } + gitProviderConfig.URL = u.Scheme + "://" + u.Host + } + } + } + + if url, ok := component.Annotations[GitProviderAnnotationURL]; ok { + if gitProviderConfig == nil { + gitProviderConfig = &pacv1alpha1.GitProvider{} + } + gitProviderConfig.URL = url + } + + repository := &pacv1alpha1.Repository{ + TypeMeta: metav1.TypeMeta{ + Kind: "Repository", + APIVersion: "pipelinesascode.tekton.dev/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: component.Name, + Namespace: component.Namespace, + }, + Spec: pacv1alpha1.RepositorySpec{ + URL: strings.TrimSuffix(strings.TrimSuffix(component.Spec.Source.GitSource.URL, ".git"), "/"), + GitProvider: gitProviderConfig, + }, + } + + return repository, nil +} + +// cleanupPaCRepositoryIncomingsAndSecret is cleaning up incomings in Repository +// for unprovisioned component, and also removes incoming secret when no longer required +func (r *ComponentBuildReconciler) cleanupPaCRepositoryIncomingsAndSecret(ctx context.Context, component *appstudiov1alpha1.Component, baseBranch string) error { + log := ctrllog.FromContext(ctx) + + // check if more components are using same repo with PaC enabled for incomings removal from repository + incomingsRepoTargetBranchCount := 0 + incomingsRepoAllBranchesCount := 0 + componentList := &appstudiov1alpha1.ComponentList{} + if err := r.Client.List(ctx, componentList, &client.ListOptions{Namespace: component.Namespace}); err != nil { + log.Error(err, "failed to list Components", l.Action, l.ActionView) + return err + } + buildStatus := &BuildStatus{} + for _, comp := range componentList.Items { + if comp.Spec.Source.GitSource.URL == component.Spec.Source.GitSource.URL { + buildStatus = readBuildStatus(component) + if buildStatus.PaC != nil && buildStatus.PaC.State == "enabled" { + incomingsRepoAllBranchesCount += 1 + + // revision can be empty and then use default branch + if comp.Spec.Source.GitSource.Revision == component.Spec.Source.GitSource.Revision || comp.Spec.Source.GitSource.Revision == baseBranch { + incomingsRepoTargetBranchCount += 1 + } + } + } + } + + repository, err := r.findPaCRepositoryForComponent(ctx, component) + if err != nil { + return err + } + + // repository is used to construct incoming secret name + if repository == nil { + return nil + } + + incomingSecretName := fmt.Sprintf("%s%s", repository.Name, pacIncomingSecretNameSuffix) + incomingUpdated := false + // update first in case there is multiple incoming entries, and it will be converted to incomings with just 1 entry + _ = updateIncoming(repository, incomingSecretName, pacIncomingSecretKey, baseBranch) + + if len((*repository.Spec.Incomings)[0].Targets) > 1 { + // incoming contains target from the current component only + if slices.Contains((*repository.Spec.Incomings)[0].Targets, baseBranch) && incomingsRepoTargetBranchCount <= 1 { + newTargets := []string{} + for _, target := range (*repository.Spec.Incomings)[0].Targets { + if target != baseBranch { + newTargets = append(newTargets, target) + } + } + (*repository.Spec.Incomings)[0].Targets = newTargets + incomingUpdated = true + } + // remove secret from incomings if just current component is using incomings in repository + if incomingsRepoAllBranchesCount <= 1 && incomingsRepoTargetBranchCount <= 1 { + (*repository.Spec.Incomings)[0].Secret = pacv1alpha1.Secret{} + incomingUpdated = true + } + + } else { + // incomings has just 1 target and that target is from the current component only + if (*repository.Spec.Incomings)[0].Targets[0] == baseBranch && incomingsRepoTargetBranchCount <= 1 { + repository.Spec.Incomings = nil + incomingUpdated = true + } + } + + if incomingUpdated { + if err := r.Client.Update(ctx, repository); err != nil { + log.Error(err, "failed to update existing PaC repository with incomings", "PaCRepositoryName", repository.Name) + return err + } + log.Info("Removed incomings from the PaC repository", "PaCRepositoryName", repository.Name, l.Action, l.ActionUpdate) + } + + // remove incoming secret if just current component is using incomings in repository + if incomingsRepoAllBranchesCount <= 1 && incomingsRepoTargetBranchCount <= 1 { + secret := &corev1.Secret{} + if err := r.Client.Get(ctx, types.NamespacedName{Namespace: component.Namespace, Name: incomingSecretName}, secret); err != nil { + if !errors.IsNotFound(err) { + log.Error(err, "failed to get incoming secret", l.Action, l.ActionView) + return err + } + log.Info("incoming secret doesn't exist anymore, removal isn't required") + } else { + if err := r.Client.Delete(ctx, secret); err != nil { + if !errors.IsNotFound(err) { + log.Error(err, "failed to remove incoming secret", l.Action, l.ActionView) + return err + } + } + log.Info("incoming secret removed") + } + } + return nil +} + +// updateIncoming updates incomings in repository, adds new incoming for provided branch with incoming secret +// if repository contains multiple incoming entries, it will merge them to one, and combine Targets and add incoming secret to incoming +// if repository contains one incoming entry, it will add new target and add incoming secret to incoming +// if repository doesn't have any incoming entry, it will add new incoming entry with target and add incoming secret to incoming +// Returns bool, indicating if incomings in repository was updated or not +func updateIncoming(repository *pacv1alpha1.Repository, incomingSecretName string, pacIncomingSecretKey string, targetBranch string) bool { + foundSecretName := false + foundTarget := false + multiple_incomings := false + all_targets := []string{} + + if repository.Spec.Incomings != nil { + if len(*repository.Spec.Incomings) > 1 { + multiple_incomings = true + } + + for idx, key := range *repository.Spec.Incomings { + if multiple_incomings { // for multiple incomings gather all targets + for _, target := range key.Targets { + all_targets = append(all_targets, target) + if target == targetBranch { + foundTarget = true + } + } + } else { // for single incoming add target & secret if missing + for _, target := range key.Targets { + if target == targetBranch { + foundTarget = true + break + } + } + // add missing target branch + if !foundTarget { + (*repository.Spec.Incomings)[idx].Targets = append((*repository.Spec.Incomings)[idx].Targets, targetBranch) + } + + if key.Secret.Name == incomingSecretName { + foundSecretName = true + } else { + (*repository.Spec.Incomings)[idx].Secret = pacv1alpha1.Secret{Name: incomingSecretName, Key: pacIncomingSecretKey} + } + } + } + + // combine multiple incomings into one and add secret + if multiple_incomings { + if !foundTarget { + all_targets = append(all_targets, targetBranch) + } + incoming := []pacv1alpha1.Incoming{{Type: "webhook-url", Secret: pacv1alpha1.Secret{Name: incomingSecretName, Key: pacIncomingSecretKey}, Targets: all_targets}} + repository.Spec.Incomings = &incoming + } + } else { + // create incomings when missing + incoming := []pacv1alpha1.Incoming{{Type: "webhook-url", Secret: pacv1alpha1.Secret{Name: incomingSecretName, Key: pacIncomingSecretKey}, Targets: []string{targetBranch}}} + repository.Spec.Incomings = &incoming + } + + return multiple_incomings || !(foundSecretName && foundTarget) +} diff --git a/controllers/component_build_controller_pipeline.go b/controllers/component_build_controller_pipeline.go new file mode 100644 index 00000000..5d5362b4 --- /dev/null +++ b/controllers/component_build_controller_pipeline.go @@ -0,0 +1,534 @@ +/* +Copyright 2021-2024 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + appstudiov1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" + tektonapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + tektonapi_v1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + oci "github.com/tektoncd/pipeline/pkg/remote/oci" + + "sigs.k8s.io/yaml" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/konflux-ci/build-service/pkg/boerrors" + . "github.com/konflux-ci/build-service/pkg/common" + gp "github.com/konflux-ci/build-service/pkg/git/gitprovider" + l "github.com/konflux-ci/build-service/pkg/logs" +) + +type BuildPipeline struct { + Name string `json:"name,omitempty"` + Bundle string `json:"bundle,omitempty"` +} + +type pipelineConfig struct { + DefaultPipelineName string `yaml:"default-pipeline-name"` + Pipelines []BuildPipeline `yaml:"pipelines"` +} + +func (r *ComponentBuildReconciler) ensurePipelineServiceAccount(ctx context.Context, namespace string) (*corev1.ServiceAccount, error) { + log := ctrllog.FromContext(ctx) + + pipelinesServiceAccount := &corev1.ServiceAccount{} + err := r.Client.Get(ctx, types.NamespacedName{Name: buildPipelineServiceAccountName, Namespace: namespace}, pipelinesServiceAccount) + if err != nil { + if !errors.IsNotFound(err) { + log.Error(err, fmt.Sprintf("Failed to read service account %s in namespace %s", buildPipelineServiceAccountName, namespace), l.Action, l.ActionView) + return nil, err + } + // Create service account for the build pipeline + buildPipelineSA := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: buildPipelineServiceAccountName, + Namespace: namespace, + }, + } + if err := r.Client.Create(ctx, &buildPipelineSA); err != nil { + log.Error(err, fmt.Sprintf("Failed to create service account %s in namespace %s", buildPipelineServiceAccountName, namespace), l.Action, l.ActionAdd) + return nil, err + } + return r.ensurePipelineServiceAccount(ctx, namespace) + } + return pipelinesServiceAccount, nil +} + +// generatePaCPipelineRunConfigs generates PipelineRun YAML configs for given component. +// The generated PipelineRun Yaml content are returned in byte string and in the order of push and pull request. +func (r *ComponentBuildReconciler) generatePaCPipelineRunConfigs(ctx context.Context, component *appstudiov1alpha1.Component, gitClient gp.GitProviderClient, pacTargetBranch string) ([]byte, []byte, error) { + log := ctrllog.FromContext(ctx) + + var pipelineName string + var pipelineBundle string + var pipelineRef *tektonapi.PipelineRef + var err error + + // no need to check error because it would fail already in Reconcile + pipelineRef, _ = r.GetBuildPipelineFromComponentAnnotation(ctx, component) + pipelineName, pipelineBundle, err = getPipelineNameAndBundle(pipelineRef) + if err != nil { + return nil, nil, err + } + log.Info(fmt.Sprintf("Selected %s pipeline from %s bundle for %s component", + pipelineName, pipelineBundle, component.Name), + l.Audit, "true") + + // Get pipeline from the bundle to be expanded to the PipelineRun + pipelineSpec, err := retrievePipelineSpec(ctx, pipelineBundle, pipelineName) + if err != nil { + r.EventRecorder.Event(component, "Warning", "ErrorGettingPipelineFromBundle", err.Error()) + return nil, nil, err + } + + pipelineRunOnPush, err := generatePaCPipelineRunForComponent(component, pipelineSpec, pacTargetBranch, gitClient, false) + if err != nil { + return nil, nil, err + } + pipelineRunOnPushYaml, err := yaml.Marshal(pipelineRunOnPush) + if err != nil { + return nil, nil, err + } + + pipelineRunOnPR, err := generatePaCPipelineRunForComponent(component, pipelineSpec, pacTargetBranch, gitClient, true) + if err != nil { + return nil, nil, err + } + pipelineRunOnPRYaml, err := yaml.Marshal(pipelineRunOnPR) + if err != nil { + return nil, nil, err + } + + return pipelineRunOnPushYaml, pipelineRunOnPRYaml, nil +} + +// retrievePipelineSpec retrieves pipeline definition with given name from the given bundle. +func retrievePipelineSpec(ctx context.Context, bundleUri, pipelineName string) (*tektonapi.PipelineSpec, error) { + log := ctrllog.FromContext(ctx) + + var obj runtime.Object + var err error + resolver := oci.NewResolver(bundleUri, authn.DefaultKeychain) + + if obj, _, err = resolver.Get(ctx, "pipeline", pipelineName); err != nil { + return nil, err + } + + var pipelineSpec tektonapi.PipelineSpec + + if v1beta1Pipeline, ok := obj.(tektonapi_v1beta1.PipelineObject); ok { + v1beta1PipelineSpec := v1beta1Pipeline.PipelineSpec() + log.Info("Converting from v1beta1 to v1", "PipelineName", pipelineName, "Bundle", bundleUri) + err := v1beta1PipelineSpec.ConvertTo(ctx, &pipelineSpec, &metav1.ObjectMeta{}) + if err != nil { + return nil, boerrors.NewBuildOpError( + boerrors.EPipelineConversionFailed, + fmt.Errorf("pipeline %s from bundle %s: failed to convert from v1beta1 to v1: %w", pipelineName, bundleUri, err), + ) + } + } else if v1Pipeline, ok := obj.(*tektonapi.Pipeline); ok { + pipelineSpec = v1Pipeline.PipelineSpec() + } else { + return nil, boerrors.NewBuildOpError( + boerrors.EPipelineRetrievalFailed, + fmt.Errorf("failed to extract pipeline %s from bundle %s", pipelineName, bundleUri), + ) + } + + return &pipelineSpec, nil +} + +// GetBuildPipelineFromComponentAnnotation parses pipeline annotation on component and returns build pipeline +func (r *ComponentBuildReconciler) GetBuildPipelineFromComponentAnnotation(ctx context.Context, component *appstudiov1alpha1.Component) (*tektonapi.PipelineRef, error) { + buildPipeline, err := readBuildPipelineAnnotation(component) + if err != nil { + return nil, err + } + if buildPipeline == nil { + err := fmt.Errorf("missing or empty pipeline annotation: %s, will add default one to the component", component.Annotations[defaultBuildPipelineAnnotation]) + return nil, boerrors.NewBuildOpError(boerrors.EMissingPipelineAnnotation, err) + } + if buildPipeline.Bundle == "" || buildPipeline.Name == "" { + err = fmt.Errorf("missing name or bundle in pipeline annotation: name=%s bundle=%s", buildPipeline.Name, buildPipeline.Bundle) + return nil, boerrors.NewBuildOpError(boerrors.EWrongPipelineAnnotation, err) + } + finalBundle := buildPipeline.Bundle + + if buildPipeline.Bundle == "latest" { + pipelinesConfigMap := &corev1.ConfigMap{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: buildPipelineConfigMapResourceName, Namespace: BuildServiceNamespaceName}, pipelinesConfigMap); err != nil { + if errors.IsNotFound(err) { + return nil, boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotDefined, err) + } + return nil, err + } + + buildPipelineData := &pipelineConfig{} + if err := yaml.Unmarshal([]byte(pipelinesConfigMap.Data[buildPipelineConfigName]), buildPipelineData); err != nil { + return nil, boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotValid, err) + } + + for _, pipeline := range buildPipelineData.Pipelines { + if pipeline.Name == buildPipeline.Name { + finalBundle = pipeline.Bundle + break + } + } + + // requested pipeline was not found in configMap + if finalBundle == "latest" { + err = fmt.Errorf("invalid pipeline name in pipeline annotation: name=%s", buildPipeline.Name) + return nil, boerrors.NewBuildOpError(boerrors.EBuildPipelineInvalid, err) + } + } + + pipelineRef := &tektonapi.PipelineRef{ + ResolverRef: tektonapi.ResolverRef{ + Resolver: "bundles", + Params: []tektonapi.Param{ + {Name: "name", Value: *tektonapi.NewStructuredValues(buildPipeline.Name)}, + {Name: "bundle", Value: *tektonapi.NewStructuredValues(finalBundle)}, + {Name: "kind", Value: *tektonapi.NewStructuredValues("pipeline")}, + }, + }, + } + return pipelineRef, nil +} + +func readBuildPipelineAnnotation(component *appstudiov1alpha1.Component) (*BuildPipeline, error) { + if component.Annotations == nil { + return nil, nil + } + + requestedPipeline, requestedPipelineExists := component.Annotations[defaultBuildPipelineAnnotation] + if requestedPipelineExists && requestedPipeline != "" { + buildPipeline := &BuildPipeline{} + buildPipelineBytes := []byte(requestedPipeline) + + if err := json.Unmarshal(buildPipelineBytes, buildPipeline); err != nil { + return nil, boerrors.NewBuildOpError(boerrors.EFailedToParsePipelineAnnotation, err) + } + return buildPipeline, nil + } + return nil, nil +} + +// SetDefaultBuildPipelineComponentAnnotation sets default build pipeline to component pipeline annotation +func (r *ComponentBuildReconciler) SetDefaultBuildPipelineComponentAnnotation(ctx context.Context, component *appstudiov1alpha1.Component) error { + log := ctrllog.FromContext(ctx) + pipelinesConfigMap := &corev1.ConfigMap{} + + if err := r.Client.Get(ctx, types.NamespacedName{Name: buildPipelineConfigMapResourceName, Namespace: BuildServiceNamespaceName}, pipelinesConfigMap); err != nil { + if errors.IsNotFound(err) { + return boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotDefined, err) + } + return err + } + + buildPipelineData := &pipelineConfig{} + if err := yaml.Unmarshal([]byte(pipelinesConfigMap.Data[buildPipelineConfigName]), buildPipelineData); err != nil { + return boerrors.NewBuildOpError(boerrors.EBuildPipelineConfigNotValid, err) + } + + pipelineAnnotation := fmt.Sprintf("{\"name\":\"%s\",\"bundle\":\"%s\"}", buildPipelineData.DefaultPipelineName, "latest") + if component.Annotations == nil { + component.Annotations = make(map[string]string) + } + component.Annotations[defaultBuildPipelineAnnotation] = pipelineAnnotation + + if err := r.Client.Update(ctx, component); err != nil { + log.Error(err, fmt.Sprintf("failed to update component with default pipeline annotation %s", defaultBuildPipelineAnnotation)) + return err + } + log.Info(fmt.Sprintf("updated component with default pipeline annotation %s", defaultBuildPipelineAnnotation)) + return nil +} + +// generatePaCPipelineRunForComponent returns pipeline run definition to build component source with. +// Generated pipeline run contains placeholders that are expanded by Pipeline-as-Code. +func generatePaCPipelineRunForComponent( + component *appstudiov1alpha1.Component, + pipelineSpec *tektonapi.PipelineSpec, + pacTargetBranch string, + gitClient gp.GitProviderClient, + onPull bool) (*tektonapi.PipelineRun, error) { + + if pacTargetBranch == "" { + return nil, fmt.Errorf("target branch can't be empty for generating PaC PipelineRun for: %v", component) + } + pipelineCelExpression, err := generateCelExpressionForPipeline(component, gitClient, pacTargetBranch, onPull) + if err != nil { + return nil, fmt.Errorf("failed to generate cel expression for pipeline: %w", err) + } + repoUrl := component.Spec.Source.GitSource.URL + + annotations := map[string]string{ + "pipelinesascode.tekton.dev/max-keep-runs": "3", + "build.appstudio.redhat.com/target_branch": "{{target_branch}}", + pacCelExpressionAnnotationName: pipelineCelExpression, + gitCommitShaAnnotationName: "{{revision}}", + gitRepoAtShaAnnotationName: gitClient.GetBrowseRepositoryAtShaLink(repoUrl, "{{revision}}"), + } + labels := map[string]string{ + ApplicationNameLabelName: component.Spec.Application, + ComponentNameLabelName: component.Name, + "pipelines.appstudio.openshift.io/type": "build", + } + + imageRepo := getContainerImageRepositoryForComponent(component) + + var pipelineName string + var proposedImage string + if onPull { + annotations["build.appstudio.redhat.com/pull_request_number"] = "{{pull_request_number}}" + pipelineName = component.Name + pipelineRunOnPRSuffix + proposedImage = imageRepo + ":on-pr-{{revision}}" + } else { + pipelineName = component.Name + pipelineRunOnPushSuffix + proposedImage = imageRepo + ":{{revision}}" + } + + params := []tektonapi.Param{ + {Name: "git-url", Value: tektonapi.ParamValue{Type: "string", StringVal: "{{source_url}}"}}, + {Name: "revision", Value: tektonapi.ParamValue{Type: "string", StringVal: "{{revision}}"}}, + {Name: "output-image", Value: tektonapi.ParamValue{Type: "string", StringVal: proposedImage}}, + } + if onPull { + prImageExpiration := os.Getenv(PipelineRunOnPRExpirationEnvVar) + if prImageExpiration == "" { + prImageExpiration = PipelineRunOnPRExpirationDefault + } + params = append(params, tektonapi.Param{Name: "image-expires-after", Value: tektonapi.ParamValue{Type: "string", StringVal: prImageExpiration}}) + } + + if component.Spec.Source.GitSource.DockerfileURL != "" { + params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: component.Spec.Source.GitSource.DockerfileURL}}) + } else { + params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: "Dockerfile"}}) + } + pathContext := getPathContext(component.Spec.Source.GitSource.Context, "") + if pathContext != "" { + params = append(params, tektonapi.Param{Name: "path-context", Value: tektonapi.ParamValue{Type: "string", StringVal: pathContext}}) + } + + pipelineRunWorkspaces := createWorkspaceBinding(pipelineSpec.Workspaces) + + pipelineRun := &tektonapi.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "PipelineRun", + APIVersion: "tekton.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: pipelineName, + Namespace: component.Namespace, + Labels: labels, + Annotations: annotations, + }, + Spec: tektonapi.PipelineRunSpec{ + PipelineSpec: pipelineSpec, + Params: params, + Workspaces: pipelineRunWorkspaces, + }, + } + + return pipelineRun, nil +} + +func getPathContext(gitContext, dockerfileContext string) string { + if gitContext == "" && dockerfileContext == "" { + return "" + } + separator := string(filepath.Separator) + path := filepath.Join(gitContext, dockerfileContext) + path = filepath.Clean(path) + path = strings.TrimPrefix(path, separator) + return path +} + +func createWorkspaceBinding(pipelineWorkspaces []tektonapi.PipelineWorkspaceDeclaration) []tektonapi.WorkspaceBinding { + pipelineRunWorkspaces := []tektonapi.WorkspaceBinding{} + for _, workspace := range pipelineWorkspaces { + switch workspace.Name { + case "workspace": + pipelineRunWorkspaces = append(pipelineRunWorkspaces, + tektonapi.WorkspaceBinding{ + Name: workspace.Name, + VolumeClaimTemplate: generateVolumeClaimTemplate(), + }) + case "git-auth": + pipelineRunWorkspaces = append(pipelineRunWorkspaces, + tektonapi.WorkspaceBinding{ + Name: workspace.Name, + Secret: &corev1.SecretVolumeSource{SecretName: "{{ git_auth_secret }}"}, + }) + } + } + return pipelineRunWorkspaces +} + +func generateVolumeClaimTemplate() *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + "ReadWriteOnce", + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + "storage": resource.MustParse("1Gi"), + }, + }, + }, + } +} + +// generateCelExpressionForPipeline generates value for pipelinesascode.tekton.dev/on-cel-expression annotation +// in order to have better flexibility with git events filtering. +// Examples of returned values: +// event == "push" && target_branch == "main" +// event == "pull_request" && target_branch == "my-branch" && ( "component-src-dir/***".pathChanged() || ".tekton/pipeline.yaml".pathChanged() || "dockerfiles/my-component/Dockerfile".pathChanged() ) +func generateCelExpressionForPipeline(component *appstudiov1alpha1.Component, gitClient gp.GitProviderClient, targetBranch string, onPull bool) (string, error) { + eventType := "push" + if onPull { + eventType = "pull_request" + } + eventCondition := fmt.Sprintf(`event == "%s"`, eventType) + + targetBranchCondition := fmt.Sprintf(`target_branch == "%s"`, targetBranch) + + // Set path changed event filtering only for Components that are stored within a directory of the git repository. + // Also, we have to rebuild everything on push events, so applying the filter only to pull request pipeline. + pathChangedSuffix := "" + if onPull && component.Spec.Source.GitSource.Context != "" && component.Spec.Source.GitSource.Context != "/" && component.Spec.Source.GitSource.Context != "./" && component.Spec.Source.GitSource.Context != "." { + contextDir := component.Spec.Source.GitSource.Context + if !strings.HasSuffix(contextDir, "/") { + contextDir += "/" + } + + // If a Dockerfile is defined for the Component, + // we should rebuild the Component if the Dockerfile has been changed. + dockerfilePathChangedSuffix := "" + dockerfile := component.Spec.Source.GitSource.DockerfileURL + if dockerfile != "" { + // Ignore dockerfile that is not stored in the same git repository but downloaded by an URL. + if !strings.Contains(dockerfile, "://") { + // dockerfile could be relative to the context directory or repository root. + // To avoid unessesary builds, it's required to pass absolute path to the Dockerfile. + repoUrl := component.Spec.Source.GitSource.URL + branch := component.Spec.Source.GitSource.Revision + dockerfilePath := contextDir + dockerfile + isDockerfileInContextDir, err := gitClient.IsFileExist(repoUrl, branch, dockerfilePath) + if err != nil { + return "", err + } + // If the Dockerfile is inside context directory, no changes to event filter needed. + if !isDockerfileInContextDir { + // Pipelines as Code doesn't match path if it starts from / + dockerfileAbsolutePath := strings.TrimPrefix(dockerfile, "/") + dockerfilePathChangedSuffix = fmt.Sprintf(`|| "%s".pathChanged() `, dockerfileAbsolutePath) + } + } + } + + pullPipelineFileName := component.Name + "-" + pipelineRunOnPRFilename + pathChangedSuffix = fmt.Sprintf(` && ( "%s***".pathChanged() || ".tekton/%s".pathChanged() %s)`, contextDir, pullPipelineFileName, dockerfilePathChangedSuffix) + } + + return fmt.Sprintf("%s && %s%s", eventCondition, targetBranchCondition, pathChangedSuffix), nil +} + +func getContainerImageRepositoryForComponent(component *appstudiov1alpha1.Component) string { + if component.Spec.ContainerImage != "" { + return getContainerImageRepository(component.Spec.ContainerImage) + } + imageRepo, _, err := getComponentImageRepoAndSecretNameFromImageAnnotation(component) + if err == nil && imageRepo != "" { + return imageRepo + } + return "" +} + +// getContainerImageRepository removes tag or SHA has from container image reference +func getContainerImageRepository(image string) string { + if strings.Contains(image, "@") { + // registry.io/user/image@sha256:586ab...d59a + return strings.Split(image, "@")[0] + } + // registry.io/user/image:tag + return strings.Split(image, ":")[0] +} + +// getComponentImageRepoAndSecretNameFromImageAnnotation parses image.redhat.com/image annotation +// for image repository and secret name to access it. +// If image.redhat.com/image is not set, the procedure returns empty values. +func getComponentImageRepoAndSecretNameFromImageAnnotation(component *appstudiov1alpha1.Component) (string, string, error) { + type RepositoryInfo struct { + Image string `json:"image"` + Secret string `json:"secret"` + } + + var repoInfo RepositoryInfo + if imageRepoDataJson, exists := component.Annotations[ImageRepoAnnotationName]; exists { + if err := json.Unmarshal([]byte(imageRepoDataJson), &repoInfo); err != nil { + return "", "", boerrors.NewBuildOpError(boerrors.EFailedToParseImageAnnotation, err) + } + return repoInfo.Image, repoInfo.Secret, nil + } + return "", "", nil +} + +func getPipelineNameAndBundle(pipelineRef *tektonapi.PipelineRef) (string, string, error) { + if pipelineRef.Resolver != "" && pipelineRef.Resolver != "bundles" { + return "", "", boerrors.NewBuildOpError( + boerrors.EUnsupportedPipelineRef, + fmt.Errorf("unsupported Tekton resolver %q", pipelineRef.Resolver), + ) + } + + name := pipelineRef.Name + var bundle string + + for _, param := range pipelineRef.Params { + switch param.Name { + case "name": + name = param.Value.StringVal + case "bundle": + bundle = param.Value.StringVal + } + } + + if name == "" || bundle == "" { + return "", "", boerrors.NewBuildOpError( + boerrors.EMissingParamsForBundleResolver, + fmt.Errorf("missing name or bundle in pipelineRef: name=%s bundle=%s", name, bundle), + ) + } + + return name, bundle, nil +} diff --git a/controllers/component_build_controller_secrets.go b/controllers/component_build_controller_secrets.go new file mode 100644 index 00000000..388c863a --- /dev/null +++ b/controllers/component_build_controller_secrets.go @@ -0,0 +1,206 @@ +/* +Copyright 2021-2024 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "regexp" + "strings" + + appstudiov1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/konflux-ci/build-service/pkg/boerrors" + . "github.com/konflux-ci/build-service/pkg/common" + "github.com/konflux-ci/build-service/pkg/git" + l "github.com/konflux-ci/build-service/pkg/logs" +) + +// ensureIncomingSecret is ensuring that incoming secret for PaC trigger exists +// if secret doesn't exists it will create it and also add repository as owner +// Returns: +// pointer to secret object +// bool which indicates if reconcile is required (which is required when we just created secret) +func (r *ComponentBuildReconciler) ensureIncomingSecret(ctx context.Context, component *appstudiov1alpha1.Component) (*corev1.Secret, bool, error) { + log := ctrllog.FromContext(ctx) + + repository, err := r.findPaCRepositoryForComponent(ctx, component) + if err != nil { + return nil, false, err + } + + incomingSecretName := fmt.Sprintf("%s%s", repository.Name, pacIncomingSecretNameSuffix) + incomingSecretPassword := generatePaCWebhookSecretString() + incomingSecretData := map[string]string{ + pacIncomingSecretKey: incomingSecretPassword, + } + + secret := corev1.Secret{} + if err := r.Client.Get(ctx, types.NamespacedName{Namespace: component.Namespace, Name: incomingSecretName}, &secret); err != nil { + if !errors.IsNotFound(err) { + log.Error(err, "failed to get incoming secret", l.Action, l.ActionView) + return nil, false, err + } + // Create incoming secret + secret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: incomingSecretName, + Namespace: component.Namespace, + }, + Type: corev1.SecretTypeOpaque, + StringData: incomingSecretData, + } + + if err := controllerutil.SetOwnerReference(repository, &secret, r.Scheme); err != nil { + log.Error(err, "failed to set owner for incoming secret") + return nil, false, err + } + + if err := r.Client.Create(ctx, &secret); err != nil { + log.Error(err, "failed to create incoming secret", l.Action, l.ActionAdd) + return nil, false, err + } + + log.Info("incoming secret created") + return &secret, true, nil + } + return &secret, false, nil +} + +func (r *ComponentBuildReconciler) lookupPaCSecret(ctx context.Context, component *appstudiov1alpha1.Component, gitProvider string) (*corev1.Secret, error) { + log := ctrllog.FromContext(ctx) + + scmComponent, err := git.NewScmComponent(gitProvider, component.Spec.Source.GitSource.URL, component.Spec.Source.GitSource.Revision, component.Name, component.Namespace) + if err != nil { + return nil, err + } + // find the best matching secret, starting from SSH type + secret, err := r.CredentialProvider.LookupSecret(ctx, scmComponent, corev1.SecretTypeSSHAuth) + if err != nil && !boerrors.IsBuildOpError(err, boerrors.EComponentGitSecretMissing) { + log.Error(err, "failed to get Pipelines as Code SSH secret", "scmComponent", scmComponent) + return nil, err + } + if secret != nil { + return secret, nil + } + // find the best matching secret, starting from BasicAuth type + secret, err = r.CredentialProvider.LookupSecret(ctx, scmComponent, corev1.SecretTypeBasicAuth) + if err != nil && !boerrors.IsBuildOpError(err, boerrors.EComponentGitSecretMissing) { + log.Error(err, "failed to get Pipelines as Code BasicAuth secret", "scmComponent", scmComponent) + return nil, err + } + if secret != nil { + return secret, nil + } + + // No SCM secrets found in the component namespace, fall back to the global configuration + if gitProvider == "github" { + return r.lookupGHAppSecret(ctx) + } else { + return nil, boerrors.NewBuildOpError(boerrors.EPaCSecretNotFound, fmt.Errorf("no matching Pipelines as Code secrets found in %s namespace", component.Namespace)) + } + +} + +func (r *ComponentBuildReconciler) lookupGHAppSecret(ctx context.Context) (*corev1.Secret, error) { + pacSecret := &corev1.Secret{} + globalPaCSecretKey := types.NamespacedName{Namespace: BuildServiceNamespaceName, Name: PipelinesAsCodeGitHubAppSecretName} + if err := r.Client.Get(ctx, globalPaCSecretKey, pacSecret); err != nil { + if !errors.IsNotFound(err) { + r.EventRecorder.Event(pacSecret, "Warning", "ErrorReadingPaCSecret", err.Error()) + return nil, fmt.Errorf("failed to get Pipelines as Code secret in %s namespace: %w", globalPaCSecretKey.Namespace, err) + } + + r.EventRecorder.Event(pacSecret, "Warning", "PaCSecretNotFound", err.Error()) + // Do not trigger a new reconcile. The PaC secret must be created first. + return nil, boerrors.NewBuildOpError(boerrors.EPaCSecretNotFound, fmt.Errorf(" Pipelines as Code secret not found in %s ", globalPaCSecretKey.Namespace)) + } + return pacSecret, nil +} + +// Returns webhook secret for given component. +// Generates the webhook secret and saves it in the k8s secret if it doesn't exist. +func (r *ComponentBuildReconciler) ensureWebhookSecret(ctx context.Context, component *appstudiov1alpha1.Component) (string, error) { + log := ctrllog.FromContext(ctx) + + webhookSecretsSecret := &corev1.Secret{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: pipelinesAsCodeWebhooksSecretName, Namespace: component.GetNamespace()}, webhookSecretsSecret); err != nil { + if errors.IsNotFound(err) { + webhookSecretsSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pipelinesAsCodeWebhooksSecretName, + Namespace: component.GetNamespace(), + Labels: map[string]string{ + PartOfLabelName: PartOfAppStudioLabelValue, + }, + }, + } + if err := r.Client.Create(ctx, webhookSecretsSecret); err != nil { + log.Error(err, "failed to create webhooks secrets secret", l.Action, l.ActionAdd) + return "", err + } + return r.ensureWebhookSecret(ctx, component) + } + + log.Error(err, "failed to get webhook secrets secret", l.Action, l.ActionView) + return "", err + } + + componentWebhookSecretKey := getWebhookSecretKeyForComponent(*component) + if _, exists := webhookSecretsSecret.Data[componentWebhookSecretKey]; exists { + // The webhook secret already exists. Use single secret for the same repository. + return string(webhookSecretsSecret.Data[componentWebhookSecretKey]), nil + } + + webhookSecretString := generatePaCWebhookSecretString() + + if webhookSecretsSecret.Data == nil { + webhookSecretsSecret.Data = make(map[string][]byte) + } + webhookSecretsSecret.Data[componentWebhookSecretKey] = []byte(webhookSecretString) + if err := r.Client.Update(ctx, webhookSecretsSecret); err != nil { + log.Error(err, "failed to update webhook secrets secret", l.Action, l.ActionUpdate) + return "", err + } + + return webhookSecretString, nil +} + +func getWebhookSecretKeyForComponent(component appstudiov1alpha1.Component) string { + gitRepoUrl := strings.TrimSuffix(component.Spec.Source.GitSource.URL, ".git") + + notAllowedCharRegex, _ := regexp.Compile("[^-._a-zA-Z0-9]{1}") + return notAllowedCharRegex.ReplaceAllString(gitRepoUrl, "_") +} + +// generatePaCWebhookSecretString generates string alike openssl rand -hex 20 +func generatePaCWebhookSecretString() string { + length := 20 // in bytes + tokenBytes := make([]byte, length) + if _, err := rand.Read(tokenBytes); err != nil { + panic("Failed to read from random generator") + } + return hex.EncodeToString(tokenBytes) +} diff --git a/controllers/suite_util_test.go b/controllers/suite_util_test.go index adec4a1e..65a0fed4 100644 --- a/controllers/suite_util_test.go +++ b/controllers/suite_util_test.go @@ -35,7 +35,7 @@ import ( appstudiov1alpha1 "github.com/konflux-ci/application-api/api/v1alpha1" . "github.com/konflux-ci/build-service/pkg/common" - "gopkg.in/yaml.v2" + "sigs.k8s.io/yaml" ) const (