diff --git a/cmd/cmd.go b/cmd/cmd.go index 837c9c96f4..1763b0107c 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -76,6 +76,7 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { commands.AddHelpFlag(rootCmd, "pack") rootCmd.AddCommand(commands.Build(logger, cfg, packClient)) + rootCmd.AddCommand(commands.Detect(logger, cfg, packClient)) rootCmd.AddCommand(commands.NewBuilderCommand(logger, cfg, packClient)) rootCmd.AddCommand(commands.NewBuildpackCommand(logger, cfg, packClient, buildpackage.NewConfigReader())) rootCmd.AddCommand(commands.NewExtensionCommand(logger, cfg, packClient, buildpackage.NewConfigReader())) diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index 0f5c9a49a4..a51ac752cd 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -40,6 +40,7 @@ type LifecycleExecution struct { mountPaths mountPaths opts LifecycleOptions tmpDir string + DetectOnly bool } func NewLifecycleExecution(logger logging.Logger, docker DockerClient, tmpDir string, opts LifecycleOptions) (*LifecycleExecution, error) { @@ -66,6 +67,7 @@ func NewLifecycleExecution(logger logging.Logger, docker DockerClient, tmpDir st os: osType, mountPaths: mountPathsForOS(osType, opts.Workspace), tmpDir: tmpDir, + DetectOnly: opts.DetectOnly, } if opts.Interactive { @@ -295,6 +297,28 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF return l.Create(ctx, buildCache, launchCache, phaseFactory) } +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 +} + func (l *LifecycleExecution) Cleanup() error { var reterr error if err := l.docker.VolumeRemove(context.Background(), l.layersVolume, true); err != nil { @@ -550,7 +574,15 @@ func (l *LifecycleExecution) Restore(ctx context.Context, buildCache Cache, kani func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCache Cache, phaseFactory PhaseFactory) error { var flags []string - args := []string{l.opts.Image.String()} + var args []string + + // TODO: a better way to handle the image name (no args are passed to detect) + if l.DetectOnly { + args = []string{"fakeimage"} + } else { + args = []string{l.opts.Image.String()} + } + platformAPILessThan07 := l.platformAPI.LessThan("0.7") cacheBindOp := NullOp() @@ -571,7 +603,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach } launchCacheBindOp := NullOp() - if l.platformAPI.AtLeast("0.9") { + if l.platformAPI.AtLeast("0.9") && !l.DetectOnly { if !l.opts.Publish { args = append([]string{"-launch-cache", l.mountPaths.launchCacheDir()}, args...) launchCacheBindOp = WithBinds(fmt.Sprintf("%s:%s", launchCache.Name(), l.mountPaths.launchCacheDir())) @@ -586,7 +618,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach flags = append(flags, "-uid", strconv.Itoa(l.opts.UID)) } - if l.opts.PreviousImage != "" { + if l.opts.PreviousImage != "" && !l.DetectOnly { if l.opts.Image == nil { return errors.New("image can't be nil") } diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index 09d9011f0c..2f610f8909 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" ) @@ -102,6 +103,8 @@ type LifecycleOptions struct { SBOMDestinationDir string CreationTime *time.Time Keychain authn.Keychain + DetectOnly bool + FetchOptions image.FetchOptions } func NewLifecycleExecutor(logger logging.Logger, docker DockerClient) *LifecycleExecutor { @@ -129,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/builder/testmocks/mock_lifecycle.go b/internal/builder/testmocks/mock_lifecycle.go index 88aacd786f..fabeab8401 100644 --- a/internal/builder/testmocks/mock_lifecycle.go +++ b/internal/builder/testmocks/mock_lifecycle.go @@ -8,9 +8,8 @@ import ( io "io" reflect "reflect" - gomock "github.com/golang/mock/gomock" - builder "github.com/buildpacks/pack/internal/builder" + gomock "github.com/golang/mock/gomock" ) // MockLifecycle is a mock of Lifecycle interface. diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 11fa645ca5..5352206e85 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -26,6 +26,7 @@ type PackClient interface { PackageBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error PackageExtension(ctx context.Context, opts client.PackageBuildpackOptions) error Build(context.Context, client.BuildOptions) error + Detect(context.Context, client.BuildOptions) error RegisterBuildpack(context.Context, client.RegisterBuildpackOptions) error YankBuildpack(client.YankBuildpackOptions) error InspectBuildpack(client.InspectBuildpackOptions) (*client.BuildpackInfo, error) diff --git a/internal/commands/detect.go b/internal/commands/detect.go new file mode 100644 index 0000000000..b443272c1d --- /dev/null +++ b/internal/commands/detect.go @@ -0,0 +1,179 @@ +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" +) + +type DetectFlags struct { + AppPath string + Builder string + Registry string + DescriptorPath string + LifecycleImage string + Volumes []string + PreBuildpacks []string + PostBuildpacks []string + Policy string + Network string + Env []string + EnvFiles []string + Buildpacks []string + Extensions []string + Workspace string + GID int + UID int + Publish bool + DockerHost string + RunImage string +} + +// Run Detect phase of lifecycle against a source code +func Detect(logger logging.Logger, cfg config.Config, packClient PackClient) *cobra.Command { + var flags DetectFlags + + cmd := &cobra.Command{ + Use: "detect", + Args: cobra.ExactArgs(0), + Short: "Run the detect phase of buildpacks against your source code", + 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" + + "You can use `--path` to specify a different source code directory, else it defaults to the current directory. Detect requires a `builder`, which can either " + + "be provided directly to build using `--builder`, or can be set using the `set-default-builder` command.", + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + if err := validateDetectFlags(&flags, cfg, 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, + 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 + }), + } + detectCommandFlags(cmd, &flags, cfg) + AddHelpFlag(cmd, "detect") + return cmd +} + +// TODO: Incomplete +func validateDetectFlags(flags *DetectFlags, cfg config.Config, logger logging.Logger) error { + return nil +} + +func detectCommandFlags(cmd *cobra.Command, detectFlags *DetectFlags, cfg config.Config) { + cmd.Flags().StringVarP(&detectFlags.AppPath, "path", "p", "", "Path to app dir or zip-formatted file (defaults to current working directory)") + cmd.Flags().StringSliceVarP(&detectFlags.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(&detectFlags.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(&detectFlags.Builder, "builder", "B", cfg.DefaultBuilder, "Builder image") + cmd.Flags().StringArrayVarP(&detectFlags.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(&detectFlags.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(&detectFlags.Network, "network", "", "Connect detect and build containers to network") + cmd.Flags().StringVar(&detectFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`) + cmd.Flags().StringVarP(&detectFlags.DescriptorPath, "descriptor", "d", "", "Path to the project descriptor file") + cmd.Flags().StringVar(&detectFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`) + cmd.Flags().StringArrayVar(&detectFlags.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().StringArrayVar(&detectFlags.PreBuildpacks, "pre-buildpack", []string{}, "Buildpacks to prepend to the groups in the builder's order") + cmd.Flags().StringArrayVar(&detectFlags.PostBuildpacks, "post-buildpack", []string{}, "Buildpacks to append to the groups in the builder's order") + cmd.Flags().BoolVar(&detectFlags.Publish, "publish", false, "Publish the application image directly to the container registry specified in , instead of the daemon. The run image must also reside in the registry.") + cmd.Flags().StringVarP(&detectFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name") + cmd.Flags().StringVar(&detectFlags.DockerHost, "docker-host", "", + `Address to docker daemon that will be exposed to the build container. +If not set (or set to empty string) the standard socket location will be used. +Special value 'inherit' may be used in which case DOCKER_HOST environment variable will be used. +This option may set DOCKER_HOST environment variable for the build container if needed. +`) + cmd.Flags().IntVar(&detectFlags.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(&detectFlags.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(&detectFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)") + cmd.Flags().StringVar(&detectFlags.Workspace, "workspace", "", "Location at which to mount the app dir in the build image") +} diff --git a/internal/commands/testmocks/mock_inspect_image_writer_factory.go b/internal/commands/testmocks/mock_inspect_image_writer_factory.go index 9f5a65c546..7b762b9949 100644 --- a/internal/commands/testmocks/mock_inspect_image_writer_factory.go +++ b/internal/commands/testmocks/mock_inspect_image_writer_factory.go @@ -48,4 +48,4 @@ func (m *MockInspectImageWriterFactory) Writer(arg0 string, arg1 bool) (writer.I func (mr *MockInspectImageWriterFactoryMockRecorder) Writer(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Writer", reflect.TypeOf((*MockInspectImageWriterFactory)(nil).Writer), arg0, arg1) -} +} \ No newline at end of file diff --git a/internal/commands/testmocks/mock_pack_client.go b/internal/commands/testmocks/mock_pack_client.go index f49b92def9..ef68a450d2 100644 --- a/internal/commands/testmocks/mock_pack_client.go +++ b/internal/commands/testmocks/mock_pack_client.go @@ -64,6 +64,20 @@ func (mr *MockPackClientMockRecorder) CreateBuilder(arg0, arg1 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBuilder", reflect.TypeOf((*MockPackClient)(nil).CreateBuilder), arg0, arg1) } +// 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() @@ -239,4 +253,4 @@ func (m *MockPackClient) YankBuildpack(arg0 client.YankBuildpackOptions) error { func (mr *MockPackClientMockRecorder) YankBuildpack(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "YankBuildpack", reflect.TypeOf((*MockPackClient)(nil).YankBuildpack), arg0) -} +} \ No newline at end of file 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/build.go b/pkg/client/build.go index 670f53f2c8..35684ab0a1 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -72,6 +72,7 @@ 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(ctx context.Context, opts build.LifecycleOptions) error } type IsTrustedBuilder func(string) bool @@ -209,6 +210,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 { @@ -288,284 +292,15 @@ 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) - if err != nil { - return 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 errors.Wrapf(err, "invalid layout paths image name '%s' or previous-image name '%s'", opts.LayoutConfig.InputImage.Name(), - opts.LayoutConfig.PreviousInputImage.Name()) - } - return errors.Wrapf(err, "invalid layout paths image name '%s'", opts.LayoutConfig.InputImage.Name()) - } - } - - appPath, err := c.processAppPath(opts.AppPath) - if err != nil { - return errors.Wrapf(err, "invalid app path '%s'", opts.AppPath) - } - - proxyConfig := c.processProxyConfig(opts.ProxyConfig) - - builderRef, err := c.processBuilderName(opts.Builder) - if err != nil { - return errors.Wrapf(err, "invalid builder '%s'", opts.Builder) - } - - rawBuilderImage, err := c.imageFetcher.Fetch(ctx, builderRef.Name(), image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy}) - if err != nil { - return errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name()) - } - - builderOS, err := rawBuilderImage.OS() - if err != nil { - return errors.Wrapf(err, "getting builder OS") - } - - builderArch, err := rawBuilderImage.Architecture() - if err != nil { - return errors.Wrapf(err, "getting builder architecture") - } - - bldr, err := c.getBuilder(rawBuilderImage) - if err != nil { - return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder)) - } - - runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, c.accessChecker) - - fetchOptions := image.FetchOptions{ - Daemon: !opts.Publish, - PullPolicy: opts.PullPolicy, - Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), - } - if opts.Layout() { - targetRunImagePath, err := layout.ParseRefToPath(runImageName) - if err != nil { - 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, - } - 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) - } - - var runMixins []string - if _, err := dist.GetLabel(runImage, stack.MixinsLabel, &runMixins); err != nil { - return err - } - - fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Image(), bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts) - if err != nil { - return err - } - - fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Image(), bldr.Extensions(), bldr.OrderExtensions(), bldr.StackID, opts) - if err != nil { - return err - } - - // Default mode: if the TrustBuilder option is not set, trust the suggested builders. - if opts.TrustBuilder == nil { - opts.TrustBuilder = IsTrustedBuilderFunc - } - - // 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, - Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), - }, - ) - if err != nil { - return fmt.Errorf("fetching lifecycle image: %w", err) - } - - 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.ImageRemoveOptions{Force: true}) - - if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 { - if !c.experimental { - return fmt.Errorf("experimental features must be enabled when builder contains image extensions") - } - if builderOS == "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(builderOS, 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) + lifecycleOpts, err := c.ResolveLifecycleOptions(ctx, opts) 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()) - } + defer c.docker.ImageRemove(context.Background(), lifecycleOpts.Builder.Name(), types.ImageRemoveOptions{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, fetchOptions) + runImage, err := c.imageFetcher.Fetch(ctx, runImageName, lifecycleOpts.FetchOptions) if err != nil { return "", err } @@ -697,10 +432,10 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { return ephemeralRunImageName, nil } - if err = c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil { + if err = c.lifecycleExecutor.Execute(ctx, *lifecycleOpts); err != nil { return fmt.Errorf("executing lifecycle: %w", err) } - return c.logImageNameAndSha(ctx, opts.Publish, imageRef) + return c.logImageNameAndSha(ctx, opts.Publish, lifecycleOpts.Image) } func extractSupportedLifecycleApis(labels map[string]string) ([]string, error) { @@ -1552,3 +1287,292 @@ func readOnlyVolume(hostPath, targetPath string) string { } return fmt.Sprintf("%s:%s", hostPath, tp) } + +func (c *Client) ResolveLifecycleOptions(ctx context.Context, opts BuildOptions) (*build.LifecycleOptions, error) { + var pathsConfig layoutPathConfig + imageVars := struct { + imageRef name.Reference + imgRegistry string + imageName string + }{} + + detectOnly := opts.DetectOnly + + if !detectOnly { + imageRef, err := c.parseReference(opts) + if err != nil { + return nil, errors.Wrapf(err, "invalid image name '%s'", opts.Image) + } + imageVars.imgRegistry = imageRef.Context().RegistryStr() + imageVars.imageName = imageRef.Name() + imageVars.imageRef = imageRef + + 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) + } + + rawBuilderImage, err := c.imageFetcher.Fetch(ctx, builderRef.Name(), image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy}) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch builder image '%s'", builderRef.Name()) + } + + builderOS, err := rawBuilderImage.OS() + if err != nil { + return nil, errors.Wrapf(err, "getting builder OS") + } + + builderArch, err := rawBuilderImage.Architecture() + if err != nil { + return nil, errors.Wrapf(err, "getting builder architecture") + } + + bldr, err := c.getBuilder(rawBuilderImage) + if err != nil { + return nil, errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder)) + } + + runImageName := c.resolveRunImage(opts.RunImage, imageVars.imgRegistry, builderRef.Context().RegistryStr(), bldr.DefaultRunImage(), opts.AdditionalMirrors, opts.Publish, c.accessChecker) + + fetchOptions := image.FetchOptions{ + Daemon: !opts.Publish, + PullPolicy: opts.PullPolicy, + Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), + } + 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 + } + + fetchedBPs, order, err := c.processBuildpacks(ctx, bldr.Image(), bldr.Buildpacks(), bldr.Order(), bldr.StackID, opts) + if err != nil { + return nil, err + } + + fetchedExs, orderExtensions, err := c.processExtensions(ctx, bldr.Image(), bldr.Extensions(), bldr.OrderExtensions(), bldr.StackID, opts) + if err != nil { + return nil, err + } + + // Default mode: if the TrustBuilder option is not set, trust the suggested builders. + if opts.TrustBuilder == nil { + opts.TrustBuilder = IsTrustedBuilderFunc + } + + // 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)) + } + + // 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, + Platform: fmt.Sprintf("%s/%s", builderOS, builderArch), + }, + ) + if err != nil { + return nil, fmt.Errorf("fetching lifecycle image: %w", err) + } + + 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) + } + } + } + + 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) + } + } + + 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 nil, err + } + + if len(bldr.OrderExtensions()) > 0 || len(ephemeralBuilder.OrderExtensions()) > 0 { + if !c.experimental { + return nil, fmt.Errorf("experimental features must be enabled when builder contains image extensions") + } + if builderOS == "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 opts.Layout() { + opts.ContainerConfig.Volumes = appendLayoutVolumes(opts.ContainerConfig.Volumes, pathsConfig) + } + + processedVolumes, warnings, err := processVolumes(builderOS, opts.ContainerConfig.Volumes) + if err != nil { + return nil, err + } + + for _, warning := range warnings { + c.logger.Warn(warning) + } + + fileFilter, err := getFileFilter(opts.ProjectDescriptor) + if err != nil { + return nil, err + } + + runImageName, err = pname.TranslateRegistry(runImageName, c.registryMirrors, c.logger) + if err != nil { + return nil, 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: imageVars.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(imageVars.imageName, ephemeralBuilder, runImageName), + ReportDestinationDir: opts.ReportDestinationDir, + SBOMDestinationDir: opts.SBOMDestinationDir, + CreationTime: opts.CreationTime, + Layout: opts.Layout(), + Keychain: c.keychain, + DetectOnly: detectOnly, + } + + 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 lifecycleOpts, nil +} diff --git a/pkg/client/detect.go b/pkg/client/detect.go new file mode 100644 index 0000000000..57ce532e3a --- /dev/null +++ b/pkg/client/detect.go @@ -0,0 +1,23 @@ +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.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 +} diff --git a/pkg/testmocks/mock_access_checker.go b/pkg/testmocks/mock_access_checker.go new file mode 100644 index 0000000000..558b85a580 --- /dev/null +++ b/pkg/testmocks/mock_access_checker.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/buildpacks/pack/pkg/client (interfaces: AccessChecker) + +// Package testmocks is a generated GoMock package. +package testmocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockAccessChecker is a mock of AccessChecker interface. +type MockAccessChecker struct { + ctrl *gomock.Controller + recorder *MockAccessCheckerMockRecorder +} + +// MockAccessCheckerMockRecorder is the mock recorder for MockAccessChecker. +type MockAccessCheckerMockRecorder struct { + mock *MockAccessChecker +} + +// NewMockAccessChecker creates a new mock instance. +func NewMockAccessChecker(ctrl *gomock.Controller) *MockAccessChecker { + mock := &MockAccessChecker{ctrl: ctrl} + mock.recorder = &MockAccessCheckerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccessChecker) EXPECT() *MockAccessCheckerMockRecorder { + return m.recorder +} + +// Check mocks base method. +func (m *MockAccessChecker) Check(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Check", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Check indicates an expected call of Check. +func (mr *MockAccessCheckerMockRecorder) Check(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockAccessChecker)(nil).Check), arg0) +} diff --git a/pkg/testmocks/mock_blob_downloader.go b/pkg/testmocks/mock_blob_downloader.go index 80a04d9878..9a044edd21 100644 --- a/pkg/testmocks/mock_blob_downloader.go +++ b/pkg/testmocks/mock_blob_downloader.go @@ -8,9 +8,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - blob "github.com/buildpacks/pack/pkg/blob" + gomock "github.com/golang/mock/gomock" ) // MockBlobDownloader is a mock of BlobDownloader interface. diff --git a/pkg/testmocks/mock_build_module.go b/pkg/testmocks/mock_build_module.go index 5548ad6f6f..35045ea9f5 100644 --- a/pkg/testmocks/mock_build_module.go +++ b/pkg/testmocks/mock_build_module.go @@ -8,9 +8,8 @@ import ( io "io" reflect "reflect" - gomock "github.com/golang/mock/gomock" - buildpack "github.com/buildpacks/pack/pkg/buildpack" + gomock "github.com/golang/mock/gomock" ) // MockBuildModule is a mock of BuildModule interface. diff --git a/pkg/testmocks/mock_buildpack_downloader.go b/pkg/testmocks/mock_buildpack_downloader.go index d67ff548d2..9758f24b2c 100644 --- a/pkg/testmocks/mock_buildpack_downloader.go +++ b/pkg/testmocks/mock_buildpack_downloader.go @@ -8,9 +8,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - buildpack "github.com/buildpacks/pack/pkg/buildpack" + gomock "github.com/golang/mock/gomock" ) // MockBuildpackDownloader is a mock of BuildpackDownloader interface. diff --git a/pkg/testmocks/mock_image_fetcher.go b/pkg/testmocks/mock_image_fetcher.go index 281f28d04d..df0e08ff7f 100644 --- a/pkg/testmocks/mock_image_fetcher.go +++ b/pkg/testmocks/mock_image_fetcher.go @@ -9,9 +9,8 @@ import ( reflect "reflect" imgutil "github.com/buildpacks/imgutil" - gomock "github.com/golang/mock/gomock" - image "github.com/buildpacks/pack/pkg/image" + gomock "github.com/golang/mock/gomock" ) // MockImageFetcher is a mock of ImageFetcher interface.