diff --git a/docs/build_pipeline_controller.md b/docs/build_pipeline_controller.md index 92079fa1a..2594e6643 100644 --- a/docs/build_pipeline_controller.md +++ b/docs/build_pipeline_controller.md @@ -12,6 +12,7 @@ flowchart TD %% Node definitions predicate((PREDICATE:
Filter events related to
PipelineRuns)) new_pipeline_run{Pipeline created?} +new_pipeline_run_without_prgroup{PR group is added to pipelineRun metadata?} get_pipeline_run{Pipeline updated?} failed_pipeline_run{Pipeline failed?} finalizer_exists{Does the finalizer already exist?} @@ -24,15 +25,20 @@ add_finalizer(Add finalizer to build PLR) remove_finalizer(Remove finalizer from build PLR) error[Return error] continue[Continue processing] +update_metadata(add PR group info to build pipelineRun metadata) %% Node connections predicate --> get_pipeline_run predicate --> new_pipeline_run +predicate --> new_pipeline_run_without_prgroup predicate --> failed_pipeline_run new_pipeline_run --Yes--> finalizer_exists finalizer_exists --No--> add_finalizer add_finalizer --> continue failed_pipeline_run --Yes --> remove_finalizer +new_pipeline_run_without_prgroup --No --> update_metadata +new_pipeline_run_without_prgroup --Yes --> continue +update_metadata --> continue get_pipeline_run --Yes --> retrieve_associated_entity get_pipeline_run --No --> error retrieve_associated_entity --No --> error diff --git a/gitops/snapshot.go b/gitops/snapshot.go index 50f95a479..92ba6277c 100644 --- a/gitops/snapshot.go +++ b/gitops/snapshot.go @@ -76,6 +76,15 @@ const ( // SnapshotStatusReportAnnotation contains metadata of tests related to status reporting to git provider SnapshotStatusReportAnnotation = "test.appstudio.openshift.io/git-reporter-status" + // PRGroupAnnotation contains the pr group name + PRGroupAnnotation = "test.appstudio.openshift.io/pr-group" + + // PRGroupHashLabel contains the pr group name in sha format + PRGroupHashLabel = "test.appstudio.openshift.io/pr-group-sha" + + // BuildPipelineRunStartTime contains the start time of build pipelineRun + BuildPipelineRunStartTime = "test.appstudio.openshift.io/pipelinerunstarttime" + // BuildPipelineRunPrefix contains the build pipeline run related labels and annotations BuildPipelineRunPrefix = "build.appstudio" diff --git a/internal/controller/buildpipeline/buildpipeline_adapter.go b/internal/controller/buildpipeline/buildpipeline_adapter.go index cd247d4e8..4d4b4c985 100644 --- a/internal/controller/buildpipeline/buildpipeline_adapter.go +++ b/internal/controller/buildpipeline/buildpipeline_adapter.go @@ -30,6 +30,7 @@ import ( "github.com/konflux-ci/integration-service/loader" "github.com/konflux-ci/integration-service/tekton" "github.com/konflux-ci/operator-toolkit/controller" + "github.com/konflux-ci/operator-toolkit/metadata" applicationapiv1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1" tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -149,6 +150,33 @@ func (a *Adapter) EnsurePipelineIsFinalized() (controller.OperationResult, error return controller.ContinueProcessing() } +// EnsurePRGroupAnnotated is an operation that will ensure that the pr group info +// is added to build pipelineRun metadata label and annotation once it is created, +// then these label and annotation will be copied to component snapshot when it is created +func (a *Adapter) EnsurePRGroupAnnotated() (controller.OperationResult, error) { + if metadata.HasLabel(a.pipelineRun, gitops.PRGroupHashLabel) && metadata.HasAnnotation(a.pipelineRun, gitops.PRGroupAnnotation) { + a.logger.Info("build pipelineRun has had pr group info in metadata, no need to update") + return controller.ContinueProcessing() + } + + err := a.addPRGroupToBuildPLRMetadata(a.pipelineRun) + if err != nil { + if errors.IsNotFound(err) { + a.logger.Error(err, "failed to add pr group info to build pipelineRun metadata due to notfound pipelineRun") + return controller.StopProcessing() + } else { + a.logger.Error(err, "failed to add pr group info to build pipelineRun metadata") + return controller.RequeueWithError(err) + } + + } + + a.logger.LogAuditEvent("pr group info is updated to build pipelineRun metadata", a.pipelineRun, h.LogActionUpdate, + "pipelineRun.Name", a.pipelineRun.Name) + + return controller.ContinueProcessing() +} + // getImagePullSpecFromPipelineRun gets the full image pullspec from the given build PipelineRun, // In case the Image pullspec can't be composed, an error will be returned. func (a *Adapter) getImagePullSpecFromPipelineRun(pipelineRun *tektonv1.PipelineRun) (string, error) { @@ -218,6 +246,10 @@ func (a *Adapter) prepareSnapshotForPipelineRun(pipelineRun *tektonv1.PipelineRu snapshot.Labels[gitops.BuildPipelineRunFinishTimeLabel] = strconv.FormatInt(time.Now().Unix(), 10) } + if pipelineRun.Status.StartTime != nil { + snapshot.Annotations[gitops.BuildPipelineRunStartTime] = strconv.FormatInt(pipelineRun.Status.StartTime.Time.Unix(), 10) + } + return snapshot, nil } @@ -329,3 +361,29 @@ func (a *Adapter) updatePipelineRunWithCustomizedError(canRemoveFinalizer *bool, return controller.RequeueOnErrorOrContinue(cerr) } + +// addPRGroupToBuildPLRMetadata will add pr-group info gotten from souce-branch to annotation +// and also the string in sha format to metadata label +func (a *Adapter) addPRGroupToBuildPLRMetadata(pipelineRun *tektonv1.PipelineRun) error { + prGroupName := tekton.GetPRGroupNameFromBuildPLR(pipelineRun) + if prGroupName != "" { + prGroupHashName := tekton.GenerateSHA(prGroupName) + + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + var err error + pipelineRun, err = a.loader.GetPipelineRun(a.context, a.client, pipelineRun.Name, pipelineRun.Namespace) + if err != nil { + return err + } + + patch := client.MergeFrom(pipelineRun.DeepCopy()) + + _ = metadata.SetAnnotation(&pipelineRun.ObjectMeta, gitops.PRGroupAnnotation, prGroupName) + _ = metadata.SetLabel(&pipelineRun.ObjectMeta, gitops.PRGroupHashLabel, prGroupHashName) + + return a.client.Patch(a.context, pipelineRun, patch) + }) + } + a.logger.Info("can't find source branch info in build PLR, not need to update build pipelineRun metadata") + return nil +} diff --git a/internal/controller/buildpipeline/buildpipeline_adapter_test.go b/internal/controller/buildpipeline/buildpipeline_adapter_test.go index 2f790b46f..0d43bd87b 100644 --- a/internal/controller/buildpipeline/buildpipeline_adapter_test.go +++ b/internal/controller/buildpipeline/buildpipeline_adapter_test.go @@ -29,6 +29,7 @@ import ( "github.com/konflux-ci/integration-service/helpers" "github.com/konflux-ci/integration-service/loader" "github.com/konflux-ci/integration-service/tekton" + "github.com/konflux-ci/operator-toolkit/metadata" "knative.dev/pkg/apis" v1 "knative.dev/pkg/apis/duck/v1" @@ -267,6 +268,8 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { "build.appstudio.openshift.io/repo": "https://github.com/devfile-samples/devfile-sample-go-basic?rev=c713067b0e65fb3de50d1f7c457eb51c2ab0dbb0", "foo": "bar", "chains.tekton.dev/signed": "true", + "pipelinesascode.tekton.dev/source-branch": "sourceBranch", + "pipelinesascode.tekton.dev/url-org": "redhat", }, }, Spec: tektonv1.PipelineRunSpec{ @@ -317,6 +320,7 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { Value: *tektonv1.NewStructuredValues(SampleCommit), }, }, + StartTime: &metav1.Time{Time: time.Now()}, }, Status: v1.Status{ Conditions: v1.Conditions{ @@ -410,6 +414,8 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { Expect(expectedSnapshot.Labels).NotTo(BeNil()) Expect(expectedSnapshot.Labels).Should(HaveKeyWithValue(Equal(gitops.BuildPipelineRunNameLabel), Equal(buildPipelineRun.Name))) Expect(expectedSnapshot.Labels).Should(HaveKeyWithValue(Equal(gitops.ApplicationNameLabel), Equal(hasApp.Name))) + Expect(metadata.HasAnnotation(expectedSnapshot, gitops.BuildPipelineRunStartTime)).To(BeTrue()) + Expect(expectedSnapshot.Annotations[gitops.BuildPipelineRunStartTime]).NotTo(BeNil()) }) It("ensures that Labels and Annotations were copied to snapshot from pipelinerun", func() { @@ -1014,6 +1020,38 @@ var _ = Describe("Pipeline Adapter", Ordered, func() { }) + When("add pr group to the build pipelineRun annotations and labels", func() { + BeforeEach(func() { + adapter = NewAdapter(ctx, buildPipelineRun, hasComp, hasApp, logger, loader.NewMockLoader(), k8sClient) + }) + It("add pr group to the build pipelineRun annotations and labels", func() { + existingBuildPLR := new(tektonv1.PipelineRun) + err := k8sClient.Get(ctx, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, existingBuildPLR) + Expect(err).NotTo(HaveOccurred()) + Expect(metadata.HasAnnotation(existingBuildPLR, gitops.PRGroupAnnotation)).To(BeFalse()) + Expect(metadata.HasLabel(existingBuildPLR, gitops.PRGroupHashLabel)).To(BeFalse()) + + // Add label and annotation to PLR + result, err := adapter.EnsurePRGroupAnnotated() + Expect(err).NotTo(HaveOccurred()) + Expect(result.CancelRequest).To(BeFalse()) + Expect(result.RequeueRequest).To(BeFalse()) + + err = adapter.client.Get(adapter.context, types.NamespacedName{ + Namespace: buildPipelineRun.Namespace, + Name: buildPipelineRun.Name, + }, existingBuildPLR) + Expect(err).NotTo(HaveOccurred()) + Expect(metadata.HasAnnotation(existingBuildPLR, gitops.PRGroupAnnotation)).To(BeTrue()) + Expect(existingBuildPLR.Annotations).Should(HaveKeyWithValue(Equal(gitops.PRGroupAnnotation), Equal("sourceBranch"))) + Expect(metadata.HasLabel(existingBuildPLR, gitops.PRGroupHashLabel)).To(BeTrue()) + Expect(existingBuildPLR.Labels[gitops.PRGroupHashLabel]).NotTo(BeNil()) + }) + }) + When("running pipeline with deletion timestamp is processed", func() { var runningDeletingBuildPipeline *tektonv1.PipelineRun diff --git a/internal/controller/buildpipeline/buildpipeline_controller.go b/internal/controller/buildpipeline/buildpipeline_controller.go index 643bb42b3..0f7bab44a 100644 --- a/internal/controller/buildpipeline/buildpipeline_controller.go +++ b/internal/controller/buildpipeline/buildpipeline_controller.go @@ -117,6 +117,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return controller.ReconcileHandler([]controller.Operation{ adapter.EnsurePipelineIsFinalized, + adapter.EnsurePRGroupAnnotated, adapter.EnsureSnapshotExists, }) } @@ -124,6 +125,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu // AdapterInterface is an interface defining all the operations that should be defined in an Integration adapter. type AdapterInterface interface { EnsurePipelineIsFinalized() (controller.OperationResult, error) + EnsurePRGroupAnnotated() (controller.OperationResult, error) EnsureSnapshotExists() (controller.OperationResult, error) } diff --git a/tekton/build_pipeline.go b/tekton/build_pipeline.go index 9b73dcb29..4d933fd9a 100644 --- a/tekton/build_pipeline.go +++ b/tekton/build_pipeline.go @@ -18,8 +18,10 @@ package tekton import ( "context" + "crypto/sha256" "encoding/json" "fmt" + "strings" h "github.com/konflux-ci/integration-service/helpers" "github.com/konflux-ci/operator-toolkit/metadata" @@ -27,6 +29,20 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + // master branch in github/gitlab + MasterBranch = "master" + + // main branch in github/gitlab + MainBranch = "main" + + // PipelineAsCodeSourceBranchAnnotation is the branch name of the the pull request is created from + PipelineAsCodeSourceBranchAnnotation = "pipelinesascode.tekton.dev/source-branch" + + // PipelineAsCodeSourceRepoOrg is the repo org build PLR is triggered by + PipelineAsCodeSourceRepoOrg = "pipelinesascode.tekton.dev/url-org" +) + // AnnotateBuildPipelineRun sets annotation for a build pipelineRun in defined context and returns that pipeline func AnnotateBuildPipelineRun(ctx context.Context, pipelineRun *tektonv1.PipelineRun, key, value string, cl client.Client) error { patch := client.MergeFrom(pipelineRun.DeepCopy()) @@ -40,6 +56,19 @@ func AnnotateBuildPipelineRun(ctx context.Context, pipelineRun *tektonv1.Pipelin return nil } +// LabelBuildPipelineRun sets annotation for a build pipelineRun in defined context and returns that pipeline +func LabelBuildPipelineRun(ctx context.Context, pipelineRun *tektonv1.PipelineRun, key, value string, cl client.Client) error { + patch := client.MergeFrom(pipelineRun.DeepCopy()) + + _ = metadata.SetLabel(&pipelineRun.ObjectMeta, key, value) + + err := cl.Patch(ctx, pipelineRun, patch) + if err != nil { + return err + } + return nil +} + // AnnotateBuildPipelineRunWithCreateSnapshotAnnotation sets annotation test.appstudio.openshift.io/create-snapshot-status to build pipelineRun with // a message that reflects either success or failure for creating a snapshot func AnnotateBuildPipelineRunWithCreateSnapshotAnnotation(ctx context.Context, pipelineRun *tektonv1.PipelineRun, cl client.Client, ensureSnapshotExistsErr error) error { @@ -67,3 +96,21 @@ func AnnotateBuildPipelineRunWithCreateSnapshotAnnotation(ctx context.Context, p } return AnnotateBuildPipelineRun(ctx, pipelineRun, h.CreateSnapshotAnnotationName, string(jsonResult), cl) } + +// GetPRGroupNameFromBuildPLR gets the PR group from the substring before @ from +// the source-branch pac annotation, for main, it generate PR group with {source-branch}-{url-org} +func GetPRGroupNameFromBuildPLR(pipelineRun *tektonv1.PipelineRun) string { + if prGroup, found := pipelineRun.ObjectMeta.Annotations[PipelineAsCodeSourceBranchAnnotation]; found { + if prGroup == MainBranch || prGroup == MasterBranch && metadata.HasAnnotation(pipelineRun, PipelineAsCodeSourceRepoOrg) { + prGroup = prGroup + "-" + pipelineRun.ObjectMeta.Annotations[PipelineAsCodeSourceRepoOrg] + } + return strings.Split(prGroup, "@")[0] + } + return "" +} + +// GenerateSHA generate a 63 charactors sha string used by pipelineRun and snapshot label +func GenerateSHA(str string) string { + hash := sha256.Sum256([]byte(str)) + return fmt.Sprintf("%x", hash)[0:62] +} diff --git a/tekton/build_pipeline_test.go b/tekton/build_pipeline_test.go new file mode 100644 index 000000000..67ee2cc6b --- /dev/null +++ b/tekton/build_pipeline_test.go @@ -0,0 +1,133 @@ +/* +Copyright 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 tekton_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "knative.dev/pkg/apis" + v1 "knative.dev/pkg/apis/duck/v1" + "time" + + tekton "github.com/konflux-ci/integration-service/tekton" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("build pipeline", func() { + + var ( + buildPipelineRun *tektonv1.PipelineRun + ) + + BeforeEach(func() { + buildPipelineRun = &tektonv1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelinerun-build-sample", + Namespace: "default", + Labels: map[string]string{ + "pipelines.appstudio.openshift.io/type": "build", + "pipelines.openshift.io/used-by": "build-cloud", + "pipelines.openshift.io/runtime": "nodejs", + "pipelines.openshift.io/strategy": "s2i", + "appstudio.openshift.io/component": "component-sample", + "build.appstudio.redhat.com/target_branch": "main", + "pipelinesascode.tekton.dev/event-type": "pull_request", + }, + Annotations: map[string]string{ + "appstudio.redhat.com/updateComponentOnSuccess": "false", + "pipelinesascode.tekton.dev/on-target-branch": "[main,master]", + "build.appstudio.openshift.io/repo": "https://github.com/devfile-samples/devfile-sample-go-basic?rev=c713067b0e65fb3de50d1f7c457eb51c2ab0dbb0", + "foo": "bar", + "chains.tekton.dev/signed": "true", + "pipelinesascode.tekton.dev/source-branch": "sourceBranch", + "pipelinesascode.tekton.dev/url-org": "redhat", + }, + }, + Spec: tektonv1.PipelineRunSpec{ + PipelineRef: &tektonv1.PipelineRef{ + Name: "build-pipeline-pass", + ResolverRef: tektonv1.ResolverRef{ + Resolver: "bundle", + Params: tektonv1.Params{ + {Name: "bundle", + Value: tektonv1.ParamValue{Type: "string", StringVal: "quay.io/redhat-appstudio/example-tekton-bundle:test"}, + }, + {Name: "name", + Value: tektonv1.ParamValue{Type: "string", StringVal: "test-task"}, + }, + }, + }, + }, + Params: []tektonv1.Param{ + { + Name: "output-image", + Value: tektonv1.ParamValue{ + Type: tektonv1.ParamTypeString, + StringVal: "quay.io/redhat-appstudio/example-tekton-bundle:test", + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, buildPipelineRun)).Should(Succeed()) + + buildPipelineRun.Status = tektonv1.PipelineRunStatus{ + PipelineRunStatusFields: tektonv1.PipelineRunStatusFields{ + StartTime: &metav1.Time{Time: time.Now()}, + }, + Status: v1.Status{ + Conditions: v1.Conditions{ + apis.Condition{ + Reason: "Completed", + Status: "True", + Type: apis.ConditionSucceeded, + }, + }, + }, + } + Expect(k8sClient.Status().Update(ctx, buildPipelineRun)).Should(Succeed()) + }) + + AfterEach(func() { + err := k8sClient.Delete(ctx, buildPipelineRun) + Expect(err == nil || k8serrors.IsNotFound(err)).To(BeTrue()) + }) + + Context("When a build pipelineRun exists", func() { + It("can get PR group from build pipelineRun", func() { + prGroup := tekton.GetPRGroupNameFromBuildPLR(buildPipelineRun) + Expect(prGroup).To(Equal("sourceBranch")) + Expect(tekton.GenerateSHA(prGroup)).NotTo(BeNil()) + }) + + It("can get PR group from build pipelineRun is source branch is main", func() { + buildPipelineRun.Annotations[tekton.PipelineAsCodeSourceBranchAnnotation] = "main" + prGroup := tekton.GetPRGroupNameFromBuildPLR(buildPipelineRun) + Expect(prGroup).To(Equal("main-redhat")) + Expect(tekton.GenerateSHA(prGroup)).NotTo(BeNil()) + }) + + It("can get PR group from build pipelineRun is source branch has @ charactor", func() { + buildPipelineRun.Annotations[tekton.PipelineAsCodeSourceBranchAnnotation] = "myfeature@change1" + prGroup := tekton.GetPRGroupNameFromBuildPLR(buildPipelineRun) + Expect(prGroup).To(Equal("myfeature")) + Expect(tekton.GenerateSHA(prGroup)).NotTo(BeNil()) + }) + }) +})