From 96c5f72827c9e865f6ca6cbe802f1630ac18c2be Mon Sep 17 00:00:00 2001 From: Robert Cerven Date: Tue, 30 Apr 2024 21:41:48 +0200 Subject: [PATCH] Consume default build pipeline bundle from Component annotation STONEBLD-2278 Signed-off-by: Robert Cerven --- controllers/component_build_controller.go | 56 ++- .../component_build_controller_common.go | 46 +++ controllers/component_build_controller_pac.go | 45 ++- ...component_build_controller_simple_build.go | 45 ++- .../component_build_controller_test.go | 163 ++++++++ .../component_build_controller_unit_test.go | 373 +++++++++++++++++- controllers/suite_util_test.go | 21 + pkg/boerrors/perror.go | 3 + 8 files changed, 713 insertions(+), 39 deletions(-) diff --git a/controllers/component_build_controller.go b/controllers/component_build_controller.go index 5cb56f65..b4042565 100644 --- a/controllers/component_build_controller.go +++ b/controllers/component_build_controller.go @@ -70,6 +70,7 @@ const ( buildPipelineServiceAccountName = "appstudio-pipeline" buildPipelineSelectorResourceName = "build-pipeline-selector" + defaultBuildPipelineAnnotation = "build.appstudio.openshift.io/pipeline" ) type BuildStatus struct { @@ -250,15 +251,46 @@ func (r *ComponentBuildReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, nil } - // Ensure devfile model is set - if component.Status.Devfile == "" { - // The Component has been just created. - // Component controller (from Application Service) must set devfile model, wait for it. - log.Info("Waiting for devfile model in component") - // Do not requeue as after model update a new update event will trigger a new reconcile + pipelineRef, err := GetBuildPipelineFromComponentAnnotation(&component) + if err != nil { + log.Error(err, fmt.Sprintf("Failed to read %s annotation on component %s", defaultBuildPipelineAnnotation, component.Name), l.Action, l.ActionView) + + buildStatus := readBuildStatus(&component) + // when reading pipeline annotation fails, we should end reconcile, unless transient error + if boErr, ok := err.(*boerrors.BuildOpError); ok && boErr.IsPersistent() { + buildStatus.Message = fmt.Sprintf("%d: %s", err.(*boerrors.BuildOpError).GetErrorId(), err.(*boerrors.BuildOpError).ShortError()) + } else { + // transient error, retry + return ctrl.Result{}, err + } + writeBuildStatus(&component, buildStatus) + delete(component.Annotations, BuildRequestAnnotationName) + + if err := r.Client.Update(ctx, &component); err != nil { + log.Error(err, fmt.Sprintf("failed to update component after wrong %s annotation", defaultBuildPipelineAnnotation)) + return ctrl.Result{}, err + } + log.Info(fmt.Sprintf("updated component after wrong %s annotation", defaultBuildPipelineAnnotation)) + r.WaitForCacheUpdate(ctx, req.NamespacedName, &component) + return ctrl.Result{}, nil } + // when we are using already pipeline annotation we don't need anymore devfile + if pipelineRef == nil { + // Ensure devfile model is set + if component.Status.Devfile == "" { + // The Component has been just created. + // Component controller (from Application Service) must set devfile model, wait for it. + log.Info("Waiting for devfile model in component") + // Do not requeue as after model update a new update event will trigger a new reconcile + return ctrl.Result{}, nil + } + } else { + buildPipelineName, buildPipelineBundle, _ := getPipelineNameAndBundle(pipelineRef) + log.Info(fmt.Sprintf("Will use default pipeline from annotation %s : name: %s; bundle: %s", defaultBuildPipelineAnnotation, buildPipelineName, buildPipelineBundle)) + } + // Ensure pipeline service account exists pipelineSA, err := r.ensurePipelineServiceAccount(ctx, component.Namespace) if err != nil { @@ -509,6 +541,14 @@ func (r *ComponentBuildReconciler) Reconcile(ctx context.Context, req ctrl.Reque // remove component from metrics map delete(bometrics.ComponentTimesForMetrics, componentIdForMetrics) + r.WaitForCacheUpdate(ctx, req.NamespacedName, &component) + + return ctrl.Result{}, nil +} + +func (r *ComponentBuildReconciler) WaitForCacheUpdate(ctx context.Context, namespace types.NamespacedName, component *appstudiov1alpha1.Component) { + log := ctrllog.FromContext(ctx) + // Here we do some trick. // The problem is that the component update triggers both: a new reconcile and operator cache update. // In other words we are getting race condition. If a new reconcile is triggered before cache update, @@ -518,7 +558,7 @@ func (r *ComponentBuildReconciler) Reconcile(ctx context.Context, req ctrl.Reque // we are waiting for the cache update. This approach prevents next reconciles with outdated cache. isComponentInCacheUpToDate := false for i := 0; i < 20; i++ { - if err := r.Client.Get(ctx, req.NamespacedName, &component); err == nil { + if err := r.Client.Get(ctx, namespace, component); err == nil { _, buildRequestAnnotationExists := component.Annotations[BuildRequestAnnotationName] _, buildStatusAnnotationExists := component.Annotations[BuildStatusAnnotationName] if !buildRequestAnnotationExists && buildStatusAnnotationExists { @@ -540,8 +580,6 @@ func (r *ComponentBuildReconciler) Reconcile(ctx context.Context, req ctrl.Reque if !isComponentInCacheUpToDate { log.Info("failed to wait for updated cache. Requested action could be repeated.", l.Audit, "true") } - - return ctrl.Result{}, nil } func readBuildStatus(component *appstudiov1alpha1.Component) *BuildStatus { diff --git a/controllers/component_build_controller_common.go b/controllers/component_build_controller_common.go index 3ab49c02..a40f9e6b 100644 --- a/controllers/component_build_controller_common.go +++ b/controllers/component_build_controller_common.go @@ -41,6 +41,11 @@ import ( ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) +type BuildPipeline struct { + Name string `json:"name,omitempty"` + Bundle string `json:"bundle,omitempty"` +} + // That way it can be mocked in tests var DevfileSearchForDockerfile = devfile.SearchForDockerfile @@ -92,6 +97,29 @@ func getGitProvider(component appstudiov1alpha1.Component) (string, error) { return gitProvider, err } +// GetBuildPipelineFromComponentAnnotation parses pipeline annotation on component and returns build pipeline +func GetBuildPipelineFromComponentAnnotation(component *appstudiov1alpha1.Component) (*tektonapi.PipelineRef, error) { + buildPipeline, err := readBuildPipelineAnnotation(component) + if err != nil { + return nil, err + } + + if buildPipeline.Bundle != "" { + // for now we will return PipelineRef format, because it is the same what methods which use build-selector are returning + pipelineRef := &tektonapi.PipelineRef{ + ResolverRef: tektonapi.ResolverRef{ + Resolver: "bundles", + Params: []tektonapi.Param{ + {Name: "name", Value: *tektonapi.NewStructuredValues(buildPipeline.Name)}, + {Name: "bundle", Value: *tektonapi.NewStructuredValues(buildPipeline.Bundle)}, + }, + }, + } + return pipelineRef, nil + } + return nil, nil +} + // GetPipelineForComponent searches for the build pipeline to use on the component. func (r *ComponentBuildReconciler) GetPipelineForComponent(ctx context.Context, component *appstudiov1alpha1.Component) (*tektonapi.PipelineRef, []tektonapi.Param, error) { var pipelineSelectors []buildappstudiov1alpha1.BuildPipelineSelector @@ -385,3 +413,21 @@ func getPipelineNameAndBundle(pipelineRef *tektonapi.PipelineRef) (string, strin return name, bundle, nil } + +func readBuildPipelineAnnotation(component *appstudiov1alpha1.Component) (*BuildPipeline, error) { + if component.Annotations == nil { + return &BuildPipeline{}, 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 &BuildPipeline{}, nil +} diff --git a/controllers/component_build_controller_pac.go b/controllers/component_build_controller_pac.go index f9209bc1..bddfb910 100644 --- a/controllers/component_build_controller_pac.go +++ b/controllers/component_build_controller_pac.go @@ -888,11 +888,22 @@ func generatePACRepository(component appstudiov1alpha1.Component, config map[str func (r *ComponentBuildReconciler) generatePaCPipelineRunConfigs(ctx context.Context, component *appstudiov1alpha1.Component, gitClient gp.GitProviderClient, pacTargetBranch string) ([]byte, []byte, error) { log := ctrllog.FromContext(ctx) - pipelineRef, additionalPipelineParams, err := r.GetPipelineForComponent(ctx, component) - if err != nil { - return nil, nil, err + var pipelineName string + var pipelineBundle string + var additionalPipelineParams []tektonapi.Param + var pipelineRef *tektonapi.PipelineRef + var err error + + // no need to check error because it would fail already in Reconcile + pipelineRef, _ = GetBuildPipelineFromComponentAnnotation(component) + if pipelineRef == nil { + pipelineRef, additionalPipelineParams, err = r.GetPipelineForComponent(ctx, component) + if err != nil { + return nil, nil, err + } } - pipelineName, pipelineBundle, err := getPipelineNameAndBundle(pipelineRef) + + pipelineName, pipelineBundle, err = getPipelineNameAndBundle(pipelineRef) if err != nil { return nil, nil, err } @@ -1169,15 +1180,25 @@ func generatePaCPipelineRunForComponent( params = append(params, tektonapi.Param{Name: "image-expires-after", Value: tektonapi.ParamValue{Type: "string", StringVal: prImageExpiration}}) } - dockerFile, err := DevfileSearchForDockerfile([]byte(component.Status.Devfile)) - if err != nil { - return nil, boerrors.NewBuildOpError(boerrors.EInvalidDevfile, err) - } - if dockerFile != nil { - if dockerFile.Uri != "" { - params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: dockerFile.Uri}}) + // no need to check error because it would fail already in Reconcile + pipelineAnnotationUsed, _ := GetBuildPipelineFromComponentAnnotation(component) + if pipelineAnnotationUsed == nil { + dockerFile, err := DevfileSearchForDockerfile([]byte(component.Status.Devfile)) + if err != nil { + return nil, boerrors.NewBuildOpError(boerrors.EInvalidDevfile, err) } - pathContext := getPathContext(component.Spec.Source.GitSource.Context, dockerFile.BuildContext) + if dockerFile != nil { + if dockerFile.Uri != "" { + params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: dockerFile.Uri}}) + } + pathContext := getPathContext(component.Spec.Source.GitSource.Context, dockerFile.BuildContext) + if pathContext != "" { + params = append(params, tektonapi.Param{Name: "path-context", Value: tektonapi.ParamValue{Type: "string", StringVal: pathContext}}) + } + } + } 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}}) } diff --git a/controllers/component_build_controller_simple_build.go b/controllers/component_build_controller_simple_build.go index 0013177d..f158944b 100644 --- a/controllers/component_build_controller_simple_build.go +++ b/controllers/component_build_controller_simple_build.go @@ -50,11 +50,22 @@ func (r *ComponentBuildReconciler) SubmitNewBuild(ctx context.Context, component fmt.Errorf("Git repository URL can't use insecure HTTP: %s", component.Spec.Source.GitSource.URL)) } - pipelineRef, additionalPipelineParams, err := r.GetPipelineForComponent(ctx, component) - if err != nil { - return err + var pipelineName string + var pipelineBundle string + var additionalPipelineParams []tektonapi.Param + var pipelineRef *tektonapi.PipelineRef + var err error + + // no need to check error because it would fail already in Reconcile + pipelineRef, _ = GetBuildPipelineFromComponentAnnotation(component) + if pipelineRef == nil { + pipelineRef, additionalPipelineParams, err = r.GetPipelineForComponent(ctx, component) + if err != nil { + return err + } } - pipelineName, pipelineBundle, err := getPipelineNameAndBundle(pipelineRef) + + pipelineName, pipelineBundle, err = getPipelineNameAndBundle(pipelineRef) if err != nil { return err } @@ -229,15 +240,25 @@ func generatePipelineRunForComponent(component *appstudiov1alpha1.Component, pip params = append(params, tektonapi.Param{Name: "skip-checks", Value: tektonapi.ParamValue{Type: "string", StringVal: "true"}}) } - dockerFile, err := DevfileSearchForDockerfile([]byte(component.Status.Devfile)) - if err != nil { - return nil, boerrors.NewBuildOpError(boerrors.EInvalidDevfile, err) - } - if dockerFile != nil { - if dockerFile.Uri != "" { - params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: dockerFile.Uri}}) + // no need to check error because it would fail already in Reconcile + pipelineAnnotationUsed, _ := GetBuildPipelineFromComponentAnnotation(component) + if pipelineAnnotationUsed == nil { + dockerFile, err := DevfileSearchForDockerfile([]byte(component.Status.Devfile)) + if err != nil { + return nil, boerrors.NewBuildOpError(boerrors.EInvalidDevfile, err) + } + if dockerFile != nil { + if dockerFile.Uri != "" { + params = append(params, tektonapi.Param{Name: "dockerfile", Value: tektonapi.ParamValue{Type: "string", StringVal: dockerFile.Uri}}) + } + pathContext := getPathContext(component.Spec.Source.GitSource.Context, dockerFile.BuildContext) + if pathContext != "" { + params = append(params, tektonapi.Param{Name: "path-context", Value: tektonapi.ParamValue{Type: "string", StringVal: pathContext}}) + } } - pathContext := getPathContext(component.Spec.Source.GitSource.Context, dockerFile.BuildContext) + } 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}}) } diff --git a/controllers/component_build_controller_test.go b/controllers/component_build_controller_test.go index a662c218..98acc0cc 100644 --- a/controllers/component_build_controller_test.go +++ b/controllers/component_build_controller_test.go @@ -107,6 +107,104 @@ var _ = Describe("Component initial build controller", func() { deleteBuildPipelineRunSelector(defaultSelectorKey) }) + Context("Test Pipelines as Code build preparation without build pipeline selector", func() { + var resourcePacPrepKey = types.NamespacedName{Name: HASCompName + "-pacprepannotation", Namespace: HASAppNamespace} + + _ = BeforeEach(func() { + deleteBuildPipelineRunSelector(defaultSelectorKey) + createNamespace(pipelinesAsCodeNamespace) + createRoute(pacRouteKey, "pac-host") + createNamespace(BuildServiceNamespaceName) + pacSecretData := map[string]string{ + "github-application-id": "12345", + "github-private-key": githubAppPrivateKey, + } + createSecret(pacSecretKey, pacSecretData) + + ResetTestGitProviderClient() + }) + + _ = AfterEach(func() { + deleteComponent(resourcePacPrepKey) + deletePaCRepository(resourcePacPrepKey) + + deleteSecret(webhookSecretKey) + deleteSecret(namespacePaCSecretKey) + + deleteSecret(pacSecretKey) + deleteRoute(pacRouteKey) + }) + + It("should successfully submit PR with PaC definitions using GitHub application", func() { + mergeUrl := "merge-url" + + isCreatePaCPullRequestInvoked := false + EnsurePaCMergeRequestFunc = func(repoUrl string, d *gp.MergeRequestData) (string, error) { + isCreatePaCPullRequestInvoked = true + Expect(repoUrl).To(Equal(SampleRepoLink + "-" + resourcePacPrepKey.Name)) + Expect(len(d.Files)).To(Equal(2)) + for _, file := range d.Files { + Expect(strings.HasPrefix(file.FullPath, ".tekton/")).To(BeTrue()) + } + Expect(d.CommitMessage).ToNot(BeEmpty()) + Expect(d.BranchName).ToNot(BeEmpty()) + Expect(d.BaseBranchName).To(Equal("main")) + Expect(d.Title).ToNot(BeEmpty()) + Expect(d.Text).ToNot(BeEmpty()) + Expect(d.AuthorName).ToNot(BeEmpty()) + Expect(d.AuthorEmail).ToNot(BeEmpty()) + return mergeUrl, nil + } + SetupPaCWebhookFunc = func(string, string, string) error { + defer GinkgoRecover() + Fail("Should not create webhook if GitHub application is used") + return nil + } + + annotationValue := fmt.Sprintf("{\"name\":\"%s\",\"bundle\":\"%s\"}", defaultPipelineName, defaultPipelineBundle) + annotations := map[string]string{defaultBuildPipelineAnnotation: annotationValue} + createCustomComponentWithBuildRequestWithoutDevfile(componentConfig{ + componentKey: resourcePacPrepKey, + annotations: annotations, + }, BuildRequestConfigurePaCAnnotationValue) + + pacRepo := waitPaCRepositoryCreated(resourcePacPrepKey) + Expect(pacRepo.Spec.Params).ShouldNot(BeNil()) + existingParams := map[string]string{} + for _, param := range *pacRepo.Spec.Params { + existingParams[param.Name] = param.Value + } + val, ok := existingParams[pacCustomParamAppstudioWorkspace] + Expect(ok).Should(BeTrue()) + Expect(val).Should(Equal("build")) + + waitPaCFinalizerOnComponent(resourcePacPrepKey) + Eventually(func() bool { + return isCreatePaCPullRequestInvoked + }, timeout, interval).Should(BeTrue()) + + expectPacBuildStatus(resourcePacPrepKey, "enabled", 0, "", mergeUrl) + }) + + It("should fail to submit PR if build pipeline annotation isn't valid json", func() { + annotations := map[string]string{defaultBuildPipelineAnnotation: "wrong"} + createCustomComponentWithBuildRequestWithoutDevfile(componentConfig{ + componentKey: resourcePacPrepKey, + annotations: annotations, + }, BuildRequestConfigurePaCAnnotationValue) + waitComponentAnnotationGone(resourcePacPrepKey, BuildRequestAnnotationName) + + expectError := boerrors.NewBuildOpError(boerrors.EFailedToParsePipelineAnnotation, nil) + + buildStatus := readBuildStatus(getComponent(resourcePacPrepKey)) + Expect(buildStatus).ToNot(BeNil()) + errorMessage := fmt.Sprintf("%d: %s", expectError.GetErrorId(), expectError.ShortError()) + Expect(buildStatus.Message).To(ContainSubstring(errorMessage)) + }) + }) + + // when build pipeline selector will be removed + // move all tests to "Test Pipelines as Code build preparation without build pipeline selector" without using build selector and remove all build selector related tests Context("Test Pipelines as Code build preparation", func() { var resourcePacPrepKey = types.NamespacedName{Name: HASCompName + "-pacprep", Namespace: HASAppNamespace} @@ -1285,6 +1383,71 @@ var _ = Describe("Component initial build controller", func() { }) }) + Context("Test simple build flow without build pipeline selector", func() { + var resouceSimpleBuildKey = types.NamespacedName{Name: HASCompName + "-simpleannotation", Namespace: HASAppNamespace} + + _ = BeforeEach(func() { + deleteBuildPipelineRunSelector(defaultSelectorKey) + createNamespace(BuildServiceNamespaceName) + ResetTestGitProviderClient() + + pacSecretData := map[string]string{ + "github-application-id": "12345", + "github-private-key": githubAppPrivateKey, + } + createSecret(pacSecretKey, pacSecretData) + }) + + _ = AfterEach(func() { + deleteSecret(pacSecretKey) + + deleteBuildPipelineRunSelector(defaultSelectorKey) + deleteComponentPipelineRuns(resouceSimpleBuildKey) + deleteComponent(resouceSimpleBuildKey) + // wait for pruner operator to finish, so it won't prune runs from new test + time.Sleep(time.Second) + }) + + It("should submit initial build on component creation", func() { + gitSourceSHA := "d1a9e858489d1515621398fb02942da068f1c956" + isGetBranchShaInvoked := false + GetBranchShaFunc = func(repoUrl string, branchName string) (string, error) { + isGetBranchShaInvoked = true + Expect(repoUrl).To(Equal(SampleRepoLink + "-" + resouceSimpleBuildKey.Name)) + return gitSourceSHA, nil + } + GetBrowseRepositoryAtShaLinkFunc = func(repoUrl, sha string) string { + Expect(repoUrl).To(Equal(SampleRepoLink + "-" + resouceSimpleBuildKey.Name)) + Expect(sha).To(Equal(gitSourceSHA)) + return "https://github.com/devfile-samples/devfile-sample-java-springboot-basic?rev=" + gitSourceSHA + } + + annotationValue := fmt.Sprintf("{\"name\":\"%s\",\"bundle\":\"%s\"}", defaultPipelineName, defaultPipelineBundle) + annotations := map[string]string{defaultBuildPipelineAnnotation: annotationValue} + createCustomComponentWithoutBuildRequestWithoutDevfile(componentConfig{ + componentKey: resouceSimpleBuildKey, + annotations: annotations}) + + Eventually(func() bool { + return isGetBranchShaInvoked + }, timeout, interval).Should(BeTrue()) + + waitOneInitialPipelineRunCreated(resouceSimpleBuildKey) + waitComponentAnnotationGone(resouceSimpleBuildKey, BuildRequestAnnotationName) + expectSimpleBuildStatus(resouceSimpleBuildKey, 0, "", false) + + // Check pipeline run labels and annotations + pipelineRun := listComponentPipelineRuns(resouceSimpleBuildKey)[0] + Expect(pipelineRun.Annotations[gitCommitShaAnnotationName]).To(Equal(gitSourceSHA)) + Expect(pipelineRun.Annotations[gitRepoAtShaAnnotationName]).To( + Equal("https://github.com/devfile-samples/devfile-sample-java-springboot-basic?rev=" + gitSourceSHA)) + Expect(pipelineRun.Annotations["build.appstudio.redhat.com/pipeline_name"]).To(Equal(defaultPipelineName)) + Expect(pipelineRun.Annotations["build.appstudio.redhat.com/bundle"]).To(Equal(defaultPipelineBundle)) + }) + }) + + // when build pipeline selector will be removed + // move all tests to "Test simple build flow without build pipeline selector" without using build selector and remove all build selector related tests Context("Test simple build flow", func() { var resouceSimpleBuildKey = types.NamespacedName{Name: HASCompName + "-simple", Namespace: HASAppNamespace} diff --git a/controllers/component_build_controller_unit_test.go b/controllers/component_build_controller_unit_test.go index 089ed3ea..1a0fd83c 100644 --- a/controllers/component_build_controller_unit_test.go +++ b/controllers/component_build_controller_unit_test.go @@ -43,7 +43,10 @@ import ( tektonapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" ) -const ghAppPrivateKeyStub = "-----BEGIN RSA PRIVATE KEY-----_key-content_-----END RSA PRIVATE KEY-----" +const ( + ghAppPrivateKeyStub = "-----BEGIN RSA PRIVATE KEY-----_key-content_-----END RSA PRIVATE KEY-----" + testPipelineAnnotation = "{\"name\":\"pipeline_name\",\"bundle\":\"bundle_name\"}" +) func TestGetProvisionTimeMetricsBuckets(t *testing.T) { buckets := bometrics.HistogramBuckets @@ -250,6 +253,7 @@ func TestGenerateInitialPipelineRunForComponentDevfileError(t *testing.T) { } } +// remove when removing build pipeline selector func TestGenerateInitialPipelineRunForComponentDockerfileContext(t *testing.T) { component := &appstudiov1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ @@ -268,6 +272,7 @@ func TestGenerateInitialPipelineRunForComponentDockerfileContext(t *testing.T) { GitSource: &appstudiov1alpha1.GitSource{ URL: "https://githost.com/user/repo.git", Revision: "custom-branch", + Context: "./base_context", }, }, }, @@ -307,7 +312,7 @@ func TestGenerateInitialPipelineRunForComponentDockerfileContext(t *testing.T) { for _, param := range pipelineRun.Spec.Params { switch param.Name { case "path-context": - if param.Value.StringVal != dockerfileContext { + if param.Value.StringVal != "base_context/some_context" { t.Errorf("generateInitialPipelineRunForComponentDockerfileContext(): wrong pipeline parameter %s", param.Name) } case "dockerfile": @@ -322,6 +327,81 @@ func TestGenerateInitialPipelineRunForComponentDockerfileContext(t *testing.T) { } } +func TestGenerateInitialPipelineRunForComponentDockerfileContextPipelineFromAnnotation(t *testing.T) { + component := &appstudiov1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-component", + Namespace: "my-namespace", + Annotations: map[string]string{ + "skip-initial-checks": "true", + GitProviderAnnotationName: "github", + defaultBuildPipelineAnnotation: testPipelineAnnotation, + }, + }, + Spec: appstudiov1alpha1.ComponentSpec{ + Application: "my-application", + ContainerImage: "registry.io/username/image:tag", + Source: appstudiov1alpha1.ComponentSource{ + ComponentSourceUnion: appstudiov1alpha1.ComponentSourceUnion{ + GitSource: &appstudiov1alpha1.GitSource{ + URL: "https://githost.com/user/repo.git", + Revision: "custom-branch", + Context: "./base_context", + }, + }, + }, + }, + Status: appstudiov1alpha1.ComponentStatus{}, + } + pipelineRef := &tektonapi.PipelineRef{ + ResolverRef: tektonapi.ResolverRef{ + Resolver: "bundles", + Params: []tektonapi.Param{ + {Name: "name", Value: *tektonapi.NewStructuredValues("pipeline-name")}, + {Name: "bundle", Value: *tektonapi.NewStructuredValues("pipeline-bundle")}, + }, + }, + } + additionalParams := []tektonapi.Param{ + {Name: "rebuild", Value: tektonapi.ParamValue{Type: "string", StringVal: "true"}}, + } + + dockerfileContext := "some_context" + dockerfileURI := "some_uri" + DevfileSearchForDockerfile = func(devfileBytes []byte) (*v1alpha2.DockerfileImage, error) { + dockerfileImage := v1alpha2.DockerfileImage{ + BaseImage: v1alpha2.BaseImage{}, + DockerfileSrc: v1alpha2.DockerfileSrc{Uri: dockerfileURI}, + Dockerfile: v1alpha2.Dockerfile{BuildContext: dockerfileContext}, + } + return &dockerfileImage, nil + } + + pipelineRun, err := generatePipelineRunForComponent(component, pipelineRef, additionalParams, &buildGitInfo{}) + + if err != nil { + t.Error("generateInitialPipelineRunForComponentDockerfileContext(): Failed to generate pipeline run") + } + + for _, param := range pipelineRun.Spec.Params { + switch param.Name { + case "path-context": + if param.Value.StringVal != "base_context" { + t.Errorf("generateInitialPipelineRunForComponentDockerfileContext(): wrong pipeline parameter %s", param.Name) + } + case "dockerfile": + if param.Value.StringVal != "Dockerfile" { + t.Errorf("generateInitialPipelineRunForComponentDockerfileContext(): wrong pipeline parameter %s", param.Name) + } + case "revision": + if param.Value.StringVal != "custom-branch" { + t.Errorf("generateInitialPipelineRunForComponentDockerfileContext(): wrong pipeline parameter %s", param.Name) + } + } + } +} + +// remove when removing build pipeline selector func TestGenerateInitialPipelineRunForComponent(t *testing.T) { component := &appstudiov1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ @@ -340,6 +420,7 @@ func TestGenerateInitialPipelineRunForComponent(t *testing.T) { GitSource: &appstudiov1alpha1.GitSource{ URL: "https://githost.com/user/repo.git", Revision: "custom-branch", + Context: "./base_context", }, }, }, @@ -367,6 +448,7 @@ func TestGenerateInitialPipelineRunForComponent(t *testing.T) { browseRepositoryAtShaLink: "https://githost.com/user/repo?rev=" + commitSha, } DevfileSearchForDockerfile = devfile.SearchForDockerfile + pipelineRun, err := generatePipelineRunForComponent(component, pipelineRef, additionalParams, pRunGitInfo) if err != nil { t.Error("generateInitialPipelineRunForComponent(): Failed to genertate pipeline run") @@ -453,6 +535,146 @@ func TestGenerateInitialPipelineRunForComponent(t *testing.T) { } } +func TestGenerateInitialPipelineRunForComponentPipelineFromAnnotation(t *testing.T) { + component := &appstudiov1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-component", + Namespace: "my-namespace", + Annotations: map[string]string{ + "skip-initial-checks": "true", + GitProviderAnnotationName: "github", + defaultBuildPipelineAnnotation: testPipelineAnnotation, + }, + }, + Spec: appstudiov1alpha1.ComponentSpec{ + Application: "my-application", + ContainerImage: "registry.io/username/image:tag", + Source: appstudiov1alpha1.ComponentSource{ + ComponentSourceUnion: appstudiov1alpha1.ComponentSourceUnion{ + GitSource: &appstudiov1alpha1.GitSource{ + URL: "https://githost.com/user/repo.git", + Revision: "custom-branch", + Context: "./base_context", + }, + }, + }, + }, + Status: appstudiov1alpha1.ComponentStatus{}, + } + pipelineRef := &tektonapi.PipelineRef{ + ResolverRef: tektonapi.ResolverRef{ + Resolver: "bundles", + Params: []tektonapi.Param{ + {Name: "name", Value: *tektonapi.NewStructuredValues("pipeline-name")}, + {Name: "bundle", Value: *tektonapi.NewStructuredValues("pipeline-bundle")}, + }, + }, + } + additionalParams := []tektonapi.Param{ + {Name: "rebuild", Value: tektonapi.ParamValue{Type: "string", StringVal: "true"}}, + } + commitSha := "26239c94569cea79b32bce32f12c8abd8bbd0fd7" + + pRunGitInfo := &buildGitInfo{ + gitSourceSha: commitSha, + browseRepositoryAtShaLink: "https://githost.com/user/repo?rev=" + commitSha, + } + + pipelineRun, err := generatePipelineRunForComponent(component, pipelineRef, additionalParams, pRunGitInfo) + if err != nil { + t.Error("generateInitialPipelineRunForComponent(): Failed to genertate pipeline run") + } + + if pipelineRun.GenerateName == "" { + t.Error("generateInitialPipelineRunForComponent(): pipeline generatename must not be empty") + } + if pipelineRun.Namespace != "my-namespace" { + t.Error("generateInitialPipelineRunForComponent(): pipeline namespace doesn't match") + } + + if pipelineRun.Labels[ApplicationNameLabelName] != "my-application" { + t.Errorf("generateInitialPipelineRunForComponent(): wrong %s label value", ApplicationNameLabelName) + } + if pipelineRun.Labels[ComponentNameLabelName] != "my-component" { + t.Errorf("generateInitialPipelineRunForComponent(): wrong %s label value", ComponentNameLabelName) + } + if pipelineRun.Labels["pipelines.appstudio.openshift.io/type"] != "build" { + t.Error("generateInitialPipelineRunForComponent(): wrong pipelines.appstudio.openshift.io/type label value") + } + + if pipelineRun.Annotations["build.appstudio.redhat.com/target_branch"] != "custom-branch" { + t.Error("generateInitialPipelineRunForComponent(): wrong build.appstudio.redhat.com/target_branch annotation value") + } + if pipelineRun.Annotations["build.appstudio.redhat.com/pipeline_name"] != "pipeline-name" { + t.Error("generateInitialPipelineRunForComponent(): wrong build.appstudio.redhat.com/pipeline_name annotation value") + } + if pipelineRun.Annotations["build.appstudio.redhat.com/bundle"] != "pipeline-bundle" { + t.Error("generateInitialPipelineRunForComponent(): wrong build.appstudio.redhat.com/bundle annotation value") + } + if pipelineRun.Annotations[gitCommitShaAnnotationName] != commitSha { + t.Errorf("generateInitialPipelineRunForComponent(): wrong %s annotation value", gitCommitShaAnnotationName) + } + if pipelineRun.Annotations[gitRepoAtShaAnnotationName] != "https://githost.com/user/repo?rev="+commitSha { + t.Errorf("generateInitialPipelineRunForComponent(): wrong %s annotation value", gitRepoAtShaAnnotationName) + } + + if getPipelineName(pipelineRun.Spec.PipelineRef) != "pipeline-name" { + t.Error("generateInitialPipelineRunForComponent(): wrong pipeline name in pipeline reference") + } + if getPipelineBundle(pipelineRun.Spec.PipelineRef) != "pipeline-bundle" { + t.Error("generateInitialPipelineRunForComponent(): wrong pipeline bundle in pipeline reference") + } + + if len(pipelineRun.Spec.Params) != 7 { + t.Error("generateInitialPipelineRunForComponent(): wrong number of pipeline params") + } + for _, param := range pipelineRun.Spec.Params { + switch param.Name { + case "git-url": + if param.Value.StringVal != "https://githost.com/user/repo.git" { + t.Errorf("generateInitialPipelineRunForComponent(): wrong pipeline parameter %s", param.Name) + } + case "revision": + if param.Value.StringVal != commitSha { + t.Errorf("generateInitialPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "output-image": + if !strings.HasPrefix(param.Value.StringVal, "registry.io/username/image:build-") { + t.Errorf("generateInitialPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "rebuild": + if param.Value.StringVal != "true" { + t.Errorf("generateInitialPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "skip-checks": + if param.Value.StringVal != "true" { + t.Errorf("generateInitialPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "dockerfile": + if param.Value.StringVal != "Dockerfile" { + t.Errorf("generateInitialPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "path-context": + if param.Value.StringVal != "base_context" { + t.Errorf("generateInitialPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + default: + t.Errorf("generateInitialPipelineRunForComponent(): unexpected pipeline parameter %v", param) + } + } + + if len(pipelineRun.Spec.Workspaces) != 1 { + t.Error("generateInitialPipelineRunForComponent(): wrong number of pipeline workspaces") + } + for _, workspace := range pipelineRun.Spec.Workspaces { + if workspace.Name == "workspace" { + continue + } + t.Errorf("generateInitialPipelineRunForComponent(): unexpected pipeline workspaces %v", workspace) + } +} + +// remove when removing build pipeline selector func TestGeneratePaCPipelineRunForComponent(t *testing.T) { component := &appstudiov1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ @@ -469,7 +691,8 @@ func TestGeneratePaCPipelineRunForComponent(t *testing.T) { Source: appstudiov1alpha1.ComponentSource{ ComponentSourceUnion: appstudiov1alpha1.ComponentSourceUnion{ GitSource: &appstudiov1alpha1.GitSource{ - URL: "https://githost.com/user/repo.git", + URL: "https://githost.com/user/repo.git", + Context: "./base_context", }, }, }, @@ -526,7 +749,8 @@ func TestGeneratePaCPipelineRunForComponent(t *testing.T) { t.Error("generatePaCPipelineRunForComponent(): wrong pipelines.appstudio.openshift.io/type label value") } - if pipelineRun.Annotations["pipelinesascode.tekton.dev/on-cel-expression"] != `event == "pull_request" && target_branch == "custom-branch"` { + onCel := `event == "pull_request" && target_branch == "custom-branch" && ( "./base_context/***".pathChanged() || ".tekton/my-component-pull-request.yaml".pathChanged() )` + if pipelineRun.Annotations["pipelinesascode.tekton.dev/on-cel-expression"] != onCel { t.Errorf("generatePaCPipelineRunForComponent(): wrong pipelinesascode.tekton.dev/on-cel-expression annotation value") } if pipelineRun.Annotations["pipelinesascode.tekton.dev/max-keep-runs"] != "3" { @@ -571,11 +795,147 @@ func TestGeneratePaCPipelineRunForComponent(t *testing.T) { t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) } case "dockerfile": - if param.Value.StringVal != dockerfileURI { + if param.Value.StringVal != "dockerfile" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "path-context": + if param.Value.StringVal != "base_context/docker" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + default: + t.Errorf("generatePaCPipelineRunForComponent(): unexpected pipeline parameter %v", param) + } + } + + if len(pipelineRun.Spec.Workspaces) != 2 { + t.Error("generatePaCPipelineRunForComponent(): wrong number of pipeline workspaces") + } + for _, workspace := range pipelineRun.Spec.Workspaces { + if workspace.Name == "workspace" { + continue + } + if workspace.Name == "git-auth" { + continue + } + t.Errorf("generatePaCPipelineRunForComponent(): unexpected pipeline workspaces %v", workspace) + } +} + +func TestGeneratePaCPipelineRunForComponentPipelineFromAnnotation(t *testing.T) { + component := &appstudiov1alpha1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-component", + Namespace: "my-namespace", + Annotations: map[string]string{ + "skip-initial-checks": "true", + GitProviderAnnotationName: "github", + defaultBuildPipelineAnnotation: testPipelineAnnotation, + }, + }, + Spec: appstudiov1alpha1.ComponentSpec{ + Application: "my-application", + ContainerImage: "registry.io/username/image:tag", + Source: appstudiov1alpha1.ComponentSource{ + ComponentSourceUnion: appstudiov1alpha1.ComponentSourceUnion{ + GitSource: &appstudiov1alpha1.GitSource{ + URL: "https://githost.com/user/repo.git", + Context: "./base_context", + }, + }, + }, + }, + Status: appstudiov1alpha1.ComponentStatus{}, + } + pipelineSpec := &tektonapi.PipelineSpec{ + Workspaces: []tektonapi.PipelineWorkspaceDeclaration{ + { + Name: "git-auth", + }, + { + Name: "workspace", + }, + }, + } + additionalParams := []tektonapi.Param{ + {Name: "revision", Value: tektonapi.ParamValue{Type: "string", StringVal: "2378a064bf6b66a8ffc650ad88d404cca24ade29"}}, + {Name: "rebuild", Value: tektonapi.ParamValue{Type: "string", StringVal: "true"}}, + } + branchName := "custom-branch" + ResetTestGitProviderClient() + + pipelineRun, err := generatePaCPipelineRunForComponent(component, pipelineSpec, additionalParams, true, branchName, testGitProviderClient) + if err != nil { + t.Error("generatePaCPipelineRunForComponent(): Failed to genertate pipeline run") + } + + if pipelineRun.Name != component.Name+pipelineRunOnPRSuffix { + t.Error("generatePaCPipelineRunForComponent(): wrong pipeline name") + } + if pipelineRun.Namespace != "my-namespace" { + t.Error("generatePaCPipelineRunForComponent(): pipeline namespace doesn't match") + } + + if pipelineRun.Labels[ApplicationNameLabelName] != "my-application" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong %s label value", ApplicationNameLabelName) + } + if pipelineRun.Labels[ComponentNameLabelName] != "my-component" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong %s label value", ComponentNameLabelName) + } + if pipelineRun.Labels["pipelines.appstudio.openshift.io/type"] != "build" { + t.Error("generatePaCPipelineRunForComponent(): wrong pipelines.appstudio.openshift.io/type label value") + } + + onCel := `event == "pull_request" && target_branch == "custom-branch" && ( "./base_context/***".pathChanged() || ".tekton/my-component-pull-request.yaml".pathChanged() )` + if pipelineRun.Annotations["pipelinesascode.tekton.dev/on-cel-expression"] != onCel { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipelinesascode.tekton.dev/on-cel-expression annotation value") + } + if pipelineRun.Annotations["pipelinesascode.tekton.dev/max-keep-runs"] != "3" { + t.Error("generatePaCPipelineRunForComponent(): wrong pipelinesascode.tekton.dev/max-keep-runs annotation value") + } + if pipelineRun.Annotations["build.appstudio.redhat.com/target_branch"] != "{{target_branch}}" { + t.Error("generatePaCPipelineRunForComponent(): wrong build.appstudio.redhat.com/target_branch annotation value") + } + if pipelineRun.Annotations[gitCommitShaAnnotationName] != "{{revision}}" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong %s annotation value", gitCommitShaAnnotationName) + } + if pipelineRun.Annotations[gitRepoAtShaAnnotationName] != "https://githost.com/user/repo?rev={{revision}}" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong %s annotation value", gitRepoAtShaAnnotationName) + } + if pipelineRun.Annotations["build.appstudio.redhat.com/pull_request_number"] != "{{pull_request_number}}" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong build.appstudio.redhat.com/pull_request_number annotation value") + } + + if len(pipelineRun.Spec.Params) != 7 { + t.Error("generatePaCPipelineRunForComponent(): wrong number of pipeline params") + } + for _, param := range pipelineRun.Spec.Params { + switch param.Name { + case "git-url": + if param.Value.StringVal != "{{source_url}}" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s", param.Name) + } + case "revision": + if param.Value.StringVal != "2378a064bf6b66a8ffc650ad88d404cca24ade29" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "output-image": + if !strings.HasPrefix(param.Value.StringVal, "registry.io/username/image:on-pr-{{revision}}") { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "rebuild": + if param.Value.StringVal != "true" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "image-expires-after": + if param.Value.StringVal != "5d" { + t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) + } + case "dockerfile": + if param.Value.StringVal != "Dockerfile" { t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) } case "path-context": - if param.Value.StringVal != dockerfileContext { + if param.Value.StringVal != "base_context" { t.Errorf("generatePaCPipelineRunForComponent(): wrong pipeline parameter %s value", param.Name) } default: @@ -597,6 +957,7 @@ func TestGeneratePaCPipelineRunForComponent(t *testing.T) { } } +// remove when removing build pipeline selector func TestGeneratePaCPipelineRunForComponent_ShouldStopOnDevfileError(t *testing.T) { component := &appstudiov1alpha1.Component{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/suite_util_test.go b/controllers/suite_util_test.go index d84738a9..8689ddb9 100644 --- a/controllers/suite_util_test.go +++ b/controllers/suite_util_test.go @@ -181,6 +181,27 @@ func createComponentWithBuildRequestAndGit(componentKey types.NamespacedName, bu createCustomComponentWithBuildRequest(componentConfig{componentKey: componentKey, gitURL: gitURL, gitRevision: gitRevision}, buildRequest) } +func createCustomComponentWithoutBuildRequestWithoutDevfile(config componentConfig) { + component := getComponentData(config) + if component.Annotations == nil { + component.Annotations = make(map[string]string) + } + + Expect(k8sClient.Create(ctx, component)).Should(Succeed()) + getComponent(config.componentKey) +} + +func createCustomComponentWithBuildRequestWithoutDevfile(config componentConfig, buildRequest string) { + component := getComponentData(config) + if component.Annotations == nil { + component.Annotations = make(map[string]string) + } + component.Annotations[BuildRequestAnnotationName] = buildRequest + + Expect(k8sClient.Create(ctx, component)).Should(Succeed()) + getComponent(config.componentKey) +} + func createCustomComponentWithBuildRequest(config componentConfig, buildRequest string) { component := getComponentData(config) if component.Annotations == nil { diff --git a/pkg/boerrors/perror.go b/pkg/boerrors/perror.go index 7e18a31e..c453cccf 100644 --- a/pkg/boerrors/perror.go +++ b/pkg/boerrors/perror.go @@ -149,6 +149,8 @@ const ( EComponentImageRegistrySecretMissing BOErrorId = 202 // The secret with git credentials not given for component with private git repository. EComponentGitSecretNotSpecified BOErrorId = 203 + // Value of 'build.appstudio.openshift.io/pipeline' component annotation is not a valid json or the json has invalid structure. + EFailedToParsePipelineAnnotation BOErrorId = 204 // EInvalidDevfile devfile of the component is not valid. EInvalidDevfile BOErrorId = 220 @@ -201,6 +203,7 @@ var boErrorMessages = map[BOErrorId]string{ EComponentGitSecretMissing: "Secret with git credential not found", EComponentImageRegistrySecretMissing: "Component image repository secret not found", EComponentGitSecretNotSpecified: "Git credentials for private Component git repository not given", + EFailedToParsePipelineAnnotation: "Failed to parse build.appstudio.openshift.io/pipeline annotation value", EInvalidDevfile: "Component Devfile is invalid",