From 0ccb3477be6ddd17df377e7d4f90cc1aabe6ef9c Mon Sep 17 00:00:00 2001 From: Rashad Sirajudeen Date: Fri, 14 Jun 2024 07:07:43 +0530 Subject: [PATCH 1/4] Refactored ok Signed-off-by: Rashad Sirajudeen --- .gitignore | 3 + cmd/cmd.go | 1 + internal/build/lifecycle_execution.go | 26 +- internal/build/lifecycle_executor.go | 17 + internal/commands/build.go | 47 +- internal/commands/commands.go | 1 + internal/commands/detect.go | 124 +++ pkg/client/build.go | 1147 +++++++++++++------------ pkg/client/detect.go | 24 + 9 files changed, 801 insertions(+), 589 deletions(-) create mode 100644 internal/commands/detect.go create mode 100644 pkg/client/detect.go diff --git a/.gitignore b/.gitignore index 30eb369432..64f9bb0e61 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ benchmarks.test # Jetbrains Goland .idea/ +# Visual Studio Code +.vscode/ + # Build outputs artifacts/ .DS_Store diff --git a/cmd/cmd.go b/cmd/cmd.go index 082d6f16ad..98ebc13422 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -75,6 +75,7 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { commands.AddHelpFlag(rootCmd, "pack") + rootCmd.AddCommand(commands.Detect(logger, cfg, packClient)) rootCmd.AddCommand(commands.Build(logger, cfg, packClient)) rootCmd.AddCommand(commands.NewBuilderCommand(logger, cfg, packClient)) rootCmd.AddCommand(commands.NewBuildpackCommand(logger, cfg, packClient, buildpackage.NewConfigReader())) diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index 2f017dd70f..f18a3b7ffd 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -567,7 +567,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach platformAPILessThan07 := l.platformAPI.LessThan("0.7") cacheBindOp := NullOp() - if l.opts.ClearCache { + if l.opts.ClearCache || buildCache == nil { if platformAPILessThan07 || l.platformAPI.AtLeast("0.9") { args = prependArg("-skip-layers", args) } @@ -584,7 +584,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach } launchCacheBindOp := NullOp() - if l.platformAPI.AtLeast("0.9") { + if l.platformAPI.AtLeast("0.9") && launchCache != nil { if !l.opts.Publish { args = append([]string{"-launch-cache", l.mountPaths.launchCacheDir()}, args...) launchCacheBindOp = WithBinds(fmt.Sprintf("%s:%s", launchCache.Name(), l.mountPaths.launchCacheDir())) @@ -1000,3 +1000,25 @@ func addTags(flags, additionalTags []string) []string { } return flags } + +func (l *LifecycleExecution) RunDetect(ctx context.Context, phaseFactoryCreator PhaseFactoryCreator) error { + phaseFactory := phaseFactoryCreator(l) + + var dummyCache Cache + + if l.platformAPI.LessThan("0.7") { + return errors.New("Detect needs at least platform API 0.7") + } + + l.logger.Info(style.Step("ANALYZING")) + if err := l.Analyze(ctx, dummyCache, dummyCache, phaseFactory); err != nil { + return err + } + + l.logger.Info(style.Step("DETECTING")) + if err := l.Detect(ctx, phaseFactory); err != nil { + return err + } + + return nil +} diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index 6394f89472..681be27926 100644 --- a/internal/build/lifecycle_executor.go +++ b/internal/build/lifecycle_executor.go @@ -16,6 +16,7 @@ import ( "github.com/buildpacks/pack/internal/container" "github.com/buildpacks/pack/pkg/cache" "github.com/buildpacks/pack/pkg/dist" + "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" ) @@ -103,6 +104,7 @@ type LifecycleOptions struct { SBOMDestinationDir string CreationTime *time.Time Keychain authn.Keychain + FetchOptions image.FetchOptions } func NewLifecycleExecutor(logger logging.Logger, docker DockerClient) *LifecycleExecutor { @@ -130,3 +132,18 @@ func (l *LifecycleExecutor) Execute(ctx context.Context, opts LifecycleOptions) lifecycleExec.Run(ctx, NewDefaultPhaseFactory) }) } + +func (l *LifecycleExecutor) Detect(ctx context.Context, opts LifecycleOptions) error { + tmpDir, err := os.MkdirTemp("", "pack.tmp") + if err != nil { + return err + } + + lifecycleExec, err := NewLifecycleExecution(l.logger, l.docker, tmpDir, opts) + if err != nil { + return err + } + + defer lifecycleExec.Cleanup() + return lifecycleExec.RunDetect(ctx, NewDefaultPhaseFactory) +} diff --git a/internal/commands/build.go b/internal/commands/build.go index 50ccebeabd..2297cdbaee 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -56,6 +56,7 @@ type BuildFlags struct { DateTime string PreBuildpacks []string PostBuildpacks []string + DetectOnly bool } // Build an image from source code @@ -236,18 +237,7 @@ func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Co cmd.Flags().StringSliceVarP(&buildFlags.Buildpacks, "buildpack", "b", nil, "Buildpack to use. One of:\n a buildpack by id and version in the form of '@',\n path to a buildpack directory (not supported on Windows),\n path/URL to a buildpack .tar or .tgz file, or\n a packaged buildpack image name in the form of '/[:]'"+stringSliceHelp("buildpack")) cmd.Flags().StringSliceVarP(&buildFlags.Extensions, "extension", "", nil, "Extension to use. One of:\n an extension by id and version in the form of '@',\n path to an extension directory (not supported on Windows),\n path/URL to an extension .tar or .tgz file, or\n a packaged extension image name in the form of '/[:]'"+stringSliceHelp("extension")) cmd.Flags().StringVarP(&buildFlags.Builder, "builder", "B", cfg.DefaultBuilder, "Builder image") - cmd.Flags().Var(&buildFlags.Cache, "cache", - `Cache options used to define cache techniques for build process. -- Cache as bind: 'type=;format=bind;source=' -- Cache as image (requires --publish): 'type=;format=image;name=' -- Cache as volume: 'type=;format=volume;[name=]' - - If no name is provided, a random name will be generated. -`) - cmd.Flags().StringVar(&buildFlags.CacheImage, "cache-image", "", `Cache build layers in remote registry. Requires --publish`) - cmd.Flags().BoolVar(&buildFlags.ClearCache, "clear-cache", false, "Clear image's associated cache before building") - cmd.Flags().StringVar(&buildFlags.DateTime, "creation-time", "", "Desired create time in the output image config. Accepted values are Unix timestamps (e.g., '1641013200'), or 'now'. Platform API version must be at least 0.9 to use this feature.") cmd.Flags().StringVarP(&buildFlags.DescriptorPath, "descriptor", "d", "", "Path to the project descriptor file") - cmd.Flags().StringVarP(&buildFlags.DefaultProcessType, "default-process", "D", "", `Set the default process type. (default "web")`) cmd.Flags().StringArrayVarP(&buildFlags.Env, "env", "e", []string{}, "Build-time environment variable, in the form 'VAR=VALUE' or 'VAR'.\nWhen using latter value-less form, value will be taken from current\n environment at the time this command is executed.\nThis flag may be specified multiple times and will override\n individual values defined by --env-file."+stringArrayHelp("env")+"\nNOTE: These are NOT available at image runtime.") cmd.Flags().StringArrayVar(&buildFlags.EnvFiles, "env-file", []string{}, "Build-time environment variables file\nOne variable per line, of the form 'VAR=VALUE' or 'VAR'\nWhen using latter value-less form, value will be taken from current\n environment at the time this command is executed\nNOTE: These are NOT available at image runtime.\"") cmd.Flags().StringVar(&buildFlags.Network, "network", "", "Connect detect and build containers to network") @@ -265,21 +255,36 @@ This option may set DOCKER_HOST environment variable for the build container if cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`) cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name") cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)") - cmd.Flags().StringSliceVarP(&buildFlags.AdditionalTags, "tag", "t", nil, "Additional tags to push the output image to.\nTags should be in the format 'image:tag' or 'repository/image:tag'."+stringSliceHelp("tag")) - cmd.Flags().BoolVar(&buildFlags.TrustBuilder, "trust-builder", false, "Trust the provided builder.\nAll lifecycle phases will be run in a single container.\nFor more on trusted builders, and when to trust or untrust a builder, check out our docs here: https://buildpacks.io/docs/tools/pack/concepts/trusted_builders") cmd.Flags().StringArrayVar(&buildFlags.Volumes, "volume", nil, "Mount host volume into the build container, in the form ':[:]'.\n- 'host path': Name of the volume or absolute directory path to mount.\n- 'target path': The path where the file or directory is available in the container.\n- 'options' (default \"ro\"): An optional comma separated list of mount options.\n - \"ro\", volume contents are read-only.\n - \"rw\", volume contents are readable and writeable.\n - \"volume-opt==\", can be specified more than once, takes a key-value pair consisting of the option name and its value."+stringArrayHelp("volume")) cmd.Flags().StringVar(&buildFlags.Workspace, "workspace", "", "Location at which to mount the app dir in the build image") cmd.Flags().IntVar(&buildFlags.GID, "gid", 0, `Override GID of user's group in the stack's build and run images. The provided value must be a positive number`) cmd.Flags().IntVar(&buildFlags.UID, "uid", 0, `Override UID of user in the stack's build and run images. The provided value must be a positive number`) - cmd.Flags().StringVar(&buildFlags.PreviousImage, "previous-image", "", "Set previous image to a particular tag reference, digest reference, or (when performing a daemon build) image ID") - cmd.Flags().StringVar(&buildFlags.SBOMDestinationDir, "sbom-output-dir", "", "Path to export SBoM contents.\nOmitting the flag will yield no SBoM content.") - cmd.Flags().StringVar(&buildFlags.ReportDestinationDir, "report-output-dir", "", "Path to export build report.toml.\nOmitting the flag yield no report file.") - cmd.Flags().BoolVar(&buildFlags.Interactive, "interactive", false, "Launch a terminal UI to depict the build process") - cmd.Flags().BoolVar(&buildFlags.Sparse, "sparse", false, "Use this flag to avoid saving on disk the run-image layers when the application image is exported to OCI layout format") - if !cfg.Experimental { - cmd.Flags().MarkHidden("interactive") - cmd.Flags().MarkHidden("sparse") + + if !buildFlags.DetectOnly { + cmd.Flags().Var(&buildFlags.Cache, "cache", + `Cache options used to define cache techniques for build process. +- Cache as bind: 'type=;format=bind;source=' +- Cache as image (requires --publish): 'type=;format=image;name=' +- Cache as volume: 'type=;format=volume;[name=]' + - If no name is provided, a random name will be generated. +`) + cmd.Flags().StringVar(&buildFlags.CacheImage, "cache-image", "", `Cache build layers in remote registry. Requires --publish`) + cmd.Flags().BoolVar(&buildFlags.ClearCache, "clear-cache", false, "Clear image's associated cache before building") + cmd.Flags().StringVar(&buildFlags.DateTime, "creation-time", "", "Desired create time in the output image config. Accepted values are Unix timestamps (e.g., '1641013200'), or 'now'. Platform API version must be at least 0.9 to use this feature.") + cmd.Flags().StringVarP(&buildFlags.DefaultProcessType, "default-process", "D", "", `Set the default process type. (default "web")`) + cmd.Flags().StringSliceVarP(&buildFlags.AdditionalTags, "tag", "t", nil, "Additional tags to push the output image to.\nTags should be in the format 'image:tag' or 'repository/image:tag'."+stringSliceHelp("tag")) + cmd.Flags().BoolVar(&buildFlags.TrustBuilder, "trust-builder", false, "Trust the provided builder.\nAll lifecycle phases will be run in a single container.\nFor more on trusted builders, and when to trust or untrust a builder, check out our docs here: https://buildpacks.io/docs/tools/pack/concepts/trusted_builders") + cmd.Flags().StringVar(&buildFlags.PreviousImage, "previous-image", "", "Set previous image to a particular tag reference, digest reference, or (when performing a daemon build) image ID") + cmd.Flags().StringVar(&buildFlags.SBOMDestinationDir, "sbom-output-dir", "", "Path to export SBoM contents.\nOmitting the flag will yield no SBoM content.") + cmd.Flags().StringVar(&buildFlags.ReportDestinationDir, "report-output-dir", "", "Path to export build report.toml.\nOmitting the flag yield no report file.") + cmd.Flags().BoolVar(&buildFlags.Interactive, "interactive", false, "Launch a terminal UI to depict the build process") + cmd.Flags().BoolVar(&buildFlags.Sparse, "sparse", false, "Use this flag to avoid saving on disk the run-image layers when the application image is exported to OCI layout format") + if !cfg.Experimental { + cmd.Flags().MarkHidden("interactive") + cmd.Flags().MarkHidden("sparse") + } } + } func validateBuildFlags(flags *BuildFlags, cfg config.Config, inputImageRef client.InputImageReference, logger logging.Logger) error { diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 1308b35ec0..29b0cb9572 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -29,6 +29,7 @@ type PackClient interface { NewBuildpack(context.Context, client.NewBuildpackOptions) error PackageBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error PackageExtension(ctx context.Context, opts client.PackageBuildpackOptions) error + Detect(context.Context, client.BuildOptions) error Build(context.Context, client.BuildOptions) error RegisterBuildpack(context.Context, client.RegisterBuildpackOptions) error YankBuildpack(client.YankBuildpackOptions) error diff --git a/internal/commands/detect.go b/internal/commands/detect.go new file mode 100644 index 0000000000..b25674aff2 --- /dev/null +++ b/internal/commands/detect.go @@ -0,0 +1,124 @@ +package commands + +import ( + "path/filepath" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/config" + "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/image" + "github.com/buildpacks/pack/pkg/logging" +) + +// Run up to the detect phase of the CNB lifecycle against a source code directory +func Detect(logger logging.Logger, cfg config.Config, packClient PackClient) *cobra.Command { + var flags BuildFlags + flags.DetectOnly = true + + cmd := &cobra.Command{ + Use: "detect", + Args: cobra.ExactArgs(1), + Short: "Pack Detect runs the analyze and detect phases of the Cloud Native Buildpacks lifecycle to determine a group of applicable buildpacks and a build plan.", + Example: "pack detect --path apps/test-app --builder cnbs/sample-builder:bionic", + Long: "Pack Detect uses Cloud Native Buildpacks to run the detect phase of buildpack groups against the source code.\n", + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + inputImageName := client.ParseInputImageReference(args[0]) + if err := validateBuildFlags(&flags, cfg, inputImageName, logger); err != nil { + return err + } + + descriptor, actualDescriptorPath, err := parseProjectToml(flags.AppPath, flags.DescriptorPath, logger) + if err != nil { + return err + } + + if actualDescriptorPath != "" { + logger.Debugf("Using project descriptor located at %s", style.Symbol(actualDescriptorPath)) + } + + builder := flags.Builder + + if !cmd.Flags().Changed("builder") && descriptor.Build.Builder != "" { + builder = descriptor.Build.Builder + } + + if builder == "" { + suggestSettingBuilder(logger, packClient) + return client.NewSoftError() + } + + buildpacks := flags.Buildpacks + extensions := flags.Extensions + + env, err := parseEnv(flags.EnvFiles, flags.Env) + if err != nil { + return err + } + + stringPolicy := flags.Policy + if stringPolicy == "" { + stringPolicy = cfg.PullPolicy + } + pullPolicy, err := image.ParsePullPolicy(stringPolicy) + if err != nil { + return errors.Wrapf(err, "parsing pull policy %s", flags.Policy) + } + var lifecycleImage string + if flags.LifecycleImage != "" { + ref, err := name.ParseReference(flags.LifecycleImage) + if err != nil { + return errors.Wrapf(err, "parsing lifecycle image %s", flags.LifecycleImage) + } + lifecycleImage = ref.Name() + } + + var gid = -1 + if cmd.Flags().Changed("gid") { + gid = flags.GID + } + + var uid = -1 + if cmd.Flags().Changed("uid") { + uid = flags.UID + } + + if err := packClient.Detect(cmd.Context(), client.BuildOptions{ + AppPath: flags.AppPath, + Builder: builder, + Registry: flags.Registry, + Env: env, + Image: inputImageName.Name(), + RunImage: flags.RunImage, + Publish: flags.Publish, + DockerHost: flags.DockerHost, + PullPolicy: pullPolicy, + ProjectDescriptorBaseDir: filepath.Dir(actualDescriptorPath), + ProjectDescriptor: descriptor, + + ContainerConfig: client.ContainerConfig{ + Network: flags.Network, + Volumes: flags.Volumes, + }, + LifecycleImage: lifecycleImage, + PreBuildpacks: flags.PreBuildpacks, + PostBuildpacks: flags.PostBuildpacks, + Buildpacks: buildpacks, + Extensions: extensions, + Workspace: flags.Workspace, + GroupID: gid, + UserID: uid, + DetectOnly: true, + }); err != nil { + return errors.Wrap(err, "failed to detect") + } + return nil + }), + } + buildCommandFlags(cmd, &flags, cfg) + AddHelpFlag(cmd, "detect") + return cmd +} diff --git a/pkg/client/build.go b/pkg/client/build.go index 4825f09f3c..80a0541fd5 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -22,7 +22,6 @@ import ( "github.com/buildpacks/imgutil/local" "github.com/buildpacks/imgutil/remote" "github.com/buildpacks/lifecycle/platform/files" - types "github.com/docker/docker/api/types/image" "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" ignore "github.com/sabhiram/go-gitignore" @@ -72,6 +71,9 @@ type LifecycleExecutor interface { // Execute is responsible for invoking each of these binaries // with the desired configuration. Execute(ctx context.Context, opts build.LifecycleOptions) error + + // Detect runs only up the detect binary + Detect(ctx context.Context, opts build.LifecycleOptions) error } type IsTrustedBuilder func(string) bool @@ -212,6 +214,9 @@ type BuildOptions struct { // Configuration to export to OCI layout format LayoutConfig *LayoutConfig + + // States if the whole lifecycle or only the detect binary should be run + DetectOnly bool } func (b *BuildOptions) Layout() bool { @@ -291,502 +296,192 @@ var IsTrustedBuilderFunc = func(b string) bool { // If any configuration is deemed invalid, or if any lifecycle phases fail, // an error will be returned and no image produced. func (c *Client) Build(ctx context.Context, opts BuildOptions) error { - var pathsConfig layoutPathConfig - - imageRef, err := c.parseReference(opts) + lifecycleOpts, err := c.ResolveLifecycleOptions(ctx, opts) if err != nil { - return errors.Wrapf(err, "invalid image name '%s'", opts.Image) + return err } - imgRegistry := imageRef.Context().RegistryStr() - imageName := imageRef.Name() - if opts.Layout() { - pathsConfig, err = c.processLayoutPath(opts.LayoutConfig.InputImage, opts.LayoutConfig.PreviousInputImage) + // cleanup the extended lifecycle image when done + // defer c.docker.ImageRemove(context.Background(), lifecycleImage.Name(), types.RemoveOptions{Force: true}) + // defer c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), types.RemoveOptions{Force: true}) + + lifecycleOpts.FetchRunImageWithLifecycleLayer = func(runImageName string) (string, error) { + ephemeralRunImageName := fmt.Sprintf("pack.local/run-image/%x:latest", randString(10)) + runImage, err := c.imageFetcher.Fetch(ctx, runImageName, lifecycleOpts.FetchOptions) if err != nil { - if opts.LayoutConfig.PreviousInputImage != nil { - return errors.Wrapf(err, "invalid layout paths image name '%s' or previous-image name '%s'", opts.LayoutConfig.InputImage.Name(), - opts.LayoutConfig.PreviousInputImage.Name()) + return "", err + } + ephemeralRunImage, err := local.NewImage(ephemeralRunImageName, c.docker, local.FromBaseImage(runImage.Name())) + if err != nil { + return "", err + } + tmpDir, err := os.MkdirTemp("", "extend-run-image-scratch") // we need to write to disk because manifest.json is last in the tar + if err != nil { + return "", err + } + defer os.RemoveAll(tmpDir) + lifecycleImageTar, err := func() (string, error) { + lifecycleImageTar := filepath.Join(tmpDir, "lifecycle-image.tar") + lifecycleImageReader, err := c.docker.ImageSave(context.Background(), []string{lifecycleOpts.LifecycleImage}) // this is fast because the lifecycle image is based on distroless static + if err != nil { + return "", err + } + defer lifecycleImageReader.Close() + lifecycleImageWriter, err := os.Create(lifecycleImageTar) + if err != nil { + return "", err + } + defer lifecycleImageWriter.Close() + if _, err = io.Copy(lifecycleImageWriter, lifecycleImageReader); err != nil { + return "", err } - return errors.Wrapf(err, "invalid layout paths image name '%s'", opts.LayoutConfig.InputImage.Name()) + return lifecycleImageTar, nil + }() + if err != nil { + return "", err } - } - - appPath, err := c.processAppPath(opts.AppPath) - if err != nil { - return errors.Wrapf(err, "invalid app path '%s'", opts.AppPath) - } - - proxyConfig := c.processProxyConfig(opts.ProxyConfig) + advanceTarToEntryWithName := func(tarReader *tar.Reader, wantName string) (*tar.Header, error) { + var ( + header *tar.Header + err error + ) + for { + header, err = tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if header.Name != wantName { + continue + } + return header, nil + } + return nil, fmt.Errorf("failed to find header with name: %s", wantName) + } + lifecycleLayerName, err := func() (string, error) { + lifecycleImageReader, err := os.Open(lifecycleImageTar) + if err != nil { + return "", err + } + defer lifecycleImageReader.Close() + tarReader := tar.NewReader(lifecycleImageReader) + if _, err = advanceTarToEntryWithName(tarReader, "manifest.json"); err != nil { + return "", err + } + type descriptor struct { + Layers []string + } + type manifestJSON []descriptor + var manifestContents manifestJSON + if err = json.NewDecoder(tarReader).Decode(&manifestContents); err != nil { + return "", err + } + if len(manifestContents) < 1 { + return "", errors.New("missing manifest entries") + } + // we can assume the lifecycle layer is the last in the tar, except if the lifecycle has been extended as an ephemeral lifecycle + layerOffset := 1 + if strings.Contains(lifecycleOpts.LifecycleImage, "pack.local/lifecycle") { + layerOffset = 2 + } - builderRef, err := c.processBuilderName(opts.Builder) - if err != nil { - return errors.Wrapf(err, "invalid builder '%s'", opts.Builder) - } + if (len(manifestContents[0].Layers) - layerOffset) < 0 { + return "", errors.New("Lifecycle image did not contain expected layer count") + } - requestedTarget := func() *dist.Target { - if opts.Platform == "" { - return nil + return manifestContents[0].Layers[len(manifestContents[0].Layers)-layerOffset], nil + }() + if err != nil { + return "", err } - parts := strings.Split(opts.Platform, "/") - switch len(parts) { - case 0: - return nil - case 1: - return &dist.Target{OS: parts[0]} - case 2: - return &dist.Target{OS: parts[0], Arch: parts[1]} - default: - return &dist.Target{OS: parts[0], Arch: parts[1], ArchVariant: parts[2]} + if lifecycleLayerName == "" { + return "", errors.New("failed to find lifecycle layer") } - }() - - rawBuilderImage, err := c.imageFetcher.Fetch( - ctx, - builderRef.Name(), - image.FetchOptions{ - Daemon: true, - Target: requestedTarget, - PullPolicy: opts.PullPolicy}, - ) - if err != nil { - return errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name()) - } - - var targetToUse *dist.Target - if requestedTarget != nil { - targetToUse = requestedTarget - } else { - targetToUse, err = getTargetFromBuilder(rawBuilderImage) + lifecycleLayerTar, err := func() (string, error) { + lifecycleImageReader, err := os.Open(lifecycleImageTar) + if err != nil { + return "", err + } + defer lifecycleImageReader.Close() + tarReader := tar.NewReader(lifecycleImageReader) + var header *tar.Header + if header, err = advanceTarToEntryWithName(tarReader, lifecycleLayerName); err != nil { + return "", err + } + lifecycleLayerTar := filepath.Join(filepath.Dir(lifecycleImageTar), filepath.Dir(lifecycleLayerName)+".tar") // this will be either /layer.tar (docker < 25.x) OR blobs/sha256.tar (docker 25.x and later OR containerd storage enabled) + if err = os.MkdirAll(filepath.Dir(lifecycleLayerTar), 0755); err != nil { + return "", err + } + lifecycleLayerWriter, err := os.OpenFile(lifecycleLayerTar, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return "", err + } + defer lifecycleLayerWriter.Close() + if _, err = io.Copy(lifecycleLayerWriter, tarReader); err != nil { + return "", err + } + return lifecycleLayerTar, nil + }() if err != nil { - return err + return "", err } - } - - bldr, err := c.getBuilder(rawBuilderImage) - if err != nil { - return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder)) - } - - fetchOptions := image.FetchOptions{ - Daemon: !opts.Publish, - PullPolicy: opts.PullPolicy, - Target: targetToUse, - } - runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, fetchOptions) - - if opts.Layout() { - targetRunImagePath, err := layout.ParseRefToPath(runImageName) + diffID, err := func() (string, error) { + lifecycleLayerReader, err := os.Open(lifecycleLayerTar) + if err != nil { + return "", err + } + defer lifecycleLayerReader.Close() + hasher := sha256.New() + if _, err = io.Copy(hasher, lifecycleLayerReader); err != nil { + return "", err + } + // it's weird that this doesn't match lifecycleLayerTar + return hex.EncodeToString(hasher.Sum(nil)), nil + }() if err != nil { - return err + return "", err } - hostRunImagePath := filepath.Join(opts.LayoutConfig.LayoutRepoDir, targetRunImagePath) - targetRunImagePath = filepath.Join(paths.RootDir, "layout-repo", targetRunImagePath) - fetchOptions.LayoutOption = image.LayoutOption{ - Path: hostRunImagePath, - Sparse: opts.LayoutConfig.Sparse, + if err = ephemeralRunImage.AddLayerWithDiffID(lifecycleLayerTar, "sha256:"+diffID); err != nil { + return "", err } - fetchOptions.Daemon = false - pathsConfig.targetRunImagePath = targetRunImagePath - pathsConfig.hostRunImagePath = hostRunImagePath - } - runImage, err := c.validateRunImage(ctx, runImageName, fetchOptions, bldr.StackID) - if err != nil { - return errors.Wrapf(err, "invalid run-image '%s'", runImageName) + if err = ephemeralRunImage.Save(); err != nil { + return "", err + } + return ephemeralRunImageName, nil } - var runMixins []string - if _, err := dist.GetLabel(runImage, stack.MixinsLabel, &runMixins); err != nil { - return err + if err = c.lifecycleExecutor.Execute(ctx, *lifecycleOpts); err != nil { + return fmt.Errorf("executing lifecycle: %w", err) } + return c.logImageNameAndSha(ctx, opts.Publish, lifecycleOpts.Image) +} - fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse) +func getTargetFromBuilder(builderImage imgutil.Image) (*dist.Target, error) { + builderOS, err := builderImage.OS() if err != nil { - return err + return nil, fmt.Errorf("failed to get builder OS: %w", err) } - - fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Extensions(), opts, targetToUse) + builderArch, err := builderImage.Architecture() if err != nil { - return err + return nil, fmt.Errorf("failed to get builder architecture: %w", err) } - - // Default mode: if the TrustBuilder option is not set, trust the suggested builders. - if opts.TrustBuilder == nil { - opts.TrustBuilder = IsTrustedBuilderFunc + builderArchVariant, err := builderImage.Variant() + if err != nil { + return nil, fmt.Errorf("failed to get builder architecture variant: %w", err) } + return &dist.Target{ + OS: builderOS, + Arch: builderArch, + ArchVariant: builderArchVariant, + }, nil +} - // Ensure the builder's platform APIs are supported - var builderPlatformAPIs builder.APISet - builderPlatformAPIs = append(builderPlatformAPIs, bldr.LifecycleDescriptor().APIs.Platform.Deprecated...) - builderPlatformAPIs = append(builderPlatformAPIs, bldr.LifecycleDescriptor().APIs.Platform.Supported...) - if !supportsPlatformAPI(builderPlatformAPIs) { - c.logger.Debugf("pack %s supports Platform API(s): %s", c.version, strings.Join(build.SupportedPlatformAPIVersions.AsStrings(), ", ")) - c.logger.Debugf("Builder %s supports Platform API(s): %s", style.Symbol(opts.Builder), strings.Join(builderPlatformAPIs.AsStrings(), ", ")) - return errors.Errorf("Builder %s is incompatible with this version of pack", style.Symbol(opts.Builder)) - } - - // Get the platform API version to use - lifecycleVersion := bldr.LifecycleDescriptor().Info.Version - useCreator := supportsCreator(lifecycleVersion) && opts.TrustBuilder(opts.Builder) - var ( - lifecycleOptsLifecycleImage string - lifecycleAPIs []string - ) - if !(useCreator) { - // fetch the lifecycle image - if supportsLifecycleImage(lifecycleVersion) { - lifecycleImageName := opts.LifecycleImage - if lifecycleImageName == "" { - lifecycleImageName = fmt.Sprintf("%s:%s", internalConfig.DefaultLifecycleImageRepo, lifecycleVersion.String()) - } - - lifecycleImage, err := c.imageFetcher.Fetch( - ctx, - lifecycleImageName, - image.FetchOptions{ - Daemon: true, - PullPolicy: opts.PullPolicy, - Target: targetToUse, - }, - ) - if err != nil { - return fmt.Errorf("fetching lifecycle image: %w", err) - } - - // if lifecyle container os isn't windows, use ephemeral lifecycle to add /workspace with correct ownership - imageOS, err := lifecycleImage.OS() - if err != nil { - return errors.Wrap(err, "getting lifecycle image OS") - } - if imageOS != "windows" { - // obtain uid/gid from builder to use when extending lifecycle image - uid, gid, err := userAndGroupIDs(rawBuilderImage) - if err != nil { - return fmt.Errorf("obtaining build uid/gid from builder image: %w", err) - } - - c.logger.Debugf("Creating ephemeral lifecycle from %s with uid %d and gid %d. With workspace dir %s", lifecycleImage.Name(), uid, gid, opts.Workspace) - // extend lifecycle image with mountpoints, and use it instead of current lifecycle image - lifecycleImage, err = c.createEphemeralLifecycle(lifecycleImage, opts.Workspace, uid, gid) - if err != nil { - return err - } - c.logger.Debugf("Selecting ephemeral lifecycle image %s for build", lifecycleImage.Name()) - // cleanup the extended lifecycle image when done - defer c.docker.ImageRemove(context.Background(), lifecycleImage.Name(), types.RemoveOptions{Force: true}) - } - - lifecycleOptsLifecycleImage = lifecycleImage.Name() - labels, err := lifecycleImage.Labels() - if err != nil { - return fmt.Errorf("reading labels of lifecycle image: %w", err) - } - - lifecycleAPIs, err = extractSupportedLifecycleApis(labels) - if err != nil { - return fmt.Errorf("reading api versions of lifecycle image: %w", err) - } - } - } - - usingPlatformAPI, err := build.FindLatestSupported(append( - bldr.LifecycleDescriptor().APIs.Platform.Deprecated, - bldr.LifecycleDescriptor().APIs.Platform.Supported...), - lifecycleAPIs) - if err != nil { - return fmt.Errorf("finding latest supported Platform API: %w", err) - } - if usingPlatformAPI.LessThan("0.12") { - if err = c.validateMixins(fetchedBPs, bldr, runImageName, runMixins); err != nil { - return fmt.Errorf("validating stack mixins: %w", err) - } - } - - buildEnvs := map[string]string{} - for _, envVar := range opts.ProjectDescriptor.Build.Env { - buildEnvs[envVar.Name] = envVar.Value - } - - for k, v := range opts.Env { - buildEnvs[k] = v - } - - ephemeralBuilder, err := c.createEphemeralBuilder(rawBuilderImage, buildEnvs, order, fetchedBPs, orderExtensions, fetchedExs, usingPlatformAPI.LessThan("0.12"), opts.RunImage) - if err != nil { - return err - } - defer c.docker.ImageRemove(context.Background(), ephemeralBuilder.Name(), types.RemoveOptions{Force: true}) - - if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 { - if targetToUse.OS == "windows" { - return fmt.Errorf("builder contains image extensions which are not supported for Windows builds") - } - if !(opts.PullPolicy == image.PullAlways) { - return fmt.Errorf("pull policy must be 'always' when builder contains image extensions") - } - } - - if opts.Layout() { - opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig) - } - - processedVolumes, warnings, err := processVolumes(targetToUse.OS, opts.ContainerConfig.Volumes) - if err != nil { - return err - } - - for _, warning := range warnings { - c.logger.Warn(warning) - } - - fileFilter, err := getFileFilter(opts.ProjectDescriptor) - if err != nil { - return err - } - - runImageName, err = pname.TranslateRegistry(runImageName, c.registryMirrors, c.logger) - if err != nil { - return err - } - - projectMetadata := files.ProjectMetadata{} - if c.experimental { - version := opts.ProjectDescriptor.Project.Version - sourceURL := opts.ProjectDescriptor.Project.SourceURL - if version != "" || sourceURL != "" { - projectMetadata.Source = &files.ProjectSource{ - Type: "project", - Version: map[string]interface{}{"declared": version}, - Metadata: map[string]interface{}{"url": sourceURL}, - } - } else { - projectMetadata.Source = v02.GitMetadata(opts.AppPath) - } - } - - lifecycleOpts := build.LifecycleOptions{ - AppPath: appPath, - Image: imageRef, - Builder: ephemeralBuilder, - BuilderImage: builderRef.Name(), - LifecycleImage: ephemeralBuilder.Name(), - RunImage: runImageName, - ProjectMetadata: projectMetadata, - ClearCache: opts.ClearCache, - Publish: opts.Publish, - TrustBuilder: opts.TrustBuilder(opts.Builder), - UseCreator: useCreator, - UseCreatorWithExtensions: supportsCreatorWithExtensions(lifecycleVersion), - DockerHost: opts.DockerHost, - Cache: opts.Cache, - CacheImage: opts.CacheImage, - HTTPProxy: proxyConfig.HTTPProxy, - HTTPSProxy: proxyConfig.HTTPSProxy, - NoProxy: proxyConfig.NoProxy, - Network: opts.ContainerConfig.Network, - AdditionalTags: opts.AdditionalTags, - Volumes: processedVolumes, - DefaultProcessType: opts.DefaultProcessType, - FileFilter: fileFilter, - Workspace: opts.Workspace, - GID: opts.GroupID, - UID: opts.UserID, - PreviousImage: opts.PreviousImage, - Interactive: opts.Interactive, - Termui: termui.NewTermui(imageName, ephemeralBuilder, runImageName), - ReportDestinationDir: opts.ReportDestinationDir, - SBOMDestinationDir: opts.SBOMDestinationDir, - CreationTime: opts.CreationTime, - Layout: opts.Layout(), - Keychain: c.keychain, - } - - switch { - case useCreator: - lifecycleOpts.UseCreator = true - case supportsLifecycleImage(lifecycleVersion): - lifecycleOpts.LifecycleImage = lifecycleOptsLifecycleImage - lifecycleOpts.LifecycleApis = lifecycleAPIs - case !opts.TrustBuilder(opts.Builder): - return errors.Errorf("Lifecycle %s does not have an associated lifecycle image. Builder must be trusted.", lifecycleVersion.String()) - } - - lifecycleOpts.FetchRunImageWithLifecycleLayer = func(runImageName string) (string, error) { - ephemeralRunImageName := fmt.Sprintf("pack.local/run-image/%x:latest", randString(10)) - runImage, err := c.imageFetcher.Fetch(ctx, runImageName, fetchOptions) - if err != nil { - return "", err - } - ephemeralRunImage, err := local.NewImage(ephemeralRunImageName, c.docker, local.FromBaseImage(runImage.Name())) - if err != nil { - return "", err - } - tmpDir, err := os.MkdirTemp("", "extend-run-image-scratch") // we need to write to disk because manifest.json is last in the tar - if err != nil { - return "", err - } - defer os.RemoveAll(tmpDir) - lifecycleImageTar, err := func() (string, error) { - lifecycleImageTar := filepath.Join(tmpDir, "lifecycle-image.tar") - lifecycleImageReader, err := c.docker.ImageSave(context.Background(), []string{lifecycleOpts.LifecycleImage}) // this is fast because the lifecycle image is based on distroless static - if err != nil { - return "", err - } - defer lifecycleImageReader.Close() - lifecycleImageWriter, err := os.Create(lifecycleImageTar) - if err != nil { - return "", err - } - defer lifecycleImageWriter.Close() - if _, err = io.Copy(lifecycleImageWriter, lifecycleImageReader); err != nil { - return "", err - } - return lifecycleImageTar, nil - }() - if err != nil { - return "", err - } - advanceTarToEntryWithName := func(tarReader *tar.Reader, wantName string) (*tar.Header, error) { - var ( - header *tar.Header - err error - ) - for { - header, err = tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - if header.Name != wantName { - continue - } - return header, nil - } - return nil, fmt.Errorf("failed to find header with name: %s", wantName) - } - lifecycleLayerName, err := func() (string, error) { - lifecycleImageReader, err := os.Open(lifecycleImageTar) - if err != nil { - return "", err - } - defer lifecycleImageReader.Close() - tarReader := tar.NewReader(lifecycleImageReader) - if _, err = advanceTarToEntryWithName(tarReader, "manifest.json"); err != nil { - return "", err - } - type descriptor struct { - Layers []string - } - type manifestJSON []descriptor - var manifestContents manifestJSON - if err = json.NewDecoder(tarReader).Decode(&manifestContents); err != nil { - return "", err - } - if len(manifestContents) < 1 { - return "", errors.New("missing manifest entries") - } - // we can assume the lifecycle layer is the last in the tar, except if the lifecycle has been extended as an ephemeral lifecycle - layerOffset := 1 - if strings.Contains(lifecycleOpts.LifecycleImage, "pack.local/lifecycle") { - layerOffset = 2 - } - - if (len(manifestContents[0].Layers) - layerOffset) < 0 { - return "", errors.New("Lifecycle image did not contain expected layer count") - } - - return manifestContents[0].Layers[len(manifestContents[0].Layers)-layerOffset], nil - }() - if err != nil { - return "", err - } - if lifecycleLayerName == "" { - return "", errors.New("failed to find lifecycle layer") - } - lifecycleLayerTar, err := func() (string, error) { - lifecycleImageReader, err := os.Open(lifecycleImageTar) - if err != nil { - return "", err - } - defer lifecycleImageReader.Close() - tarReader := tar.NewReader(lifecycleImageReader) - var header *tar.Header - if header, err = advanceTarToEntryWithName(tarReader, lifecycleLayerName); err != nil { - return "", err - } - lifecycleLayerTar := filepath.Join(filepath.Dir(lifecycleImageTar), filepath.Dir(lifecycleLayerName)+".tar") // this will be either /layer.tar (docker < 25.x) OR blobs/sha256.tar (docker 25.x and later OR containerd storage enabled) - if err = os.MkdirAll(filepath.Dir(lifecycleLayerTar), 0755); err != nil { - return "", err - } - lifecycleLayerWriter, err := os.OpenFile(lifecycleLayerTar, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) - if err != nil { - return "", err - } - defer lifecycleLayerWriter.Close() - if _, err = io.Copy(lifecycleLayerWriter, tarReader); err != nil { - return "", err - } - return lifecycleLayerTar, nil - }() - if err != nil { - return "", err - } - diffID, err := func() (string, error) { - lifecycleLayerReader, err := os.Open(lifecycleLayerTar) - if err != nil { - return "", err - } - defer lifecycleLayerReader.Close() - hasher := sha256.New() - if _, err = io.Copy(hasher, lifecycleLayerReader); err != nil { - return "", err - } - // it's weird that this doesn't match lifecycleLayerTar - return hex.EncodeToString(hasher.Sum(nil)), nil - }() - if err != nil { - return "", err - } - if err = ephemeralRunImage.AddLayerWithDiffID(lifecycleLayerTar, "sha256:"+diffID); err != nil { - return "", err - } - if err = ephemeralRunImage.Save(); err != nil { - return "", err - } - return ephemeralRunImageName, nil - } - - if err = c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil { - return fmt.Errorf("executing lifecycle: %w", err) - } - return c.logImageNameAndSha(ctx, opts.Publish, imageRef) -} - -func getTargetFromBuilder(builderImage imgutil.Image) (*dist.Target, error) { - builderOS, err := builderImage.OS() - if err != nil { - return nil, fmt.Errorf("failed to get builder OS: %w", err) - } - builderArch, err := builderImage.Architecture() - if err != nil { - return nil, fmt.Errorf("failed to get builder architecture: %w", err) - } - builderArchVariant, err := builderImage.Variant() - if err != nil { - return nil, fmt.Errorf("failed to get builder architecture variant: %w", err) - } - return &dist.Target{ - OS: builderOS, - Arch: builderArch, - ArchVariant: builderArchVariant, - }, nil -} - -func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) { - // sample contents of labels: - // {io.buildpacks.builder.metadata:\"{\"lifecycle\":{\"version\":\"0.15.3\"},\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}}", - // io.buildpacks.lifecycle.apis":"{\"buildpack\":{\"deprecated\":[],\"supported\":[\"0.2\",\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\"]},\"platform\":{\"deprecated\":[],\"supported\":[\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\",\"0.10\"]}}\",\"io.buildpacks.lifecycle.version\":\"0.15.3\"}") +func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) { + // sample contents of labels: + // {io.buildpacks.builder.metadata:\"{\"lifecycle\":{\"version\":\"0.15.3\"},\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}}", + // io.buildpacks.lifecycle.apis":"{\"buildpack\":{\"deprecated\":[],\"supported\":[\"0.2\",\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\"]},\"platform\":{\"deprecated\":[],\"supported\":[\"0.3\",\"0.4\",\"0.5\",\"0.6\",\"0.7\",\"0.8\",\"0.9\",\"0.10\"]}}\",\"io.buildpacks.lifecycle.version\":\"0.15.3\"}") // This struct is defined in lifecycle-repository/tools/image/main.go#Descriptor -- we could consider moving it from the main package to an importable location. var bpPlatformAPI struct { @@ -1550,138 +1245,458 @@ func randString(n int) string { return string(b) } -func (c *Client) logImageNameAndSha(ctx context.Context, publish bool, imageRef name.Reference) error { - // The image name and sha are printed in the lifecycle logs, and there is no need to print it again, unless output is suppressed. - if !logging.IsQuiet(c.logger) { - return nil +func (c *Client) logImageNameAndSha(ctx context.Context, publish bool, imageRef name.Reference) error { + // The image name and sha are printed in the lifecycle logs, and there is no need to print it again, unless output is suppressed. + if !logging.IsQuiet(c.logger) { + return nil + } + + img, err := c.imageFetcher.Fetch(ctx, imageRef.Name(), image.FetchOptions{Daemon: !publish, PullPolicy: image.PullNever}) + if err != nil { + return fmt.Errorf("fetching built image: %w", err) + } + + id, err := img.Identifier() + if err != nil { + return fmt.Errorf("reading image sha: %w", err) + } + + // Remove tag, if it exists, from the image name + imgName := strings.TrimSuffix(imageRef.String(), imageRef.Identifier()) + imgNameAndSha := fmt.Sprintf("%s@%s\n", imgName, parseDigestFromImageID(id)) + + // Access the logger's Writer directly to bypass ReportSuccessfulQuietBuild mode + _, err = c.logger.Writer().Write([]byte(imgNameAndSha)) + return err +} + +func parseDigestFromImageID(id imgutil.Identifier) string { + var digest string + switch v := id.(type) { + case local.IDIdentifier: + digest = v.String() + case remote.DigestIdentifier: + digest = v.Digest.DigestStr() + } + + digest = strings.TrimPrefix(digest, "sha256:") + return fmt.Sprintf("sha256:%s", digest) +} + +func createInlineBuildpack(bp projectTypes.Buildpack, stackID string) (string, error) { + pathToInlineBuilpack, err := os.MkdirTemp("", "inline-cnb") + if err != nil { + return pathToInlineBuilpack, err + } + + if bp.Version == "" { + bp.Version = "0.0.0" + } + + if err = createBuildpackTOML(pathToInlineBuilpack, bp.ID, bp.Version, bp.Script.API, []dist.Stack{{ID: stackID}}, []dist.Target{}, nil); err != nil { + return pathToInlineBuilpack, err + } + + shell := bp.Script.Shell + if shell == "" { + shell = "/bin/sh" + } + + binBuild := fmt.Sprintf(`#!%s + +%s +`, shell, bp.Script.Inline) + + binDetect := fmt.Sprintf(`#!%s + +exit 0 +`, shell) + + if err = createBinScript(pathToInlineBuilpack, "build", binBuild, nil); err != nil { + return pathToInlineBuilpack, err + } + + if err = createBinScript(pathToInlineBuilpack, "build.bat", bp.Script.Inline, nil); err != nil { + return pathToInlineBuilpack, err + } + + if err = createBinScript(pathToInlineBuilpack, "detect", binDetect, nil); err != nil { + return pathToInlineBuilpack, err + } + + if err = createBinScript(pathToInlineBuilpack, "detect.bat", bp.Script.Inline, nil); err != nil { + return pathToInlineBuilpack, err + } + + return pathToInlineBuilpack, nil +} + +// fullImagePath parses the inputImageReference provided by the user and creates the directory +// structure if create value is true +func fullImagePath(inputImageRef InputImageReference, create bool) (string, error) { + imagePath, err := inputImageRef.FullName() + if err != nil { + return "", errors.Wrapf(err, "evaluating image %s destination path", inputImageRef.Name()) + } + + if create { + if err := os.MkdirAll(imagePath, os.ModePerm); err != nil { + return "", errors.Wrapf(err, "creating %s layout application destination", imagePath) + } + } + + return imagePath, nil +} + +// appendLayoutVolumes mount host volume into the build container, in the form ':[:]' +// the volumes mounted are: +// - The path where the user wants the image to be exported in OCI layout format +// - The previous image path if it exits +// - The run-image path +func appendLayoutVolumes(volumes []string, config layoutPathConfig) []string { + if config.hostPreviousImagePath != "" { + volumes = append(volumes, readOnlyVolume(config.hostPreviousImagePath, config.targetPreviousImagePath), + readOnlyVolume(config.hostRunImagePath, config.targetRunImagePath), + writableVolume(config.hostImagePath, config.targetImagePath)) + } else { + volumes = append(volumes, readOnlyVolume(config.hostRunImagePath, config.targetRunImagePath), + writableVolume(config.hostImagePath, config.targetImagePath)) + } + return volumes +} + +func writableVolume(hostPath, targetPath string) string { + tp := targetPath + if !filepath.IsAbs(targetPath) { + tp = filepath.Join(string(filepath.Separator), targetPath) + } + return fmt.Sprintf("%s:%s:rw", hostPath, tp) +} + +func readOnlyVolume(hostPath, targetPath string) string { + tp := targetPath + if !filepath.IsAbs(targetPath) { + tp = filepath.Join(string(filepath.Separator), targetPath) + } + return fmt.Sprintf("%s:%s", hostPath, tp) +} + +func (c *Client) ResolveLifecycleOptions(ctx context.Context, opts BuildOptions) (*build.LifecycleOptions, error) { + var pathsConfig layoutPathConfig + + imageRef, err := c.parseReference(opts) + if err != nil { + return nil, errors.Wrapf(err, "invalid image name '%s'", opts.Image) + } + imgRegistry := imageRef.Context().RegistryStr() + imageName := imageRef.Name() + + if opts.Layout() { + pathsConfig, err = c.processLayoutPath(opts.LayoutConfig.InputImage, opts.LayoutConfig.PreviousInputImage) + if err != nil { + if opts.LayoutConfig.PreviousInputImage != nil { + return nil, errors.Wrapf(err, "invalid layout paths image name '%s' or previous-image name '%s'", opts.LayoutConfig.InputImage.Name(), + opts.LayoutConfig.PreviousInputImage.Name()) + } + return nil, errors.Wrapf(err, "invalid layout paths image name '%s'", opts.LayoutConfig.InputImage.Name()) + } + } + + appPath, err := c.processAppPath(opts.AppPath) + if err != nil { + return nil, errors.Wrapf(err, "invalid app path '%s'", opts.AppPath) + } + + proxyConfig := c.processProxyConfig(opts.ProxyConfig) + + builderRef, err := c.processBuilderName(opts.Builder) + if err != nil { + return nil, errors.Wrapf(err, "invalid builder '%s'", opts.Builder) + } + + requestedTarget := func() *dist.Target { + if opts.Platform == "" { + return nil + } + parts := strings.Split(opts.Platform, "/") + switch len(parts) { + case 0: + return nil + case 1: + return &dist.Target{OS: parts[0]} + case 2: + return &dist.Target{OS: parts[0], Arch: parts[1]} + default: + return &dist.Target{OS: parts[0], Arch: parts[1], ArchVariant: parts[2]} + } + }() + + rawBuilderImage, err := c.imageFetcher.Fetch( + ctx, + builderRef.Name(), + image.FetchOptions{ + Daemon: true, + Target: requestedTarget, + PullPolicy: opts.PullPolicy}, + ) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name()) + } + + var targetToUse *dist.Target + if requestedTarget != nil { + targetToUse = requestedTarget + } else { + targetToUse, err = getTargetFromBuilder(rawBuilderImage) + if err != nil { + return nil, err + } + } + + bldr, err := c.getBuilder(rawBuilderImage) + if err != nil { + return nil, errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder)) + } + + fetchOptions := image.FetchOptions{ + Daemon: !opts.Publish, + PullPolicy: opts.PullPolicy, + Target: targetToUse, + } + runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, fetchOptions) + + if opts.Layout() { + targetRunImagePath, err := layout.ParseRefToPath(runImageName) + if err != nil { + return nil, err + } + hostRunImagePath := filepath.Join(opts.LayoutConfig.LayoutRepoDir, targetRunImagePath) + targetRunImagePath = filepath.Join(paths.RootDir, "layout-repo", targetRunImagePath) + fetchOptions.LayoutOption = image.LayoutOption{ + Path: hostRunImagePath, + Sparse: opts.LayoutConfig.Sparse, + } + fetchOptions.Daemon = false + pathsConfig.targetRunImagePath = targetRunImagePath + pathsConfig.hostRunImagePath = hostRunImagePath + } + runImage, err := c.validateRunImage(ctx, runImageName, fetchOptions, bldr.StackID) + if err != nil { + return nil, errors.Wrapf(err, "invalid run-image '%s'", runImageName) + } + + var runMixins []string + if _, err := dist.GetLabel(runImage, stack.MixinsLabel, &runMixins); err != nil { + return nil, err } - img, err := c.imageFetcher.Fetch(ctx, imageRef.Name(), image.FetchOptions{Daemon: !publish, PullPolicy: image.PullNever}) + fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts, targetToUse) if err != nil { - return fmt.Errorf("fetching built image: %w", err) + return nil, err } - id, err := img.Identifier() + fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Extensions(), opts, targetToUse) if err != nil { - return fmt.Errorf("reading image sha: %w", err) + return nil, err } - // Remove tag, if it exists, from the image name - imgName := strings.TrimSuffix(imageRef.String(), imageRef.Identifier()) - imgNameAndSha := fmt.Sprintf("%s@%s\n", imgName, parseDigestFromImageID(id)) - - // Access the logger's Writer directly to bypass ReportSuccessfulQuietBuild mode - _, err = c.logger.Writer().Write([]byte(imgNameAndSha)) - return err -} + // Default mode: if the TrustBuilder option is not set, trust the suggested builders. + if opts.TrustBuilder == nil { + opts.TrustBuilder = IsTrustedBuilderFunc + } -func parseDigestFromImageID(id imgutil.Identifier) string { - var digest string - switch v := id.(type) { - case local.IDIdentifier: - digest = v.String() - case remote.DigestIdentifier: - digest = v.Digest.DigestStr() + // Ensure the builder's platform APIs are supported + var builderPlatformAPIs builder.APISet + builderPlatformAPIs = append(builderPlatformAPIs, bldr.LifecycleDescriptor().APIs.Platform.Deprecated...) + builderPlatformAPIs = append(builderPlatformAPIs, bldr.LifecycleDescriptor().APIs.Platform.Supported...) + if !supportsPlatformAPI(builderPlatformAPIs) { + c.logger.Debugf("pack %s supports Platform API(s): %s", c.version, strings.Join(build.SupportedPlatformAPIVersions.AsStrings(), ", ")) + c.logger.Debugf("Builder %s supports Platform API(s): %s", style.Symbol(opts.Builder), strings.Join(builderPlatformAPIs.AsStrings(), ", ")) + return nil, errors.Errorf("Builder %s is incompatible with this version of pack", style.Symbol(opts.Builder)) } - digest = strings.TrimPrefix(digest, "sha256:") - return fmt.Sprintf("sha256:%s", digest) -} + // Get the platform API version to use + lifecycleVersion := bldr.LifecycleDescriptor().Info.Version + useCreator := supportsCreator(lifecycleVersion) && opts.TrustBuilder(opts.Builder) + var ( + lifecycleOptsLifecycleImage string + lifecycleAPIs []string + ) + if !(useCreator) { + // fetch the lifecycle image + if supportsLifecycleImage(lifecycleVersion) { + lifecycleImageName := opts.LifecycleImage + if lifecycleImageName == "" { + lifecycleImageName = fmt.Sprintf("%s:%s", internalConfig.DefaultLifecycleImageRepo, lifecycleVersion.String()) + } -func createInlineBuildpack(bp projectTypes.Buildpack, stackID string) (string, error) { - pathToInlineBuilpack, err := os.MkdirTemp("", "inline-cnb") - if err != nil { - return pathToInlineBuilpack, err - } + lifecycleImage, err := c.imageFetcher.Fetch( + ctx, + lifecycleImageName, + image.FetchOptions{ + Daemon: true, + PullPolicy: opts.PullPolicy, + Target: targetToUse, + }, + ) + if err != nil { + return nil, fmt.Errorf("fetching lifecycle image: %w", err) + } - if bp.Version == "" { - bp.Version = "0.0.0" - } + // if lifecyle container os isn't windows, use ephemeral lifecycle to add /workspace with correct ownership + imageOS, err := lifecycleImage.OS() + if err != nil { + return nil, errors.Wrap(err, "getting lifecycle image OS") + } + if imageOS != "windows" { + // obtain uid/gid from builder to use when extending lifecycle image + uid, gid, err := userAndGroupIDs(rawBuilderImage) + if err != nil { + return nil, fmt.Errorf("obtaining build uid/gid from builder image: %w", err) + } - if err = createBuildpackTOML(pathToInlineBuilpack, bp.ID, bp.Version, bp.Script.API, []dist.Stack{{ID: stackID}}, []dist.Target{}, nil); err != nil { - return pathToInlineBuilpack, err - } + c.logger.Debugf("Creating ephemeral lifecycle from %s with uid %d and gid %d. With workspace dir %s", lifecycleImage.Name(), uid, gid, opts.Workspace) + // extend lifecycle image with mountpoints, and use it instead of current lifecycle image + lifecycleImage, err = c.createEphemeralLifecycle(lifecycleImage, opts.Workspace, uid, gid) + if err != nil { + return nil, err + } + c.logger.Debugf("Selecting ephemeral lifecycle image %s for build", lifecycleImage.Name()) + } - shell := bp.Script.Shell - if shell == "" { - shell = "/bin/sh" + lifecycleOptsLifecycleImage = lifecycleImage.Name() + labels, err := lifecycleImage.Labels() + if err != nil { + return nil, fmt.Errorf("reading labels of lifecycle image: %w", err) + } + + lifecycleAPIs, err = extractSupportedLifecycleApis(labels) + if err != nil { + return nil, fmt.Errorf("reading api versions of lifecycle image: %w", err) + } + } } - binBuild := fmt.Sprintf(`#!%s + usingPlatformAPI, err := build.FindLatestSupported(append( + bldr.LifecycleDescriptor().APIs.Platform.Deprecated, + bldr.LifecycleDescriptor().APIs.Platform.Supported...), + lifecycleAPIs) + if err != nil { + return nil, fmt.Errorf("finding latest supported Platform API: %w", err) + } + if usingPlatformAPI.LessThan("0.12") { + if err = c.validateMixins(fetchedBPs, bldr, runImageName, runMixins); err != nil { + return nil, fmt.Errorf("validating stack mixins: %w", err) + } + } -%s -`, shell, bp.Script.Inline) + buildEnvs := map[string]string{} + for _, envVar := range opts.ProjectDescriptor.Build.Env { + buildEnvs[envVar.Name] = envVar.Value + } - binDetect := fmt.Sprintf(`#!%s + for k, v := range opts.Env { + buildEnvs[k] = v + } -exit 0 -`, shell) + ephemeralBuilder, err := c.createEphemeralBuilder(rawBuilderImage, buildEnvs, order, fetchedBPs, orderExtensions, fetchedExs, usingPlatformAPI.LessThan("0.12"), opts.RunImage) + if err != nil { + return nil, err + } - if err = createBinScript(pathToInlineBuilpack, "build", binBuild, nil); err != nil { - return pathToInlineBuilpack, err + if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 { + if targetToUse.OS == "windows" { + return nil, fmt.Errorf("builder contains image extensions which are not supported for Windows builds") + } + if !(opts.PullPolicy == image.PullAlways) { + return nil, fmt.Errorf("pull policy must be 'always' when builder contains image extensions") + } } - if err = createBinScript(pathToInlineBuilpack, "build.bat", bp.Script.Inline, nil); err != nil { - return pathToInlineBuilpack, err + if opts.Layout() { + opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig) } - if err = createBinScript(pathToInlineBuilpack, "detect", binDetect, nil); err != nil { - return pathToInlineBuilpack, err + processedVolumes, warnings, err := processVolumes(targetToUse.OS, opts.ContainerConfig.Volumes) + if err != nil { + return nil, err } - if err = createBinScript(pathToInlineBuilpack, "detect.bat", bp.Script.Inline, nil); err != nil { - return pathToInlineBuilpack, err + for _, warning := range warnings { + c.logger.Warn(warning) } - return pathToInlineBuilpack, nil -} + fileFilter, err := getFileFilter(opts.ProjectDescriptor) + if err != nil { + return nil, err + } -// fullImagePath parses the inputImageReference provided by the user and creates the directory -// structure if create value is true -func fullImagePath(inputImageRef InputImageReference, create bool) (string, error) { - imagePath, err := inputImageRef.FullName() + runImageName, err = pname.TranslateRegistry(runImageName, c.registryMirrors, c.logger) if err != nil { - return "", errors.Wrapf(err, "evaluating image %s destination path", inputImageRef.Name()) + return nil, err } - if create { - if err := os.MkdirAll(imagePath, os.ModePerm); err != nil { - return "", errors.Wrapf(err, "creating %s layout application destination", imagePath) + projectMetadata := files.ProjectMetadata{} + if c.experimental { + version := opts.ProjectDescriptor.Project.Version + sourceURL := opts.ProjectDescriptor.Project.SourceURL + if version != "" || sourceURL != "" { + projectMetadata.Source = &files.ProjectSource{ + Type: "project", + Version: map[string]interface{}{"declared": version}, + Metadata: map[string]interface{}{"url": sourceURL}, + } + } else { + projectMetadata.Source = v02.GitMetadata(opts.AppPath) } } - return imagePath, nil -} - -// appendLayoutVolumes mount host volume into the build container, in the form ':[:]' -// the volumes mounted are: -// - The path where the user wants the image to be exported in OCI layout format -// - The previous image path if it exits -// - The run-image path -func appendLayoutVolumes(volumes []string, config layoutPathConfig) []string { - if config.hostPreviousImagePath != "" { - volumes = append(volumes, readOnlyVolume(config.hostPreviousImagePath, config.targetPreviousImagePath), - readOnlyVolume(config.hostRunImagePath, config.targetRunImagePath), - writableVolume(config.hostImagePath, config.targetImagePath)) - } else { - volumes = append(volumes, readOnlyVolume(config.hostRunImagePath, config.targetRunImagePath), - writableVolume(config.hostImagePath, config.targetImagePath)) + lifecycleOpts := build.LifecycleOptions{ + AppPath: appPath, + Image: imageRef, + Builder: ephemeralBuilder, + BuilderImage: builderRef.Name(), + LifecycleImage: ephemeralBuilder.Name(), + RunImage: runImageName, + ProjectMetadata: projectMetadata, + ClearCache: opts.ClearCache, + Publish: opts.Publish, + TrustBuilder: opts.TrustBuilder(opts.Builder), + UseCreator: useCreator, + UseCreatorWithExtensions: supportsCreatorWithExtensions(lifecycleVersion), + DockerHost: opts.DockerHost, + Cache: opts.Cache, + CacheImage: opts.CacheImage, + HTTPProxy: proxyConfig.HTTPProxy, + HTTPSProxy: proxyConfig.HTTPSProxy, + NoProxy: proxyConfig.NoProxy, + Network: opts.ContainerConfig.Network, + AdditionalTags: opts.AdditionalTags, + Volumes: processedVolumes, + DefaultProcessType: opts.DefaultProcessType, + FileFilter: fileFilter, + Workspace: opts.Workspace, + GID: opts.GroupID, + UID: opts.UserID, + PreviousImage: opts.PreviousImage, + Interactive: opts.Interactive, + Termui: termui.NewTermui(imageName, ephemeralBuilder, runImageName), + ReportDestinationDir: opts.ReportDestinationDir, + SBOMDestinationDir: opts.SBOMDestinationDir, + CreationTime: opts.CreationTime, + Layout: opts.Layout(), + Keychain: c.keychain, } - return volumes -} -func writableVolume(hostPath, targetPath string) string { - tp := targetPath - if !filepath.IsAbs(targetPath) { - tp = filepath.Join(string(filepath.Separator), targetPath) + switch { + case useCreator: + lifecycleOpts.UseCreator = true + case supportsLifecycleImage(lifecycleVersion): + lifecycleOpts.LifecycleImage = lifecycleOptsLifecycleImage + lifecycleOpts.LifecycleApis = lifecycleAPIs + case !opts.TrustBuilder(opts.Builder): + return nil, errors.Errorf("Lifecycle %s does not have an associated lifecycle image. Builder must be trusted.", lifecycleVersion.String()) } - return fmt.Sprintf("%s:%s:rw", hostPath, tp) -} -func readOnlyVolume(hostPath, targetPath string) string { - tp := targetPath - if !filepath.IsAbs(targetPath) { - tp = filepath.Join(string(filepath.Separator), targetPath) - } - return fmt.Sprintf("%s:%s", hostPath, tp) + return &lifecycleOpts, nil } diff --git a/pkg/client/detect.go b/pkg/client/detect.go new file mode 100644 index 0000000000..20703c9fa8 --- /dev/null +++ b/pkg/client/detect.go @@ -0,0 +1,24 @@ +package client + +import ( + "context" + "fmt" + + "github.com/docker/docker/api/types" +) + +func (c *Client) Detect(ctx context.Context, opts BuildOptions) error { + lifecycleOpts, err := c.ResolveLifecycleOptions(ctx, opts) + if err != nil { + return err + } + + defer c.docker.ImageRemove(context.Background(), lifecycleOpts.LifecycleImage, types.ImageRemoveOptions{Force: true}) + defer c.docker.ImageRemove(context.Background(), lifecycleOpts.Builder.Name(), types.ImageRemoveOptions{Force: true}) + + if err = c.lifecycleExecutor.Detect(ctx, *lifecycleOpts); err != nil { + return fmt.Errorf("executing detect: %w", err) + } + // Log / Save to disk, the final detected group + return nil +} From c00a832fa27aa86577b4bb23b7ec290b3ca33210 Mon Sep 17 00:00:00 2001 From: Rashad Sirajudeen Date: Fri, 14 Jun 2024 07:37:48 +0530 Subject: [PATCH 2/4] fixed mocks Signed-off-by: Rashad Sirajudeen --- internal/commands/build.go | 1 - internal/commands/testmocks/mock_pack_client.go | 14 ++++++++++++++ internal/fakes/fake_lifecycle.go | 5 +++++ pkg/client/detect.go | 5 +---- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/internal/commands/build.go b/internal/commands/build.go index 2297cdbaee..007c801ce9 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -284,7 +284,6 @@ This option may set DOCKER_HOST environment variable for the build container if cmd.Flags().MarkHidden("sparse") } } - } func validateBuildFlags(flags *BuildFlags, cfg config.Config, inputImageRef client.InputImageReference, logger logging.Logger) error { diff --git a/internal/commands/testmocks/mock_pack_client.go b/internal/commands/testmocks/mock_pack_client.go index 981704c090..125cd83fbf 100644 --- a/internal/commands/testmocks/mock_pack_client.go +++ b/internal/commands/testmocks/mock_pack_client.go @@ -120,6 +120,20 @@ func (mr *MockPackClientMockRecorder) DeleteManifest(arg0 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteManifest", reflect.TypeOf((*MockPackClient)(nil).DeleteManifest), arg0) } +// Detect mocks base method. +func (m *MockPackClient) Detect(arg0 context.Context, arg1 client.BuildOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Detect", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Detect indicates an expected call of Detect. +func (mr *MockPackClientMockRecorder) Detect(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detect", reflect.TypeOf((*MockPackClient)(nil).Detect), arg0, arg1) +} + // DownloadSBOM mocks base method. func (m *MockPackClient) DownloadSBOM(arg0 string, arg1 client.DownloadSBOMOptions) error { m.ctrl.T.Helper() diff --git a/internal/fakes/fake_lifecycle.go b/internal/fakes/fake_lifecycle.go index 280c1bf1d8..a431ef69ec 100644 --- a/internal/fakes/fake_lifecycle.go +++ b/internal/fakes/fake_lifecycle.go @@ -14,3 +14,8 @@ func (f *FakeLifecycle) Execute(ctx context.Context, opts build.LifecycleOptions f.Opts = opts return nil } + +func (f *FakeLifecycle) Detect(ctx context.Context, opts build.LifecycleOptions) error { + f.Opts = opts + return nil +} diff --git a/pkg/client/detect.go b/pkg/client/detect.go index 20703c9fa8..7f0bf69c8d 100644 --- a/pkg/client/detect.go +++ b/pkg/client/detect.go @@ -3,8 +3,6 @@ package client import ( "context" "fmt" - - "github.com/docker/docker/api/types" ) func (c *Client) Detect(ctx context.Context, opts BuildOptions) error { @@ -13,8 +11,7 @@ func (c *Client) Detect(ctx context.Context, opts BuildOptions) error { return err } - defer c.docker.ImageRemove(context.Background(), lifecycleOpts.LifecycleImage, types.ImageRemoveOptions{Force: true}) - defer c.docker.ImageRemove(context.Background(), lifecycleOpts.Builder.Name(), types.ImageRemoveOptions{Force: true}) + // TODO: Cleanup if err = c.lifecycleExecutor.Detect(ctx, *lifecycleOpts); err != nil { return fmt.Errorf("executing detect: %w", err) From 63b140d8cfb3229d240eb55a5ab344bcfdb6d80e Mon Sep 17 00:00:00 2001 From: Rashad Sirajudeen Date: Fri, 14 Jun 2024 09:46:51 +0530 Subject: [PATCH 3/4] changed detect to execute detect Signed-off-by: Rashad Sirajudeen --- cmd/cmd.go | 2 +- internal/commands/execute.go | 20 +++++++++++++++++++ .../commands/{detect.go => execute_detect.go} | 8 ++++---- 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 internal/commands/execute.go rename internal/commands/{detect.go => execute_detect.go} (87%) diff --git a/cmd/cmd.go b/cmd/cmd.go index 98ebc13422..093ee94973 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -75,7 +75,7 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { commands.AddHelpFlag(rootCmd, "pack") - rootCmd.AddCommand(commands.Detect(logger, cfg, packClient)) + rootCmd.AddCommand(commands.ExecuteCommand(logger, cfg, packClient)) rootCmd.AddCommand(commands.Build(logger, cfg, packClient)) rootCmd.AddCommand(commands.NewBuilderCommand(logger, cfg, packClient)) rootCmd.AddCommand(commands.NewBuildpackCommand(logger, cfg, packClient, buildpackage.NewConfigReader())) diff --git a/internal/commands/execute.go b/internal/commands/execute.go new file mode 100644 index 0000000000..a21a61bf3f --- /dev/null +++ b/internal/commands/execute.go @@ -0,0 +1,20 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/config" + "github.com/buildpacks/pack/pkg/logging" +) + +func ExecuteCommand(logger logging.Logger, cfg config.Config, client PackClient) *cobra.Command { + cmd := &cobra.Command{ + Use: "execute", + Short: "Executes a specific phase in the buildpacks lifecycle", + RunE: nil, + } + + cmd.AddCommand(ExecuteDetect(logger, cfg, client)) + AddHelpFlag(cmd, "execute") + return cmd +} diff --git a/internal/commands/detect.go b/internal/commands/execute_detect.go similarity index 87% rename from internal/commands/detect.go rename to internal/commands/execute_detect.go index b25674aff2..f42eba4043 100644 --- a/internal/commands/detect.go +++ b/internal/commands/execute_detect.go @@ -15,16 +15,16 @@ import ( ) // Run up to the detect phase of the CNB lifecycle against a source code directory -func Detect(logger logging.Logger, cfg config.Config, packClient PackClient) *cobra.Command { +func ExecuteDetect(logger logging.Logger, cfg config.Config, packClient PackClient) *cobra.Command { var flags BuildFlags flags.DetectOnly = true cmd := &cobra.Command{ Use: "detect", Args: cobra.ExactArgs(1), - Short: "Pack Detect runs the analyze and detect phases of the Cloud Native Buildpacks lifecycle to determine a group of applicable buildpacks and a build plan.", - Example: "pack detect --path apps/test-app --builder cnbs/sample-builder:bionic", - Long: "Pack Detect uses Cloud Native Buildpacks to run the detect phase of buildpack groups against the source code.\n", + Short: "Execute detect runs the analyze and detect phases of the Cloud Native Buildpacks lifecycle to determine a group of applicable buildpacks and a build plan.", + Example: "pack execute detect --path apps/test-app --builder cnbs/sample-builder:bionic", + Long: "Execute detect uses Cloud Native Buildpacks to run the detect phase of buildpack groups against the source code.\n", RunE: logError(logger, func(cmd *cobra.Command, args []string) error { inputImageName := client.ParseInputImageReference(args[0]) if err := validateBuildFlags(&flags, cfg, inputImageName, logger); err != nil { From ae266dd3b4238e31dbb74e743e59ba5806e4650e Mon Sep 17 00:00:00 2001 From: Rashad Sirajudeen Date: Fri, 14 Jun 2024 10:35:47 +0530 Subject: [PATCH 4/4] added support for --detect-output-dir Signed-off-by: Rashad Sirajudeen --- internal/build/lifecycle_execution.go | 4 ++++ internal/build/lifecycle_executor.go | 1 + internal/build/mount_paths.go | 4 ++++ internal/commands/build.go | 2 ++ internal/commands/execute_detect.go | 21 ++++++++++++--------- pkg/client/build.go | 3 +++ pkg/client/detect.go | 2 +- 7 files changed, 27 insertions(+), 10 deletions(-) diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index f18a3b7ffd..c74c178ae3 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -452,6 +452,10 @@ func (l *LifecycleExecution) Detect(ctx context.Context, phaseFactory PhaseFacto CopyOutToMaybe(filepath.Join(l.mountPaths.layersDir(), "analyzed.toml"), l.tmpDir))), If(l.hasExtensions(), WithPostContainerRunOperations( CopyOutToMaybe(filepath.Join(l.mountPaths.layersDir(), "generated"), l.tmpDir))), + If(l.opts.GroupDestinationDir != "", WithPostContainerRunOperations( + EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume), + CopyOutTo(l.mountPaths.groupPath(), l.opts.GroupDestinationDir))), + envOp, ) diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index 681be27926..bf94c56b29 100644 --- a/internal/build/lifecycle_executor.go +++ b/internal/build/lifecycle_executor.go @@ -102,6 +102,7 @@ type LifecycleOptions struct { PreviousImage string ReportDestinationDir string SBOMDestinationDir string + GroupDestinationDir string CreationTime *time.Time Keychain authn.Keychain FetchOptions image.FetchOptions diff --git a/internal/build/mount_paths.go b/internal/build/mount_paths.go index ad4bd91136..57da995ab0 100644 --- a/internal/build/mount_paths.go +++ b/internal/build/mount_paths.go @@ -73,3 +73,7 @@ func (m mountPaths) launchCacheDir() string { func (m mountPaths) sbomDir() string { return m.join(m.volume, "layers", "sbom") } + +func (m mountPaths) groupPath() string { + return m.join(m.layersDir(), "group.toml") +} diff --git a/internal/commands/build.go b/internal/commands/build.go index 007c801ce9..b6422a2141 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -51,6 +51,7 @@ type BuildFlags struct { GID int UID int PreviousImage string + GroupDestinationDir string SBOMDestinationDir string ReportDestinationDir string DateTime string @@ -259,6 +260,7 @@ This option may set DOCKER_HOST environment variable for the build container if cmd.Flags().StringVar(&buildFlags.Workspace, "workspace", "", "Location at which to mount the app dir in the build image") cmd.Flags().IntVar(&buildFlags.GID, "gid", 0, `Override GID of user's group in the stack's build and run images. The provided value must be a positive number`) cmd.Flags().IntVar(&buildFlags.UID, "uid", 0, `Override UID of user in the stack's build and run images. The provided value must be a positive number`) + cmd.Flags().StringVar(&buildFlags.GroupDestinationDir, "detect-output-dir", "", "Path to export group.toml.") if !buildFlags.DetectOnly { cmd.Flags().Var(&buildFlags.Cache, "cache", diff --git a/internal/commands/execute_detect.go b/internal/commands/execute_detect.go index f42eba4043..ddcb7dbda9 100644 --- a/internal/commands/execute_detect.go +++ b/internal/commands/execute_detect.go @@ -54,6 +54,8 @@ func ExecuteDetect(logger logging.Logger, cfg config.Config, packClient PackClie buildpacks := flags.Buildpacks extensions := flags.Extensions + groupDestinationDir := flags.GroupDestinationDir + env, err := parseEnv(flags.EnvFiles, flags.Env) if err != nil { return err @@ -103,15 +105,16 @@ func ExecuteDetect(logger logging.Logger, cfg config.Config, packClient PackClie Network: flags.Network, Volumes: flags.Volumes, }, - LifecycleImage: lifecycleImage, - PreBuildpacks: flags.PreBuildpacks, - PostBuildpacks: flags.PostBuildpacks, - Buildpacks: buildpacks, - Extensions: extensions, - Workspace: flags.Workspace, - GroupID: gid, - UserID: uid, - DetectOnly: true, + LifecycleImage: lifecycleImage, + PreBuildpacks: flags.PreBuildpacks, + PostBuildpacks: flags.PostBuildpacks, + Buildpacks: buildpacks, + Extensions: extensions, + Workspace: flags.Workspace, + GroupID: gid, + UserID: uid, + DetectOnly: true, + GroupDestinationDir: groupDestinationDir, }); err != nil { return errors.Wrap(err, "failed to detect") } diff --git a/pkg/client/build.go b/pkg/client/build.go index 80a0541fd5..9742efea3e 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -203,6 +203,9 @@ type BuildOptions struct { // Only trust builders from reputable sources. TrustBuilder IsTrustedBuilder + // Directory to output any SBOM artifacts + GroupDestinationDir string + // Directory to output any SBOM artifacts SBOMDestinationDir string diff --git a/pkg/client/detect.go b/pkg/client/detect.go index 7f0bf69c8d..22da8b28df 100644 --- a/pkg/client/detect.go +++ b/pkg/client/detect.go @@ -11,7 +11,7 @@ func (c *Client) Detect(ctx context.Context, opts BuildOptions) error { return err } - // TODO: Cleanup + lifecycleOpts.GroupDestinationDir = opts.GroupDestinationDir if err = c.lifecycleExecutor.Detect(ctx, *lifecycleOpts); err != nil { return fmt.Errorf("executing detect: %w", err)