Skip to content

Commit

Permalink
Merge pull request #288 from rcerven/pipeline_annotation
Browse files Browse the repository at this point in the history
Consume default build pipeline bundle from Component annotation
  • Loading branch information
rcerven authored May 10, 2024
2 parents fd2513f + 96c5f72 commit d1ed9ce
Show file tree
Hide file tree
Showing 8 changed files with 713 additions and 39 deletions.
56 changes: 47 additions & 9 deletions controllers/component_build_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const (
buildPipelineServiceAccountName = "appstudio-pipeline"

buildPipelineSelectorResourceName = "build-pipeline-selector"
defaultBuildPipelineAnnotation = "build.appstudio.openshift.io/pipeline"
)

type BuildStatus struct {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
46 changes: 46 additions & 0 deletions controllers/component_build_controller_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
45 changes: 33 additions & 12 deletions controllers/component_build_controller_pac.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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}})
}
Expand Down
45 changes: 33 additions & 12 deletions controllers/component_build_controller_simple_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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}})
}
Expand Down
Loading

0 comments on commit d1ed9ce

Please sign in to comment.