diff --git a/cmd/ci-operator/main_test.go b/cmd/ci-operator/main_test.go index 5c41913aa3f..d2ab10c4ebe 100644 --- a/cmd/ci-operator/main_test.go +++ b/cmd/ci-operator/main_test.go @@ -1004,7 +1004,7 @@ func TestBuildPartialGraph(t *testing.T) { loggingclient.New(fakectrlruntimeclient.NewClientBuilder().WithRuntimeObjects(&imagev1.ImageStreamTag{ObjectMeta: metav1.ObjectMeta{Name: ":"}}).Build(), nil), nil, ), - steps.SourceStep(api.SourceStepConfiguration{From: api.PipelineImageStreamTagReferenceRoot, To: api.PipelineImageStreamTagReferenceSource}, api.ResourceConfiguration{}, nil, nil, &api.JobSpec{}, nil, nil, nil), + steps.SourceStep(api.SourceStepConfiguration{From: api.PipelineImageStreamTagReferenceRoot, To: api.PipelineImageStreamTagReferenceSource}, api.ResourceConfiguration{}, nil, nil, &api.JobSpec{}, nil, nil, nil, "openshift"), steps.ProjectDirectoryImageBuildStep( api.ProjectDirectoryImageBuildStepConfiguration{ From: api.PipelineImageStreamTagReferenceSource, diff --git a/go.mod b/go.mod index 549f00d1b88..32874fd403f 100644 --- a/go.mod +++ b/go.mod @@ -164,13 +164,15 @@ require ( cloud.google.com/go/resourcemanager v1.10.3 cloud.google.com/go/secretmanager v1.14.5 github.com/GoogleCloudPlatform/secrets-store-csi-driver-provider-gcp v1.7.0 - github.com/aws/aws-sdk-go-v2 v1.36.3 + github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.29.14 github.com/aws/aws-sdk-go-v2/credentials v1.17.67 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.0 + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0 + github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.13 github.com/aws/aws-sdk-go-v2/service/ec2 v1.194.0 github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 - github.com/aws/smithy-go v1.22.2 + github.com/aws/smithy-go v1.24.2 github.com/coreos/stream-metadata-go v0.1.8 github.com/estesp/manifest-tool/v2 v2.1.8 github.com/felixge/httpsnoop v1.0.4 @@ -231,11 +233,11 @@ require ( github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect diff --git a/go.sum b/go.sum index 1e59e277992..cb4224b461b 100644 --- a/go.sum +++ b/go.sum @@ -172,10 +172,10 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l github.com/aws/aws-sdk-go v1.42.23/go.mod h1:gyRszuZ/icHmHAVE4gc/r+cfCmhA1AD+vqfWbgI+eHs= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= @@ -184,16 +184,20 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mln github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg= github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.0 h1:zmXJiEm/fQYtFDLIUsZrcPIjTrL3R/noFICGlYBj3Ww= github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.0/go.mod h1:9nOjXCDKE+QMK4JaCrLl36PU+VEfJmI7WVehYmojO8s= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0 h1:+/lmB/+i2oqkzbmlQxsW0kr/+wmJgmyiEF9VDJicX34= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0/go.mod h1:PobeppEnIjw4pcgjFryNDZCTH7AiqZw0yb5r98Gvf9c= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.13 h1:EEdmtkVROLA9VniV5STKv/EfEgV+n9NFBpOYU1jN9As= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.13/go.mod h1:+pwMMAvpmRuI7oHsTT2F5Lrp4ZQV2RF7b6tiaBj3Ugk= github.com/aws/aws-sdk-go-v2/service/ec2 v1.194.0 h1:56YXcRmryw9wiTrvdVeJEUwBCoN/+o33R52PA7CCi08= github.com/aws/aws-sdk-go-v2/service/ec2 v1.194.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= @@ -212,8 +216,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0c github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/bazelbuild/buildtools v0.0.0-20200922170545-10384511ce98 h1:OhVnC5zU5QHQ+DUSmgOTPqPnJnrlFmrh2S0HKeHmpbw= github.com/bazelbuild/buildtools v0.0.0-20200922170545-10384511ce98/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= diff --git a/pkg/api/config.go b/pkg/api/config.go index ae58ba32452..d259d9af3d4 100644 --- a/pkg/api/config.go +++ b/pkg/api/config.go @@ -57,6 +57,10 @@ func (config *ReleaseBuildConfiguration) Default() { for i := range config.Tests { defTest(&config.Tests[i]) } + + if config.BuildType == "" { + config.BuildType = "openshift" + } } // ImageStreamFor guesses at the ImageStream that will hold a tag. diff --git a/pkg/api/types.go b/pkg/api/types.go index 1af626139c2..b1729d8349b 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -75,6 +75,8 @@ type ReleaseBuildConfiguration struct { InputConfiguration `json:",inline"` + BuildType string `json:"build_type,omitempty"` + // BinaryBuildCommands will create a "bin" image based on "src" that // contains the output of this command. This allows reuse of binary artifacts // across other steps. If empty, no "bin" image will be created. diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index 9a8a5594a95..56dd5363678 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -201,13 +201,13 @@ func fromConfig(ctx context.Context, cfg *Config) ([]api.Step, []api.Step, error inputImages[conf.InputImage] = struct{}{} } else if rawStep.PipelineImageCacheStepConfiguration != nil { skippedBinaries := filterRequiredBinariesFromSkipped(cfg.CIConfig.Images.Items, cfg.SkippedImages) - step = steps.PipelineImageCacheStep(*rawStep.PipelineImageCacheStepConfiguration, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.MetricsAgent, skippedBinaries) + step = steps.PipelineImageCacheStep(*rawStep.PipelineImageCacheStepConfiguration, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.MetricsAgent, skippedBinaries, cfg.CIConfig.BuildType) } else if rawStep.SourceStepConfiguration != nil { - step = steps.SourceStep(*rawStep.SourceStepConfiguration, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.CloneAuthConfig, cfg.PullSecret, cfg.MetricsAgent) + step = steps.SourceStep(*rawStep.SourceStepConfiguration, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.CloneAuthConfig, cfg.PullSecret, cfg.MetricsAgent, cfg.CIConfig.BuildType) } else if rawStep.BundleSourceStepConfiguration != nil { - step = steps.BundleSourceStep(*rawStep.BundleSourceStepConfiguration, cfg.CIConfig, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret) + step = steps.BundleSourceStep(*rawStep.BundleSourceStepConfiguration, cfg.CIConfig, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.CIConfig.BuildType) } else if rawStep.IndexGeneratorStepConfiguration != nil { - step = steps.IndexGeneratorStep(*rawStep.IndexGeneratorStepConfiguration, cfg.CIConfig, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.MetricsAgent) + step = steps.IndexGeneratorStep(*rawStep.IndexGeneratorStepConfiguration, cfg.CIConfig, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.MetricsAgent, cfg.CIConfig.BuildType) } else if rawStep.ProjectDirectoryImageBuildStepConfiguration != nil { imgConfig := rawStep.ProjectDirectoryImageBuildStepConfiguration if cfg.SkippedImages.Has(string(imgConfig.To)) { @@ -216,9 +216,9 @@ func fromConfig(ctx context.Context, cfg *Config) ([]api.Step, []api.Step, error } step = steps.ProjectDirectoryImageBuildStep(*imgConfig, cfg.CIConfig, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.MetricsAgent) } else if rawStep.ProjectDirectoryImageBuildInputs != nil { - step = steps.GitSourceStep(*rawStep.ProjectDirectoryImageBuildInputs, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.CloneAuthConfig, cfg.PullSecret, cfg.MetricsAgent) + step = steps.GitSourceStep(*rawStep.ProjectDirectoryImageBuildInputs, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.CloneAuthConfig, cfg.PullSecret, cfg.MetricsAgent, cfg.CIConfig.BuildType) } else if rawStep.RPMImageInjectionStepConfiguration != nil { - step = steps.RPMImageInjectionStep(*rawStep.RPMImageInjectionStepConfiguration, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.MetricsAgent) + step = steps.RPMImageInjectionStep(*rawStep.RPMImageInjectionStepConfiguration, cfg.CIConfig.Resources, cfg.buildClient, cfg.podClient, cfg.JobSpec, cfg.PullSecret, cfg.MetricsAgent, cfg.CIConfig.BuildType) } else if rawStep.RPMServeStepConfiguration != nil { step = steps.RPMServerStep(*rawStep.RPMServeStepConfiguration, cfg.kubeClient, cfg.JobSpec) } else if rawStep.OutputImageTagStepConfiguration != nil { diff --git a/pkg/steps/bundle_source.go b/pkg/steps/bundle_source.go index e41d2256b49..5ec95572b7d 100644 --- a/pkg/steps/bundle_source.go +++ b/pkg/steps/bundle_source.go @@ -26,6 +26,7 @@ type bundleSourceStep struct { podClient kubernetes.PodClient jobSpec *api.JobSpec pullSecret *coreapi.Secret + buildType string } func (s *bundleSourceStep) Inputs() (api.InputDefinition, error) { @@ -82,7 +83,7 @@ func (s *bundleSourceStep) run(ctx context.Context) error { // Bundle images are not multi-arch by design. Here we build it without creating a manifest-listed image. // Note that we are not configuring a node selector here, so the build will be scheduled on any available // node no matter the architecture. - return handleBuild(ctx, s.client, s.podClient, *build) + return handleBuild(ctx, s.client, s.podClient, *build, s.buildType) } func replaceCommand(pullSpec, with string) string { @@ -144,6 +145,7 @@ func BundleSourceStep( podClient kubernetes.PodClient, jobSpec *api.JobSpec, pullSecret *coreapi.Secret, + buildType string, ) api.Step { return &bundleSourceStep{ config: config, @@ -153,5 +155,6 @@ func BundleSourceStep( podClient: podClient, jobSpec: jobSpec, pullSecret: pullSecret, + buildType: buildType, } } diff --git a/pkg/steps/git_source.go b/pkg/steps/git_source.go index a92cda826ee..22b34fa9a92 100644 --- a/pkg/steps/git_source.go +++ b/pkg/steps/git_source.go @@ -27,6 +27,7 @@ type gitSourceStep struct { pullSecret *coreapi.Secret architectures sets.Set[string] metricsAgent *metrics.MetricsAgent + buildType string } func (s *gitSourceStep) Inputs() (api.InputDefinition, error) { @@ -52,7 +53,7 @@ func (s *gitSourceStep) run(ctx context.Context) error { if s.config.Ref != "" { root = fmt.Sprintf("%s-%s", root, s.config.Ref) } - return handleBuilds(ctx, s.buildClient, s.podClient, *buildFromSource(s.jobSpec, "", api.PipelineImageStreamTagReference(root), buildapi.BuildSource{ + return handleBuilds(ctx, s.buildClient, s.podClient, s.buildType, *buildFromSource(s.jobSpec, "", api.PipelineImageStreamTagReference(root), buildapi.BuildSource{ Type: buildapi.BuildSourceGit, Dockerfile: s.config.DockerfileLiteral, ContextDir: s.config.ContextDir, @@ -142,6 +143,7 @@ func GitSourceStep( cloneAuthConfig *CloneAuthConfig, pullSecret *coreapi.Secret, metricsAgent *metrics.MetricsAgent, + buildType string, ) api.Step { return &gitSourceStep{ config: config, @@ -153,5 +155,6 @@ func GitSourceStep( pullSecret: pullSecret, architectures: sets.New[string](), metricsAgent: metricsAgent, + buildType: buildType, } } diff --git a/pkg/steps/index_generator.go b/pkg/steps/index_generator.go index 3025b539351..d0cbc9dcd47 100644 --- a/pkg/steps/index_generator.go +++ b/pkg/steps/index_generator.go @@ -32,6 +32,7 @@ type indexGeneratorStep struct { pullSecret *coreapi.Secret architectures sets.Set[string] metricsAgent *metrics.MetricsAgent + buildType string } const IndexDataDirectory = "/index-data" @@ -126,7 +127,7 @@ func (s *indexGeneratorStep) run(ctx context.Context) error { nil, "", ) - err = handleBuilds(ctx, s.client, s.podClient, *build, s.metricsAgent, newImageBuildOptions(s.architectures.UnsortedList())) + err = handleBuilds(ctx, s.client, s.podClient, s.buildType, *build, s.metricsAgent, newImageBuildOptions(s.architectures.UnsortedList())) if err != nil && strings.Contains(err.Error(), "error checking provided apis") { return results.ForReason("generating_index").WithError(err).Errorf("failed to generate operator index due to invalid bundle info: %v", err) } @@ -217,6 +218,7 @@ func IndexGeneratorStep( jobSpec *api.JobSpec, pullSecret *coreapi.Secret, metricsAgent *metrics.MetricsAgent, + buildType string, ) api.Step { return &indexGeneratorStep{ config: config, @@ -228,5 +230,6 @@ func IndexGeneratorStep( pullSecret: pullSecret, architectures: sets.New[string](), metricsAgent: metricsAgent, + buildType: buildType, } } diff --git a/pkg/steps/pipeline_image_cache.go b/pkg/steps/pipeline_image_cache.go index b367147f5fd..7e38c776b77 100644 --- a/pkg/steps/pipeline_image_cache.go +++ b/pkg/steps/pipeline_image_cache.go @@ -38,6 +38,7 @@ type pipelineImageCacheStep struct { architectures sets.Set[string] metricsAgent *metrics.MetricsAgent skippedImages sets.Set[string] + buildType string } func (s *pipelineImageCacheStep) Inputs() (api.InputDefinition, error) { @@ -75,7 +76,7 @@ func (s *pipelineImageCacheStep) run(ctx context.Context) error { build.Spec.Strategy.DockerStrategy.Env = append(build.Spec.Strategy.DockerStrategy.Env, coreapi.EnvVar{Name: SkippedImagesEnvVar, Value: strings.Join(sets.List(s.skippedImages), ",")}) } - return handleBuilds(ctx, s.client, s.podClient, *build, s.metricsAgent, newImageBuildOptions(s.architectures.UnsortedList())) + return handleBuilds(ctx, s.client, s.podClient, s.buildType, *build, s.metricsAgent, newImageBuildOptions(s.architectures.UnsortedList())) } func (s *pipelineImageCacheStep) Requires() []api.StepLink { @@ -122,6 +123,7 @@ func PipelineImageCacheStep( pullSecret *coreapi.Secret, metricsAgent *metrics.MetricsAgent, skippedImages sets.Set[string], + buildType string, ) api.Step { return &pipelineImageCacheStep{ config: config, @@ -133,5 +135,6 @@ func PipelineImageCacheStep( architectures: sets.New[string](), metricsAgent: metricsAgent, skippedImages: skippedImages, + buildType: buildType, } } diff --git a/pkg/steps/project_image.go b/pkg/steps/project_image.go index b9e42005f84..4eb693458c4 100644 --- a/pkg/steps/project_image.go +++ b/pkg/steps/project_image.go @@ -32,6 +32,7 @@ type projectDirectoryImageBuildStep struct { multiArch bool architectures sets.Set[string] metricsAgent *metrics.MetricsAgent + buildType string } func (s *projectDirectoryImageBuildStep) Inputs() (api.InputDefinition, error) { @@ -72,10 +73,10 @@ func (s *projectDirectoryImageBuildStep) run(ctx context.Context) error { // Bundle images are non multi-arch by design. No manifest list is needed. Here we spawn a single build. if s.config.IsBundleImage() { - return handleBuild(ctx, s.client, s.podClient, *build) + return handleBuild(ctx, s.client, s.podClient, *build, s.buildType) } - return handleBuilds(ctx, s.client, s.podClient, *build, s.metricsAgent, newImageBuildOptions(s.architectures.UnsortedList())) + return handleBuilds(ctx, s.client, s.podClient, s.buildType, *build, s.metricsAgent, newImageBuildOptions(s.architectures.UnsortedList())) } type workingDir func(tag string) (string, error) diff --git a/pkg/steps/rpm_injection.go b/pkg/steps/rpm_injection.go index 395b8a407f6..9fa9056e8f0 100644 --- a/pkg/steps/rpm_injection.go +++ b/pkg/steps/rpm_injection.go @@ -31,6 +31,7 @@ type rpmImageInjectionStep struct { pullSecret *coreapi.Secret architectures sets.Set[string] metricsAgent *metrics.MetricsAgent + buildType string } func (s *rpmImageInjectionStep) Inputs() (api.InputDefinition, error) { @@ -54,7 +55,7 @@ func (s *rpmImageInjectionStep) run(ctx context.Context) error { if err != nil { return err } - return handleBuilds(ctx, s.client, s.podClient, *buildFromSource( + return handleBuilds(ctx, s.client, s.podClient, s.buildType, *buildFromSource( s.jobSpec, s.config.From, s.config.To, buildapi.BuildSource{ Type: buildapi.BuildSourceDockerfile, @@ -107,6 +108,7 @@ func RPMImageInjectionStep( jobSpec *api.JobSpec, pullSecret *coreapi.Secret, metricsAgent *metrics.MetricsAgent, + buildType string, ) api.Step { return &rpmImageInjectionStep{ config: config, @@ -117,5 +119,6 @@ func RPMImageInjectionStep( pullSecret: pullSecret, architectures: sets.New[string](), metricsAgent: metricsAgent, + buildType: buildType, } } diff --git a/pkg/steps/source.go b/pkg/steps/source.go index 9ea8b4f0186..1f8a3d08ad7 100644 --- a/pkg/steps/source.go +++ b/pkg/steps/source.go @@ -5,14 +5,22 @@ import ( "fmt" "io" "os" + "path/filepath" + "regexp" "sort" "strings" "sync" "sync/atomic" "time" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/openshift/ci-tools/pkg/kubernetes/pkg/credentialprovider" + "github.com/openshift/ci-tools/pkg/util/buildspec" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/utils/ptr" + "sigs.k8s.io/yaml" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -30,6 +38,10 @@ import ( buildapi "github.com/openshift/api/build/v1" imagev1 "github.com/openshift/api/image/v1" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go-v2/service/codebuild" + codebuildtypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types" "github.com/openshift/ci-tools/pkg/api" apiutils "github.com/openshift/ci-tools/pkg/api/utils" "github.com/openshift/ci-tools/pkg/kubernetes" @@ -173,6 +185,7 @@ type sourceStep struct { pullSecret *corev1.Secret architectures sets.Set[string] metricsAgent *metrics.MetricsAgent + buildType string } func (s *sourceStep) Inputs() (api.InputDefinition, error) { @@ -196,6 +209,7 @@ func (s *sourceStep) run(ctx context.Context) error { ctx, s.client, s.podClient, + s.buildType, *createBuild(s.config, s.jobSpec, clonerefsRef, s.resources, s.cloneAuthConfig, s.pullSecret, fromDigest), s.metricsAgent, newImageBuildOptions(s.architectures.UnsortedList()), ) } @@ -478,7 +492,7 @@ func newImageBuildOptions(archs []string) ImageBuildOptions { return ImageBuildOptions{Architectures: archs} } -func handleBuilds(ctx context.Context, buildClient BuildClient, podClient kubernetes.PodClient, build buildapi.Build, metricsAgent *metrics.MetricsAgent, opts ...ImageBuildOptions) error { +func handleBuilds(ctx context.Context, buildClient BuildClient, podClient kubernetes.PodClient, buildType string, build buildapi.Build, metricsAgent *metrics.MetricsAgent, opts ...ImageBuildOptions) error { var wg sync.WaitGroup o := ImageBuildOptions{} @@ -494,7 +508,7 @@ func handleBuilds(ctx context.Context, buildClient BuildClient, podClient kubern go func(b buildapi.Build) { defer wg.Done() metricsAgent.AddNodeWorkload(ctx, b.Namespace, fmt.Sprintf("%s-build", b.Name), b.Name, podClient) - if err := handleBuild(ctx, buildClient, podClient, b); err != nil { + if err := handleBuild(ctx, buildClient, podClient, b, buildType); err != nil { errChan <- fmt.Errorf("error occurred handling build %s: %w", b.Name, err) } metricsAgent.RemoveNodeWorkload(b.Name) @@ -552,33 +566,14 @@ func constructMultiArchBuilds(build buildapi.Build, stepArchitectures []string) return ret } -func handleBuild(ctx context.Context, client BuildClient, podClient kubernetes.PodClient, build buildapi.Build) error { +func handleBuild(ctx context.Context, buildClient BuildClient, podClient kubernetes.PodClient, build buildapi.Build, buildType string) error { const attempts = 5 - ns, name := build.Namespace, build.Name var errs []error if err := wait.ExponentialBackoff(wait.Backoff{Duration: time.Minute, Factor: 1.5, Steps: attempts}, func() (bool, error) { - var attempt buildapi.Build - - build.DeepCopyInto(&attempt) - if err := client.Create(ctx, &attempt); err == nil { - logrus.Infof("Created build %q", name) - } else if kerrors.IsAlreadyExists(err) { - logrus.Infof("Found existing build %q", name) - } else { - return false, fmt.Errorf("could not create build %s: %w", name, err) - } - - client.MetricsAgent().AddNodeWorkload(ctx, ns, fmt.Sprintf("%s-build", name), name, podClient) - client.MetricsAgent().StoreMachinesSnapshotForBuildPod(ctx, ns, fmt.Sprintf("%s-build", name), podClient) - if err := waitForBuildOrTimeout(ctx, client, podClient, ns, name); err != nil { - errs = append(errs, err) - return false, handleFailedBuild(ctx, client, ns, name, err) + if buildType == "aws" { + return awsBuild(ctx, buildClient, build, &errs) } - if err := gatherSuccessfulBuildLog(client, ns, name); err != nil { - // log error but do not fail successful build - logrus.WithError(err).Warnf("Failed gathering successful build %s logs into artifacts.", name) - } - return true, nil + return openshiftBuild(ctx, buildClient, podClient, build, &errs) }); err != nil { if err == wait.ErrWaitTimeout { return fmt.Errorf("build not successful after %d attempts: %w", attempts, utilerrors.NewAggregate(errs)) @@ -848,6 +843,7 @@ func SourceStep( cloneAuthConfig *CloneAuthConfig, pullSecret *corev1.Secret, metricsAgent *metrics.MetricsAgent, + buildType string, ) api.Step { return &sourceStep{ config: config, @@ -859,6 +855,7 @@ func SourceStep( pullSecret: pullSecret, architectures: sets.New[string](), metricsAgent: metricsAgent, + buildType: buildType, } } @@ -912,3 +909,325 @@ func addLabelsToBuild(refs *prowv1.Refs, build *buildapi.Build, contextDir strin return build.Spec.Output.ImageLabels[i].Name < build.Spec.Output.ImageLabels[j].Name }) } + +func openshiftBuild(ctx context.Context, buildClient BuildClient, podClient kubernetes.PodClient, build buildapi.Build, errs *[]error) (bool, error) { + var attempt buildapi.Build + ns, name := build.Namespace, build.Name + build.DeepCopyInto(&attempt) + if err := buildClient.Create(ctx, &attempt); err == nil { + logrus.Infof("Created build %q", name) + } else if kerrors.IsAlreadyExists(err) { + logrus.Infof("Found existing build %q", name) + } else { + return false, fmt.Errorf("could not create build %s: %w", name, err) + } + + buildClient.MetricsAgent().AddNodeWorkload(ctx, ns, fmt.Sprintf("%s-build", name), name, podClient) + buildClient.MetricsAgent().StoreMachinesSnapshotForBuildPod(ctx, ns, fmt.Sprintf("%s-build", name), podClient) + if err := waitForBuildOrTimeout(ctx, buildClient, podClient, ns, name); err != nil { + *errs = append(*errs, err) + return false, handleFailedBuild(ctx, buildClient, ns, name, err) + } + if err := gatherSuccessfulBuildLog(buildClient, ns, name); err != nil { + // log error but do not fail successful build + logrus.WithError(err).Warnf("Failed gathering successful build %s logs into artifacts.", name) + } + return true, nil +} + +func awsBuild(ctx context.Context, buildClient BuildClient, build buildapi.Build, errs *[]error) (bool, error) { + projectName := fmt.Sprintf("%s-%s", build.Namespace, build.Name) + sdkConfig, err := config.LoadDefaultConfig(ctx) + if err != nil { + *errs = append(*errs, err) + return false, fmt.Errorf("could not load aws credentials: %w", err) + } + + cbClient := codebuild.NewFromConfig(sdkConfig) + projects, err := cbClient.BatchGetProjects(ctx, &codebuild.BatchGetProjectsInput{Names: []string{projectName}}) + if err != nil { + *errs = append(*errs, err) + return false, fmt.Errorf("could not get project %s: %w", projectName, err) + } + + projectExists := len(projects.Projects) > 0 + if !projectExists { + is := &imagev1.ImageStreamTag{} + credentials, err := awsBuildAssemblyCredentials(ctx, buildClient, build, is, errs) + if err != nil { + return false, err + } + buildSpec, err := awsBuildSpec(build, string(credentials), is.Image.DockerImageReference, buildClient.LocalRegistryDNS()) + if err != nil { + return false, err + } + + err = awsBuildCreateCloudBuildProject(ctx, build, cbClient, buildSpec, projectName, errs) + if err != nil { + return false, err + } + } + + sbi := &codebuild.StartBuildInput{ + ProjectName: &projectName, + } + + awsBuildResult, err := cbClient.StartBuild(ctx, sbi) + if err != nil { + *errs = append(*errs, err) + return false, fmt.Errorf("could not start build at codebuild project %s: %w", projectName, err) + } + + err = awsBuildWaitForIt(ctx, sdkConfig, cbClient, awsBuildResult, build.Name) + if err != nil { + *errs = append(*errs, err) + return false, fmt.Errorf("could not wait/pull logs for build %s: %w", build.Namespace, err) + } + + return true, nil +} + +func awsBuildSpec(build buildapi.Build, credentials string, imageStreamReference, localRegistryDNS string) (buildspec.BuildSpec, error) { + commands := []string{ + `mkdir -p ~/.docker`, + `echo "$credentials" > ~/.docker/config.json`, + `echo "$dockerfile" > Dockerfile`, + } + for i, image := range build.Spec.CommonSpec.Source.Images { + commands = append(commands, fmt.Sprintf(`docker create --name c%d %s`, i, util.ShellEscape(image.From.Name))) + for _, path := range image.Paths { + commands = append(commands, fmt.Sprintf(`docker cp c%d:%s %s`, i, util.ShellEscape(path.SourcePath), util.ShellEscape(path.DestinationDir))) + } + commands = append(commands, fmt.Sprintf(`docker rm c%d`, i)) + } + + buildEnv := []string{} + buildArgs := []string{} + for _, env := range build.Spec.CommonSpec.Strategy.DockerStrategy.Env { + buildEnv = append(buildEnv, fmt.Sprintf(`--build-arg %s='%s'`, env.Name, util.ShellEscape(env.Value))) + buildArgs = append(buildArgs, fmt.Sprintf(`ARG %s`, env.Name)) + } + + re := regexp.MustCompile(`image-registry\.openshift-image-registry\.svc[.:a-z0-9]*`) + imageStreamReference = re.ReplaceAllString(imageStreamReference, localRegistryDNS) + commands = append(commands, fmt.Sprintf(`docker buildx build %s --platform linux/%s --tag %s/%s/%s --output type=registry .`, strings.Join(buildEnv, " "), build.Spec.NodeSelector["kubernetes.io/arch"], localRegistryDNS, build.Namespace, build.Spec.Output.To.Name)) + if build.Spec.CommonSpec.Source.Dockerfile == nil { + return buildspec.BuildSpec{}, fmt.Errorf("no Dockerfile defined on build.spec") + } + dockerFile := strings.ReplaceAll(*build.Spec.CommonSpec.Source.Dockerfile, build.Spec.CommonSpec.Strategy.DockerStrategy.From.Name, fmt.Sprintf("%s\n%s", imageStreamReference, strings.Join(buildArgs, "\n"))) + + return buildspec.BuildSpec{ + Env: buildspec.Env{ + Variables: buildspec.Variables{ + DockerFile: dockerFile, + Credentials: credentials, + }, + }, + Phases: buildspec.Phases{ + Build: buildspec.BuildPhase{ + OnFailure: "ABORT", + Commands: commands, + }, + }, + Version: "0.2", + }, nil +} + +func awsBuildAssemblyCredentials(ctx context.Context, buildClient BuildClient, build buildapi.Build, is *imagev1.ImageStreamTag, errs *[]error) ([]byte, error) { + if build.Spec.Strategy.DockerStrategy == nil || build.Spec.Strategy.DockerStrategy.PullSecret == nil { + err := fmt.Errorf("build %s has no pull secret configured", build.Name) + *errs = append(*errs, err) + return nil, err + } + secretName := build.Spec.Strategy.DockerStrategy.PullSecret.Name + secret := &corev1.Secret{} + err := buildClient.Get(ctx, ctrlruntimeclient.ObjectKey{Namespace: build.Namespace, Name: secretName}, secret) + if err != nil { + *errs = append(*errs, err) + if kerrors.IsNotFound(err) { + return nil, fmt.Errorf("could not get secret %s: %w", secretName, err) + } + return nil, fmt.Errorf("could not get secret %s: %w", secretName, err) + } + + isName := build.Spec.Strategy.DockerStrategy.From.Name + err = buildClient.Get(ctx, ctrlruntimeclient.ObjectKey{Namespace: build.Namespace, Name: isName}, is) + if err != nil { + *errs = append(*errs, err) + if kerrors.IsNotFound(err) { + return nil, fmt.Errorf("could not get imagestream %s: %w", isName, err) + } + return nil, fmt.Errorf("could not get imagestream %s: %w", isName, err) + } + + dockerConfigJSON, found := secret.Data[corev1.DockerConfigJsonKey] + if !found { + *errs = append(*errs, fmt.Errorf("key %s not found on secret %s", corev1.DockerConfigJsonKey, secret.Name)) + return nil, fmt.Errorf("key %s not found on secret %s", corev1.DockerConfigJsonKey, secret.Name) + } + + manifestToolDockerCfg := buildClient.ManifestToolDockerCfg() + pushCredentialsData, err := os.ReadFile(manifestToolDockerCfg) + if err != nil { + *errs = append(*errs, err) + return nil, fmt.Errorf("could not read secret %s: %w", manifestToolDockerCfg, err) + } + pushCredentials := credentialprovider.DockerConfigJSON{} + err = json.Unmarshal(pushCredentialsData, &pushCredentials) + if err != nil { + *errs = append(*errs, err) + return nil, fmt.Errorf("could not unmarshal secret %s: %w", manifestToolDockerCfg, err) + } + + registryCredentials := credentialprovider.DockerConfigJSON{} + err = json.Unmarshal(dockerConfigJSON, ®istryCredentials) + if err != nil { + *errs = append(*errs, err) + return nil, fmt.Errorf("could not unmarshal secret %s: %w", secret.Name, err) + } + if registryCredentials.Auths == nil { + registryCredentials.Auths = map[string]credentialprovider.DockerConfigEntry{} + } + localRegistryDNS := buildClient.LocalRegistryDNS() + localRegistryAuth, exists := pushCredentials.Auths[localRegistryDNS] + if !exists { + err := fmt.Errorf("local registry auth %s not found", localRegistryDNS) + *errs = append(*errs, err) + return nil, err + } + registryCredentials.Auths[localRegistryDNS] = localRegistryAuth + marshalledRegistryCredentials, err := json.Marshal(registryCredentials) + if err != nil { + *errs = append(*errs, err) + return nil, fmt.Errorf("could not marshal manifesttooldockercfg combined with registry-pull-credentials: %w", err) + } + return marshalledRegistryCredentials, nil +} + +func awsBuildCreateCloudBuildProject(ctx context.Context, build buildapi.Build, cbClient *codebuild.Client, buildSpec buildspec.BuildSpec, projectName string, errs *[]error) error { + awsBuildImage := "aws/codebuild/amazonlinux2-x86_64-standard:6.0" + awsEnvironmentTypeContainer := codebuildtypes.EnvironmentTypeLinuxContainer + if build.Spec.NodeSelector["kubernetes.io/arch"] == "arm64" { + awsBuildImage = "aws/codebuild/amazonlinux2-aarch64-standard:3.0" + awsEnvironmentTypeContainer = codebuildtypes.EnvironmentTypeArmContainer + } + const ServiceRole string = "codebuild-ci-operator" + marshaledBuildSpec, err := yaml.Marshal(buildSpec) + if err != nil { + *errs = append(*errs, err) + return fmt.Errorf("could not marshal codebuild buildspec: %w", err) + } + logrus.Infof("Creating %s project", projectName) + _, err = cbClient.CreateProject(ctx, &codebuild.CreateProjectInput{ + Name: &projectName, + TimeoutInMinutes: ptr.To(int32(60 * 8)), + Artifacts: &codebuildtypes.ProjectArtifacts{ + Type: codebuildtypes.ArtifactsTypeNoArtifacts, + }, + Environment: &codebuildtypes.ProjectEnvironment{ + Type: awsEnvironmentTypeContainer, + Image: &awsBuildImage, + ComputeType: codebuildtypes.ComputeTypeBuildGeneral1Medium, + }, + Source: &codebuildtypes.ProjectSource{ + Type: codebuildtypes.SourceTypeNoSource, + Buildspec: ptr.To(string(marshaledBuildSpec)), + }, + ServiceRole: ptr.To(ServiceRole), + }) + if err != nil { + *errs = append(*errs, err) + return fmt.Errorf("could not create codebuild project %s: %w", projectName, err) + } + return nil +} + +func awsBuildWaitForIt(ctx context.Context, sdkConfig aws.Config, cbClient *codebuild.Client, buildOutput *codebuild.StartBuildOutput, imageName string) error { + artifactDir, set := api.Artifacts() + if !set { + return fmt.Errorf("no artifacts directory configured") + } + dir := filepath.Join(artifactDir, "build-logs") + if err := os.MkdirAll(dir, 0750); err != nil { + return fmt.Errorf("could not create artifacts/build-logs dir: %w", err) + } + logPath := filepath.Join(dir, fmt.Sprintf("%s.log", imageName)) + log, err := os.Create(logPath) + if err != nil { + return fmt.Errorf("failed to create %s: %w", logPath, err) + } + defer log.Close() + buildId := aws.ToString(buildOutput.Build.Id) + logrus.Infof("Waiting for build %s to complete", buildId) + for { + // avoid maximum number of attempts + time.Sleep(10 * time.Second) + + if ctx.Err() != nil { + return fmt.Errorf("build polling canceled: %w", ctx.Err()) + } + + buildInputs := codebuild.BatchGetBuildsInput{Ids: []string{buildId}} + builds, err := cbClient.BatchGetBuilds(ctx, &buildInputs) + if err != nil { + return fmt.Errorf("could not get build %s: %w", buildId, err) + } + + if len(builds.Builds) == 0 { + return fmt.Errorf("no builds found with id %s", buildId) + } + + build := builds.Builds[0] + status := build.BuildStatus + if status == codebuildtypes.StatusTypeSucceeded { + return awsBuildGatherLogs(ctx, sdkConfig, build, log) + } else if status == codebuildtypes.StatusTypeFailed || status == codebuildtypes.StatusTypeStopped { + if err := awsBuildGatherLogs(ctx, sdkConfig, build, log); err != nil { + return err + } + logFile, err := os.Open(logPath) + defer logFile.Close() + if err != nil { + return fmt.Errorf("could not open %s: %w", logPath, err) + } + if _, err := io.Copy(os.Stdout, logFile); err != nil { + logrus.WithError(err).Warn("Unable to copy log output from failed aws codebuild.") + } + return fmt.Errorf("build %s failed or was stopped with status: %s", buildId, status) + } + } +} + +func awsBuildGatherLogs(ctx context.Context, sdkConfig aws.Config, build codebuildtypes.Build, log *os.File) error { + buildId := aws.ToString(build.Id) + if build.Logs == nil || build.Logs.GroupName == nil || build.Logs.StreamName == nil { + return fmt.Errorf("cloudwatch log location is missing for build %s", buildId) + } + cloudwatch := cloudwatchlogs.NewFromConfig(sdkConfig) + input := &cloudwatchlogs.GetLogEventsInput{ + LogGroupName: build.Logs.GroupName, + LogStreamName: build.Logs.StreamName, + StartFromHead: &[]bool{true}[0], + } + paginator := cloudwatchlogs.NewGetLogEventsPaginator(cloudwatch, input) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + return fmt.Errorf("could not read logs for build %s: %w", buildId, err) + } + if len(page.Events) == 0 { + break + } + for _, event := range page.Events { + if event.Timestamp == nil || event.Message == nil { + continue + } + timestamp := time.Unix(*event.Timestamp/1000, 0) + _, err := log.Write([]byte(fmt.Sprintf("[%s] %s", timestamp.Format(time.RFC3339), *event.Message))) + if err != nil { + return fmt.Errorf("could not write log for %s on %s: %w", buildId, log.Name(), err) + } + } + } + return nil +} diff --git a/pkg/util/buildspec/buildspec.go b/pkg/util/buildspec/buildspec.go new file mode 100644 index 00000000000..e49aee702e6 --- /dev/null +++ b/pkg/util/buildspec/buildspec.go @@ -0,0 +1,23 @@ +package buildspec + +type BuildSpec struct { + Env Env `json:"env"` + Phases Phases `json:"phases"` + Version string `json:"version"` +} + +type Env struct { + Variables Variables `json:"variables"` +} +type Phases struct { + Build BuildPhase `json:"build"` +} + +type Variables struct { + DockerFile string `json:"dockerfile"` + Credentials string `json:"credentials"` +} +type BuildPhase struct { + OnFailure string `json:"on-failure"` + Commands []string `json:"commands"` +} diff --git a/pkg/util/util.go b/pkg/util/util.go index d9506e7d7fd..3866a983f2e 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -2,6 +2,7 @@ package util import ( "sort" + "strings" "golang.org/x/exp/constraints" ) @@ -94,3 +95,8 @@ func PopCount[T comparable](xs ...T) (ret uint) { } return } + +// ShellEscape escapes a string for safe use in shell single quotes +func ShellEscape(command string) string { + return strings.ReplaceAll(command, "'", "'\\''") +}