Skip to content

Commit

Permalink
Merge pull request #302 from rcerven/latest_bundle
Browse files Browse the repository at this point in the history
Support retrieving latest bundle in Build Service
  • Loading branch information
rcerven authored May 17, 2024
2 parents 0e591f6 + 67feb57 commit d501cc6
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 48 deletions.
3 changes: 2 additions & 1 deletion controllers/component_build_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const (
buildPipelineSelectorResourceName = "build-pipeline-selector"
defaultBuildPipelineAnnotation = "build.appstudio.openshift.io/pipeline"
buildPipelineConfigMapResourceName = "build-pipeline-config"
buildPipelineConfigName = "config.yaml"
)

type BuildStatus struct {
Expand Down Expand Up @@ -252,7 +253,7 @@ func (r *ComponentBuildReconciler) Reconcile(ctx context.Context, req ctrl.Reque
return ctrl.Result{}, nil
}

pipelineRef, err := GetBuildPipelineFromComponentAnnotation(&component)
pipelineRef, err := r.GetBuildPipelineFromComponentAnnotation(ctx, &component)
if err != nil {
log.Error(err, fmt.Sprintf("Failed to read %s annotation on component %s", defaultBuildPipelineAnnotation, component.Name), l.Action, l.ActionView)

Expand Down
69 changes: 54 additions & 15 deletions controllers/component_build_controller_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
appstudiov1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1"
devfile "github.com/redhat-appstudio/application-service/cdq-analysis/pkg"
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"
"k8s.io/apimachinery/pkg/api/resource"
Expand All @@ -46,6 +47,11 @@ type BuildPipeline struct {
Bundle string `json:"bundle,omitempty"`
}

type pipelineConfig struct {
DefaultPipelineName string `yaml:"default-pipeline-name"`
Pipelines []BuildPipeline `yaml:"pipelines"`
}

// That way it can be mocked in tests
var DevfileSearchForDockerfile = devfile.SearchForDockerfile

Expand Down Expand Up @@ -98,26 +104,59 @@ func getGitProvider(component appstudiov1alpha1.Component) (string, error) {
}

// GetBuildPipelineFromComponentAnnotation parses pipeline annotation on component and returns build pipeline
func GetBuildPipelineFromComponentAnnotation(component *appstudiov1alpha1.Component) (*tektonapi.PipelineRef, error) {
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 {
return nil, nil
}
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 != "" {
// 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)},
},
},
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)
}
return pipelineRef, nil
}
return nil, nil

// 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(finalBundle)},
},
},
}
return pipelineRef, nil
}

// GetPipelineForComponent searches for the build pipeline to use on the component.
Expand Down Expand Up @@ -416,7 +455,7 @@ func getPipelineNameAndBundle(pipelineRef *tektonapi.PipelineRef) (string, strin

func readBuildPipelineAnnotation(component *appstudiov1alpha1.Component) (*BuildPipeline, error) {
if component.Annotations == nil {
return &BuildPipeline{}, nil
return nil, nil
}

requestedPipeline, requestedPipelineExists := component.Annotations[defaultBuildPipelineAnnotation]
Expand All @@ -429,5 +468,5 @@ func readBuildPipelineAnnotation(component *appstudiov1alpha1.Component) (*Build
}
return buildPipeline, nil
}
return &BuildPipeline{}, nil
return nil, nil
}
14 changes: 8 additions & 6 deletions controllers/component_build_controller_pac.go
Original file line number Diff line number Diff line change
Expand Up @@ -900,8 +900,10 @@ func (r *ComponentBuildReconciler) generatePaCPipelineRunConfigs(ctx context.Con
var err error

// no need to check error because it would fail already in Reconcile
pipelineRef, _ = GetBuildPipelineFromComponentAnnotation(component)
pipelineRef, _ = r.GetBuildPipelineFromComponentAnnotation(ctx, component)
pipelineAnnotationUsed := true
if pipelineRef == nil {
pipelineAnnotationUsed = false
pipelineRef, additionalPipelineParams, err = r.GetPipelineForComponent(ctx, component)
if err != nil {
return nil, nil, err
Expand All @@ -924,7 +926,7 @@ func (r *ComponentBuildReconciler) generatePaCPipelineRunConfigs(ctx context.Con
}

pipelineRunOnPush, err := generatePaCPipelineRunForComponent(
component, pipelineSpec, additionalPipelineParams, false, pacTargetBranch, gitClient)
component, pipelineSpec, additionalPipelineParams, false, pacTargetBranch, gitClient, pipelineAnnotationUsed)
if err != nil {
return nil, nil, err
}
Expand All @@ -934,7 +936,7 @@ func (r *ComponentBuildReconciler) generatePaCPipelineRunConfigs(ctx context.Con
}

pipelineRunOnPR, err := generatePaCPipelineRunForComponent(
component, pipelineSpec, additionalPipelineParams, true, pacTargetBranch, gitClient)
component, pipelineSpec, additionalPipelineParams, true, pacTargetBranch, gitClient, pipelineAnnotationUsed)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -1135,7 +1137,8 @@ func generatePaCPipelineRunForComponent(
additionalPipelineParams []tektonapi.Param,
onPull bool,
pacTargetBranch string,
gitClient gp.GitProviderClient) (*tektonapi.PipelineRun, error) {
gitClient gp.GitProviderClient,
pipelineAnnotationUsed bool) (*tektonapi.PipelineRun, error) {

if pacTargetBranch == "" {
return nil, fmt.Errorf("target branch can't be empty for generating PaC PipelineRun for: %v", component)
Expand Down Expand Up @@ -1186,8 +1189,7 @@ func generatePaCPipelineRunForComponent(
}

// no need to check error because it would fail already in Reconcile
pipelineAnnotationUsed, _ := GetBuildPipelineFromComponentAnnotation(component)
if pipelineAnnotationUsed == nil {
if !pipelineAnnotationUsed {
dockerFile, err := DevfileSearchForDockerfile([]byte(component.Status.Devfile))
if err != nil {
return nil, boerrors.NewBuildOpError(boerrors.EInvalidDevfile, err)
Expand Down
11 changes: 6 additions & 5 deletions controllers/component_build_controller_simple_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ func (r *ComponentBuildReconciler) SubmitNewBuild(ctx context.Context, component
var err error

// no need to check error because it would fail already in Reconcile
pipelineRef, _ = GetBuildPipelineFromComponentAnnotation(component)
pipelineRef, _ = r.GetBuildPipelineFromComponentAnnotation(ctx, component)
pipelineAnnotationUsed := true
if pipelineRef == nil {
pipelineAnnotationUsed = false
pipelineRef, additionalPipelineParams, err = r.GetPipelineForComponent(ctx, component)
if err != nil {
return err
Expand Down Expand Up @@ -87,7 +89,7 @@ func (r *ComponentBuildReconciler) SubmitNewBuild(ctx context.Context, component
return err
}

buildPipelineRun, err := generatePipelineRunForComponent(component, pipelineRef, additionalPipelineParams, buildGitInfo)
buildPipelineRun, err := generatePipelineRunForComponent(component, pipelineRef, additionalPipelineParams, buildGitInfo, pipelineAnnotationUsed)
if err != nil {
log.Error(err, fmt.Sprintf("Failed to generate PipelineRun to build %s component in %s namespace", component.Name, component.Namespace))
return err
Expand Down Expand Up @@ -199,7 +201,7 @@ func (r *ComponentBuildReconciler) getBuildGitInfo(ctx context.Context, componen
}, nil
}

func generatePipelineRunForComponent(component *appstudiov1alpha1.Component, pipelineRef *tektonapi.PipelineRef, additionalPipelineParams []tektonapi.Param, pRunGitInfo *buildGitInfo) (*tektonapi.PipelineRun, error) {
func generatePipelineRunForComponent(component *appstudiov1alpha1.Component, pipelineRef *tektonapi.PipelineRef, additionalPipelineParams []tektonapi.Param, pRunGitInfo *buildGitInfo, pipelineAnnotationUsed bool) (*tektonapi.PipelineRun, error) {
timestamp := time.Now().Unix()
pipelineGenerateName := fmt.Sprintf("%s-", component.Name)
gitBranch := ""
Expand Down Expand Up @@ -241,8 +243,7 @@ func generatePipelineRunForComponent(component *appstudiov1alpha1.Component, pip
}

// no need to check error because it would fail already in Reconcile
pipelineAnnotationUsed, _ := GetBuildPipelineFromComponentAnnotation(component)
if pipelineAnnotationUsed == nil {
if !pipelineAnnotationUsed {
dockerFile, err := DevfileSearchForDockerfile([]byte(component.Status.Devfile))
if err != nil {
return nil, boerrors.NewBuildOpError(boerrors.EInvalidDevfile, err)
Expand Down
131 changes: 131 additions & 0 deletions controllers/component_build_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,59 @@ var _ = Describe("Component initial build controller", func() {
expectPacBuildStatus(resourcePacPrepKey, "enabled", 0, "", mergeUrl)
})

It("should successfully submit PR with PaC definitions using GitHub application, using 'latest' bundle", 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
}

createDefaultBuildPipelineConfigMap(defaultPipelineConfigMapKey)
annotationValue := fmt.Sprintf("{\"name\":\"%s\",\"bundle\":\"%s\"}", defaultPipelineName, "latest")
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)
deleteBuildPipelineConfigMap(defaultPipelineConfigMapKey)
})

It("should fail to submit PR if build pipeline annotation isn't valid json", func() {
annotations := map[string]string{defaultBuildPipelineAnnotation: "wrong"}
createCustomComponentWithBuildRequestWithoutDevfile(componentConfig{
Expand All @@ -201,6 +254,44 @@ var _ = Describe("Component initial build controller", func() {
errorMessage := fmt.Sprintf("%d: %s", expectError.GetErrorId(), expectError.ShortError())
Expect(buildStatus.Message).To(ContainSubstring(errorMessage))
})

It("should fail to submit PR if build pipeline annotation has non existing pipeline", func() {
createDefaultBuildPipelineConfigMap(defaultPipelineConfigMapKey)
annotationValue := fmt.Sprintf("{\"name\":\"%s\",\"bundle\":\"%s\"}", "wrong-pipeline", "latest")
annotations := map[string]string{defaultBuildPipelineAnnotation: annotationValue}
createCustomComponentWithBuildRequestWithoutDevfile(componentConfig{
componentKey: resourcePacPrepKey,
annotations: annotations,
}, BuildRequestConfigurePaCAnnotationValue)
waitComponentAnnotationGone(resourcePacPrepKey, BuildRequestAnnotationName)

expectError := boerrors.NewBuildOpError(boerrors.EBuildPipelineInvalid, nil)

buildStatus := readBuildStatus(getComponent(resourcePacPrepKey))
Expect(buildStatus).ToNot(BeNil())
errorMessage := fmt.Sprintf("%d: %s", expectError.GetErrorId(), expectError.ShortError())
Expect(buildStatus.Message).To(ContainSubstring(errorMessage))
deleteBuildPipelineConfigMap(defaultPipelineConfigMapKey)
})

It("should fail to submit PR if build pipeline annotation is missing bundle and name", func() {
createDefaultBuildPipelineConfigMap(defaultPipelineConfigMapKey)
annotationValue := "{\"some\":\"wrong\"}"
annotations := map[string]string{defaultBuildPipelineAnnotation: annotationValue}
createCustomComponentWithBuildRequestWithoutDevfile(componentConfig{
componentKey: resourcePacPrepKey,
annotations: annotations,
}, BuildRequestConfigurePaCAnnotationValue)
waitComponentAnnotationGone(resourcePacPrepKey, BuildRequestAnnotationName)

expectError := boerrors.NewBuildOpError(boerrors.EWrongPipelineAnnotation, nil)

buildStatus := readBuildStatus(getComponent(resourcePacPrepKey))
Expect(buildStatus).ToNot(BeNil())
errorMessage := fmt.Sprintf("%d: %s", expectError.GetErrorId(), expectError.ShortError())
Expect(buildStatus.Message).To(ContainSubstring(errorMessage))
deleteBuildPipelineConfigMap(defaultPipelineConfigMapKey)
})
})

// when build pipeline selector will be removed
Expand Down Expand Up @@ -1444,6 +1535,46 @@ var _ = Describe("Component initial build controller", func() {
Expect(pipelineRun.Annotations["build.appstudio.redhat.com/pipeline_name"]).To(Equal(defaultPipelineName))
Expect(pipelineRun.Annotations["build.appstudio.redhat.com/bundle"]).To(Equal(defaultPipelineBundle))
})

It("should submit initial build on component creation, using 'latest' bundle", 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
}

createDefaultBuildPipelineConfigMap(defaultPipelineConfigMapKey)
annotationValue := fmt.Sprintf("{\"name\":\"%s\",\"bundle\":\"%s\"}", defaultPipelineName, "latest")
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))

deleteBuildPipelineConfigMap(defaultPipelineConfigMapKey)
})
})

// when build pipeline selector will be removed
Expand Down
Loading

0 comments on commit d501cc6

Please sign in to comment.