diff --git a/pkg/shp/bundle/bundle.go b/pkg/shp/bundle/bundle.go new file mode 100644 index 000000000..bdbd1346b --- /dev/null +++ b/pkg/shp/bundle/bundle.go @@ -0,0 +1,81 @@ +package bundle + +import ( + "context" + "fmt" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + progressbar "github.com/schollz/progressbar/v3" + "github.com/shipwright-io/build/pkg/reconciler/buildrun/resources/sources" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// Push bundles the provided local directory into a container image and pushes +// it to the given registry. +func Push(ctx context.Context, io *genericclioptions.IOStreams, localDirectory string, targetImage string) error { + tag, err := name.NewTag(targetImage) + if err != nil { + return err + } + + auth, err := authn.DefaultKeychain.Resolve(tag.Context()) + if err != nil { + return err + } + + updates := make(chan v1.Update, 1) + done := make(chan struct{}, 1) + go func() { + var progress *progressbar.ProgressBar + for { + select { + case <-ctx.Done(): + return + + case <-done: + return + + case update, ok := <-updates: + if !ok { + return + } + + if progress == nil { + progress = progressbar.NewOptions(int(update.Total), + progressbar.OptionSetWriter(io.ErrOut), + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowBytes(true), + progressbar.OptionSetWidth(15), + progressbar.OptionSetPredictTime(false), + progressbar.OptionSetDescription("Uploading local source..."), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[green]=[reset]", + SaucerHead: "[green]>[reset]", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]"}), + progressbar.OptionClearOnFinish(), + ) + defer progress.Close() + } + + progress.ChangeMax64(update.Total) + _ = progress.Set64(update.Complete) + } + } + }() + + fmt.Fprintf(io.Out, "Bundling %q as %q ...\n", localDirectory, targetImage) + _, err = sources.Bundle( + tag, + localDirectory, + remote.WithAuth(auth), + remote.WithProgress(updates), + ) + + done <- struct{}{} + return err +} diff --git a/pkg/shp/cmd/build/create.go b/pkg/shp/cmd/build/create.go index 3a71c3076..133d7244c 100644 --- a/pkg/shp/cmd/build/create.go +++ b/pkg/shp/cmd/build/create.go @@ -47,6 +47,19 @@ func (c *CreateCommand) Validate() error { if c.name == "" { return fmt.Errorf("name must be provided") } + + if c.buildSpec.Source.URL != "" && + c.buildSpec.Source.Container != nil && + c.buildSpec.Source.Container.Image != "" { + return fmt.Errorf("both source URL and container image are specified, only one can be used at the same time") + } + + if c.buildSpec.Source.URL == "" && + c.buildSpec.Source.Container != nil && + c.buildSpec.Source.Container.Image == "" { + return fmt.Errorf("no input source was specified, either source URL or container image needs to be provided") + } + return nil } @@ -74,9 +87,6 @@ func createCmd() runner.SubCommand { // instantiating command-line flags and the build-spec structure which receives the informed flag // values, also marking certain flags as mandatory buildSpecFlags := flags.BuildSpecFromFlags(cmd.Flags()) - if err := cmd.MarkFlagRequired(flags.SourceURLFlag); err != nil { - panic(err) - } if err := cmd.MarkFlagRequired(flags.OutputImageFlag); err != nil { panic(err) } diff --git a/pkg/shp/cmd/buildrun/create.go b/pkg/shp/cmd/buildrun/create.go index f3a1b9b5c..05970a89b 100644 --- a/pkg/shp/cmd/buildrun/create.go +++ b/pkg/shp/cmd/buildrun/create.go @@ -5,6 +5,7 @@ import ( "strings" buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/cli/pkg/shp/bundle" "github.com/shipwright-io/cli/pkg/shp/cmd/runner" "github.com/shipwright-io/cli/pkg/shp/flags" "github.com/shipwright-io/cli/pkg/shp/params" @@ -18,8 +19,9 @@ import ( type CreateCommand struct { cmd *cobra.Command // cobra command instance - name string // buildrun name - buildRunSpec *buildv1alpha1.BuildRunSpec // stores command-line flags + name string // buildrun name + localSourceDir *string // path to local source code directory + buildRunSpec *buildv1alpha1.BuildRunSpec // stores command-line flags } const buildRunCreateLongDesc = ` @@ -73,10 +75,25 @@ func (c *CreateCommand) Run(params *params.Params, ioStreams *genericclioptions. br := &buildv1alpha1.BuildRun{Spec: *c.buildRunSpec} flags.SanitizeBuildRunSpec(&br.Spec) + var build = buildv1alpha1.Build{} + if err := resource.GetBuildResource(params).Get(c.cmd.Context(), c.buildRunSpec.BuildRef.Name, &build); err != nil { + return fmt.Errorf("failed to get referenced build %q: %w", c.buildRunSpec.BuildRef.Name, err) + } + + // Local source code mode: + // Make sure to bundle the configured local source directory into an image + // bundle and push it to the provided target registry. + if build.Spec.Source.Container.Image != "" { + if err := bundle.Push(c.cmd.Context(), ioStreams, *c.localSourceDir, build.Spec.Source.Container.Image); err != nil { + return err + } + } + buildRunResource := resource.GetBuildRunResource(params) if err := buildRunResource.Create(c.cmd.Context(), c.name, br); err != nil { return err } + fmt.Fprintf(ioStreams.Out, "BuildRun created %q for Build %q\n", c.name, br.Spec.BuildRef.Name) return nil } @@ -98,7 +115,8 @@ func createCmd() runner.SubCommand { } return &CreateCommand{ - cmd: cmd, - buildRunSpec: buildRunSpecFlags, + cmd: cmd, + localSourceDir: flags.BundleLocalSourceFlag(cmd.Flags()), + buildRunSpec: buildRunSpecFlags, } } diff --git a/pkg/shp/flags/build.go b/pkg/shp/flags/build.go index 2e64e6f99..6d686f704 100644 --- a/pkg/shp/flags/build.go +++ b/pkg/shp/flags/build.go @@ -1,6 +1,8 @@ package flags import ( + "os" + buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" @@ -16,6 +18,7 @@ func BuildSpecFromFlags(flags *pflag.FlagSet) *buildv1alpha1.BuildSpec { Credentials: &corev1.LocalObjectReference{}, Revision: pointer.String(""), ContextDir: pointer.String(""), + Container: &buildv1alpha1.Container{}, }, Strategy: &buildv1alpha1.Strategy{ Kind: &clusterBuildStrategyKind, @@ -49,6 +52,11 @@ func SanitizeBuildSpec(b *buildv1alpha1.BuildSpec) { if b.Source.Credentials != nil && b.Source.Credentials.Name == "" { b.Source.Credentials = nil } + + if b.Source.Container != nil && b.Source.Container.Image == "" { + b.Source.Container = nil + } + if b.Builder != nil { if b.Builder.Credentials != nil && b.Builder.Credentials.Name == "" { b.Builder.Credentials = nil @@ -58,3 +66,17 @@ func SanitizeBuildSpec(b *buildv1alpha1.BuildSpec) { } } } + +// BundleLocalSourceFlag returns the source directory setting based on command-line flags. +func BundleLocalSourceFlag(flags *pflag.FlagSet) (result *string) { + cwd, err := os.Getwd() + if err != nil { + cwd = "." + } + + return flags.String( + "source-directory", + cwd, + "directory to be used for local source code", + ) +} diff --git a/pkg/shp/flags/build_test.go b/pkg/shp/flags/build_test.go index d78153cc6..652e27448 100644 --- a/pkg/shp/flags/build_test.go +++ b/pkg/shp/flags/build_test.go @@ -17,14 +17,14 @@ import ( func TestBuildSpecFromFlags(t *testing.T) { g := gomega.NewGomegaWithT(t) - credentials := corev1.LocalObjectReference{Name: "name"} buildStrategyKind := buildv1alpha1.ClusterBuildStrategyKind expected := &buildv1alpha1.BuildSpec{ Source: buildv1alpha1.Source{ - Credentials: &credentials, + Credentials: &corev1.LocalObjectReference{Name: "source-credentials"}, URL: "https://some.url", Revision: pointer.String("some-rev"), ContextDir: pointer.String("some-contextdir"), + Container: &buildv1alpha1.Container{}, }, Strategy: &buildv1alpha1.Strategy{ Name: "strategy-name", @@ -33,11 +33,11 @@ func TestBuildSpecFromFlags(t *testing.T) { }, Dockerfile: pointer.String("some-dockerfile"), Builder: &buildv1alpha1.Image{ - Credentials: &credentials, + Credentials: &corev1.LocalObjectReference{Name: "builder-credentials"}, Image: "builder-image", }, Output: buildv1alpha1.Image{ - Credentials: &credentials, + Credentials: &corev1.LocalObjectReference{Name: "output-credentials"}, Image: "output-image", }, Timeout: &metav1.Duration{ diff --git a/pkg/shp/flags/flags.go b/pkg/shp/flags/flags.go index 84b56718a..5a4dc2a62 100644 --- a/pkg/shp/flags/flags.go +++ b/pkg/shp/flags/flags.go @@ -22,6 +22,8 @@ const ( SourceURLFlag = "source-url" // SourceRevisionFlag command-line flag. SourceRevisionFlag = "source-revision" + // SourceContainerImageFlag command-line flag. + SourceContainerImageFlag = "source-bundle-image" // SourceContextDirFlag command-line flag. SourceContextDirFlag = "source-context-dir" // SourceCredentialsSecretFlag command-line flag. @@ -58,6 +60,12 @@ func sourceFlags(flags *pflag.FlagSet, source *buildv1alpha1.Source) { "", "git repository source revision", ) + flags.StringVar( + &source.Container.Image, + SourceContainerImageFlag, + "", + "source code bundle image", + ) flags.StringVar( source.ContextDir, SourceContextDirFlag,