Skip to content

Commit

Permalink
Add ADO pipeline preview run feature and test (kyma-project#9462)
Browse files Browse the repository at this point in the history
* Add sign-only mode.

* POC of image building in ADO.

* go mod tidy

* Use default build config.

* POC of image building in ADO.

* Use default build config.

* fix linter errors

* rendertemplates

* Move ado code to separate pacakge and make it testable.

* Move build in ado code to separate package. This can be tested independent and reused in other tools.

* Tests.
TODOs for needed improvements.
Comments.
Small changes in code.

* go mod tidy

* Added building image-builder image to the prowjob.

* Use go buildpack. Chainguard offers free access to the latest tag only.

* Moved Dockerfile back to original location. images directory is not suitable for building image-builder.

* Unexport options fields.

* Remove testing prowjob.

* Add preview run flag.

* Add ADO pipeline preview run feature and test

Added a feature in the image builder that allows ADO (Azure DevOps) pipeline to run in preview mode. This allows users to see the final YAML of the pipeline before executing it. The use of this option can be flagged with adoPreviewRun and it has been limited to work only when running in ADO and not locally. Made companion changes to the tests for these features ensuring all new code is covered. This addition was made to aid debug and development efforts by providing more comprehensive information about pipeline executions in ADO.

* package not used in image-builder

* File committed by mistake.

* go mod tidy

* Pull number should be expect only for presubmit job types.

* Print exit code in new line.

* Align with naming standard.

* Add error handling for nil final yaml in ADO pipeline preview run

This update adds an error message when the final yaml in the Azure DevOps (ADO) pipeline preview run is nil. This handling is necessary to give clearer feedback when the pipeline preview run fails due to a nil final yaml and to prevent runtime errors.

* Add preview run option for Azure DevOps pipelines

Refactored the Azure DevOps pipeline trigger functionality to support a "preview run" mode where users can see the generated pipeline yaml before running the actual pipeline.

This feature is useful in creating or troubleshooting pipelines in Azure DevOps as it enables users to verify and adjust the yaml configuration before executing a pipeline run. This reduces the risk of pipeline failures due to misconfigurations.

Added a new flag that allows the user to specify the path of a yaml file that contains the pipeline definition for the preview run. Made necessary changes in cmd/image-builder/main.go and pkg/azuredevops/pipelines/pipelines.go.

Also modified some tests to accommodate these changes and ensure the functionality is working as expected in both preview run and standard run modes.

* Review comments.

* Added missing negation.
Aligned variable name in if condition.
  • Loading branch information
dekiel committed Dec 16, 2023
1 parent 878301c commit 87f9b74
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 46 deletions.
66 changes: 53 additions & 13 deletions cmd/image-builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ type options struct {
platforms sets.Strings
exportTags bool
// signOnly only sign images. No build will be performed.
signOnly bool
imagesToSign sets.Strings
buildInADO bool
parseTagsOnly bool
signOnly bool
imagesToSign sets.Strings
buildInADO bool
adoPreviewRun bool
adoPreviewRunYamlPath string
parseTagsOnly bool
}

const (
Expand Down Expand Up @@ -200,11 +202,13 @@ func prepareADOTemplateParameters(options options) (adopipelines.OCIImageBuilder
return nil, fmt.Errorf("JOB_TYPE environment variable is not set to valid value, please set it to either 'presubmit' or 'postsubmit'")
}

number, present := os.LookupEnv("PULL_NUMBER")
if !present {
pullNumber, isPullNumberSet := os.LookupEnv("PULL_NUMBER")
if jobType == "presubmit" && !isPullNumberSet {
return nil, fmt.Errorf("PULL_NUMBER environment variable is not set, please set it to valid pull request number")
}
templateParameters.SetPullNumber(number)
if isPullNumberSet {
templateParameters.SetPullNumber(pullNumber)
}

baseSHA, present := os.LookupEnv("PULL_BASE_SHA")
if !present {
Expand Down Expand Up @@ -254,22 +258,43 @@ func buildInADO(o options) error {
if err != nil {
return fmt.Errorf("build in ADO failed, failed preparing ADO template parameters, err: %s", err)
}
fmt.Printf("Using TemplateParameters: %+v\n", templateParameters)

fmt.Println("Running ADO pipeline.")
adoClient := adopipelines.NewClient(o.AdoConfig.ADOOrganizationURL, adoPAT)

var opts []adopipelines.RunPipelineArgsOptions
if o.adoPreviewRun {
fmt.Println("Running in preview mode.")
opts = append(opts, adopipelines.PipelinePreviewRun(o.adoPreviewRunYamlPath))
}

fmt.Println("Preparing ADO pipeline run arguments.")
runPipelineArgs, err := adopipelines.NewRunPipelineArgs(templateParameters, o.AdoConfig.GetADOConfig(), opts...)
if err != nil {
return fmt.Errorf("build in ADO failed, failed creating ADO pipeline run args, err: %s", err)
}

fmt.Println("Triggering ADO build pipeline")
ctx := context.Background()
pipelineRun, err := adopipelines.Run(ctx, adoClient, templateParameters, o.AdoConfig.GetADOConfig())
pipelineRun, err := adoClient.RunPipeline(ctx, runPipelineArgs)
if err != nil {
return fmt.Errorf("build in ADO failed, failed running ADO pipeline, err: %s", err)
}

if o.adoPreviewRun {
if pipelineRun.FinalYaml != nil {
fmt.Printf("ADO pipeline preview run final yaml\n: %s", *pipelineRun.FinalYaml)
} else {
fmt.Println("ADO pipeline preview run final yaml is empty")
}
return nil
}

pipelineRunResult, err := adopipelines.GetRunResult(ctx, adoClient, o.AdoConfig.GetADOConfig(), pipelineRun.Id, 30*time.Second)
if err != nil {
return fmt.Errorf("build in ADO failed, failed getting ADO pipeline run result, err: %s", err)
}
fmt.Printf("ADO pipeline run finished with status: %s", *pipelineRunResult)
fmt.Printf("ADO pipeline run finished with status: %s\n", *pipelineRunResult)

fmt.Println("Getting ADO pipeline run logs.")
adoBuildClient, err := adopipelines.NewBuildClient(o.AdoConfig.ADOOrganizationURL, adoPAT)
Expand Down Expand Up @@ -565,13 +590,25 @@ func validateOptions(o options) error {
}

if o.envFile != "" && o.buildInADO {
errs = append(errs, fmt.Errorf("envFile flag is not supported when running in ADO"))
errs = append(errs, fmt.Errorf("env-file flag is not supported when running in ADO"))
}

if o.variant != "" && o.buildInADO {
errs = append(errs, fmt.Errorf("variant flag is not supported when running in ADO"))
}

if o.adoPreviewRun && !o.buildInADO {
errs = append(errs, fmt.Errorf("ado-preview-run flag is not supported when running locally"))
}

if o.adoPreviewRun && o.adoPreviewRunYamlPath == "" {
errs = append(errs, fmt.Errorf("ado-preview-run-yaml-path flag is missing, please provide path to yaml file with ADO pipeline definition"))
}

if o.adoPreviewRunYamlPath != "" && !o.adoPreviewRun {
errs = append(errs, fmt.Errorf("ado-preview-run-yaml-path flag is provided, but adoPreviewRun flag is not set to true"))
}

return errutil.NewAggregate(errs)
}

Expand Down Expand Up @@ -643,6 +680,8 @@ func (o *options) gatherOptions(flagSet *flag.FlagSet) *flag.FlagSet {
flagSet.BoolVar(&o.signOnly, "sign-only", false, "Only sign the image, do not build it")
flagSet.Var(&o.imagesToSign, "images-to-sign", "Comma-separated list of images to sign. Only used when sign-only flag is set")
flagSet.BoolVar(&o.buildInADO, "build-in-ado", false, "Build in Azure DevOps pipeline environment")
flagSet.BoolVar(&o.adoPreviewRun, "ado-preview-run", false, "Trigger ADO pipeline in preview mode")
flagSet.StringVar(&o.adoPreviewRunYamlPath, "ado-preview-run-yaml-path", "", "Path to yaml file with ADO pipeline definition to be used in preview mode")
flagSet.BoolVar(&o.parseTagsOnly, "parse-tags-only", false, "Only parse tags and print them to stdout")

return flagSet
Expand Down Expand Up @@ -715,15 +754,16 @@ func main() {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("%s", jsonTags)
fmt.Printf("%s\n", jsonTags)
os.Exit(0)
}
if o.buildInADO {
err = buildInADO(o)
if err != nil {
fmt.Printf("Image build failed with error: %s", err)
fmt.Printf("Image build failed with error: %s\n", err)
os.Exit(1)
}
os.Exit(0)
}
err = buildLocally(o)
if err != nil {
Expand Down
59 changes: 59 additions & 0 deletions cmd/image-builder/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,62 @@ type mockSigner struct {
func (m *mockSigner) Sign(images []string) error {
return m.signFunc(images)
}

// TODO: add tests for functions related to execution in ado.
// Test copied from pkg/azuredevops/pipelines/pipelines_test.go, rewrite to run it here.
// Describe("Run", func() {
// var (
// templateParams map[string]string
// runPipelineArgs adoPipelines.RunPipelineArgs
// )
//
// BeforeEach(func() {
// templateParams = map[string]string{"param1": "value1", "param2": "value2"}
// runPipelineArgs = adoPipelines.RunPipelineArgs{
// Project: &adoConfig.ADOProjectName,
// PipelineId: &adoConfig.ADOPipelineID,
// RunParameters: &adoPipelines.RunPipelineParameters{
// PreviewRun: ptr.To(false),
// TemplateParameters: &templateParams,
// },
// PipelineVersion: &adoConfig.ADOPipelineVersion,
// }
// })
//
// It("should run the pipeline", func() {
// mockRun := &adoPipelines.Run{Id: ptr.To(123)}
// mockADOClient.On("RunPipeline", ctx, runPipelineArgs).Return(mockRun, nil)
//
// run, err := pipelines.Run(ctx, mockADOClient, templateParams, adoConfig)
// Expect(err).ToNot(HaveOccurred())
// Expect(run.Id).To(Equal(ptr.To(123)))
// mockADOClient.AssertCalled(t, "RunPipeline", ctx, runPipelineArgs)
// mockADOClient.AssertNumberOfCalls(t, "RunPipeline", 1)
// mockADOClient.AssertExpectations(GinkgoT())
// })
//
// It("should handle ADO client error", func() {
// mockADOClient.On("RunPipeline", ctx, runPipelineArgs).Return(nil, fmt.Errorf("ADO client error"))
//
// _, err := pipelines.Run(ctx, mockADOClient, templateParams, adoConfig)
// Expect(err).To(HaveOccurred())
// mockADOClient.AssertCalled(t, "RunPipeline", ctx, runPipelineArgs)
// mockADOClient.AssertNumberOfCalls(t, "RunPipeline", 1)
// mockADOClient.AssertExpectations(GinkgoT())
// })
//
// It("should run the pipeline in preview mode", func() {
// finalYaml := "pipeline:\n stages:\n - stage: Build\n jobs:\n - job: Build\n steps:\n - script: echo Hello, world!\n displayName: 'Run a one-line script'"
// runPipelineArgs.RunParameters.PreviewRun = ptr.To(true)
// mockRun := &adoPipelines.Run{Id: ptr.To(123), FinalYaml: &finalYaml}
// mockADOClient.On("RunPipeline", ctx, runPipelineArgs).Return(mockRun, nil)
//
// run, err := pipelines.Run(ctx, mockADOClient, templateParams, adoConfig, pipelines.PipelinePreviewRun)
// Expect(err).ToNot(HaveOccurred())
// Expect(run.Id).To(Equal(ptr.To(123)))
// Expect(run.FinalYaml).To(Equal(&finalYaml))
// mockADOClient.AssertCalled(t, "RunPipeline", ctx, runPipelineArgs)
// mockADOClient.AssertNumberOfCalls(t, "RunPipeline", 1)
// mockADOClient.AssertExpectations(GinkgoT())
// })
// })
28 changes: 25 additions & 3 deletions pkg/azuredevops/pipelines/pipelines.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"os"
"time"

adov7 "github.com/microsoft/azure-devops-go-api/azuredevops/v7"
Expand Down Expand Up @@ -109,7 +110,7 @@ func GetRunLogs(ctx context.Context, buildClient BuildClient, httpClient HTTPCli
return string(body), nil
}

func Run(ctx context.Context, adoClient Client, templateParameters map[string]string, adoConfig Config) (*pipelines.Run, error) {
func NewRunPipelineArgs(templateParameters map[string]string, adoConfig Config, pipelineRunArgs ...RunPipelineArgsOptions) (pipelines.RunPipelineArgs, error) {
adoRunPipelineArgs := pipelines.RunPipelineArgs{
Project: &adoConfig.ADOProjectName,
PipelineId: &adoConfig.ADOPipelineID,
Expand All @@ -121,7 +122,28 @@ func Run(ctx context.Context, adoClient Client, templateParameters map[string]st
if adoConfig.ADOPipelineVersion != 0 {
adoRunPipelineArgs.PipelineVersion = &adoConfig.ADOPipelineVersion
}
for _, arg := range pipelineRunArgs {
err := arg(&adoRunPipelineArgs)
if err != nil {
return pipelines.RunPipelineArgs{}, fmt.Errorf("failed setting pipeline run args, err: %w", err)
}
}
// TODO: use structured logging with debug severity
fmt.Printf("Using TemplateParameters: %+v\n", adoRunPipelineArgs.RunParameters.TemplateParameters)
return adoClient.RunPipeline(ctx, adoRunPipelineArgs)
// fmt.Printf("Using TemplateParameters: %+v\n", adoRunPipelineArgs.RunParameters.TemplateParameters)
return adoRunPipelineArgs, nil
}

type RunPipelineArgsOptions func(*pipelines.RunPipelineArgs) error

func PipelinePreviewRun(overrideYamlPath string) func(args *pipelines.RunPipelineArgs) error {
return func(args *pipelines.RunPipelineArgs) error {
args.RunParameters.PreviewRun = ptr.To(true)
data, err := os.ReadFile(overrideYamlPath)
if err != nil {
return fmt.Errorf("failed reading override yaml file, err: %w", err)
}
overrideYaml := string(data)
args.RunParameters.YamlOverride = &overrideYaml
return nil
}
}
Loading

0 comments on commit 87f9b74

Please sign in to comment.