Skip to content

Commit

Permalink
Implement pack detect command
Browse files Browse the repository at this point in the history
Signed-off-by: Rashad Sirajudeen <[email protected]>
  • Loading branch information
rashadism committed Apr 22, 2024
1 parent 12b7d24 commit 51f0978
Show file tree
Hide file tree
Showing 16 changed files with 629 additions and 289 deletions.
1 change: 1 addition & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
38 changes: 35 additions & 3 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand All @@ -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()))
Expand All @@ -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")
}
Expand Down
18 changes: 18 additions & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
3 changes: 1 addition & 2 deletions internal/builder/testmocks/mock_lifecycle.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
179 changes: 179 additions & 0 deletions internal/commands/detect.go
Original file line number Diff line number Diff line change
@@ -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 '<buildpack>@<version>',\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 '<hostname>/<repo>[:<tag>]'"+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 '<extension>@<version>',\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 '<hostname>/<repo>[:<tag>]'"+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 '<host path>:<target path>[:<options>]'.\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=<key>=<value>\", 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 <image-name>, 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")
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion internal/commands/testmocks/mock_pack_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions internal/fakes/fake_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit 51f0978

Please sign in to comment.