github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/commands/buildpack_package.go (about)

     1  package commands
     2  
     3  import (
     4  	"context"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/pkg/errors"
     9  	"github.com/spf13/cobra"
    10  
    11  	pubbldpkg "github.com/buildpacks/pack/buildpackage"
    12  	"github.com/buildpacks/pack/internal/config"
    13  	"github.com/buildpacks/pack/internal/style"
    14  	"github.com/buildpacks/pack/pkg/client"
    15  	"github.com/buildpacks/pack/pkg/image"
    16  	"github.com/buildpacks/pack/pkg/logging"
    17  )
    18  
    19  // BuildpackPackageFlags define flags provided to the BuildpackPackage command
    20  type BuildpackPackageFlags struct {
    21  	PackageTomlPath   string
    22  	Format            string
    23  	Policy            string
    24  	BuildpackRegistry string
    25  	Path              string
    26  	FlattenExclude    []string
    27  	Label             map[string]string
    28  	Publish           bool
    29  	Flatten           bool
    30  }
    31  
    32  // BuildpackPackager packages buildpacks
    33  type BuildpackPackager interface {
    34  	PackageBuildpack(ctx context.Context, options client.PackageBuildpackOptions) error
    35  }
    36  
    37  // PackageConfigReader reads BuildpackPackage configs
    38  type PackageConfigReader interface {
    39  	Read(path string) (pubbldpkg.Config, error)
    40  }
    41  
    42  // BuildpackPackage packages (a) buildpack(s) into OCI format, based on a package config
    43  func BuildpackPackage(logger logging.Logger, cfg config.Config, packager BuildpackPackager, packageConfigReader PackageConfigReader) *cobra.Command {
    44  	var flags BuildpackPackageFlags
    45  	cmd := &cobra.Command{
    46  		Use:     "package <name> --config <config-path>",
    47  		Short:   "Package a buildpack in OCI format.",
    48  		Args:    cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
    49  		Example: "pack buildpack package my-buildpack --config ./package.toml\npack buildpack package my-buildpack.cnb --config ./package.toml --f file",
    50  		Long: "buildpack package allows users to package (a) buildpack(s) into OCI format, which can then to be hosted in " +
    51  			"image repositories or persisted on disk as a '.cnb' file. You can also package a number of buildpacks " +
    52  			"together, to enable easier distribution of a set of buildpacks. " +
    53  			"Packaged buildpacks can be used as inputs to `pack build` (using the `--buildpack` flag), " +
    54  			"and they can be included in the configs used in `pack builder create` and `pack buildpack package`. For more " +
    55  			"on how to package a buildpack, see: https://buildpacks.io/docs/buildpack-author-guide/package-a-buildpack/.",
    56  		RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
    57  			if err := validateBuildpackPackageFlags(cfg, &flags); err != nil {
    58  				return err
    59  			}
    60  
    61  			stringPolicy := flags.Policy
    62  			if stringPolicy == "" {
    63  				stringPolicy = cfg.PullPolicy
    64  			}
    65  			pullPolicy, err := image.ParsePullPolicy(stringPolicy)
    66  			if err != nil {
    67  				return errors.Wrap(err, "parsing pull policy")
    68  			}
    69  			bpPackageCfg := pubbldpkg.DefaultConfig()
    70  			var bpPath string
    71  			if flags.Path != "" {
    72  				if bpPath, err = filepath.Abs(flags.Path); err != nil {
    73  					return errors.Wrap(err, "resolving buildpack path")
    74  				}
    75  				bpPackageCfg.Buildpack.URI = bpPath
    76  			}
    77  			relativeBaseDir := ""
    78  			if flags.PackageTomlPath != "" {
    79  				bpPackageCfg, err = packageConfigReader.Read(flags.PackageTomlPath)
    80  				if err != nil {
    81  					return errors.Wrap(err, "reading config")
    82  				}
    83  
    84  				relativeBaseDir, err = filepath.Abs(filepath.Dir(flags.PackageTomlPath))
    85  				if err != nil {
    86  					return errors.Wrap(err, "getting absolute path for config")
    87  				}
    88  			}
    89  			name := args[0]
    90  			if flags.Format == client.FormatFile {
    91  				switch ext := filepath.Ext(name); ext {
    92  				case client.CNBExtension:
    93  				case "":
    94  					name += client.CNBExtension
    95  				default:
    96  					logger.Warnf("%s is not a valid extension for a packaged buildpack. Packaged buildpacks must have a %s extension", style.Symbol(ext), style.Symbol(client.CNBExtension))
    97  				}
    98  			}
    99  			if flags.Flatten {
   100  				logger.Warn("Flattening a buildpack package could break the distribution specification. Please use it with caution.")
   101  			}
   102  
   103  			if err := packager.PackageBuildpack(cmd.Context(), client.PackageBuildpackOptions{
   104  				RelativeBaseDir: relativeBaseDir,
   105  				Name:            name,
   106  				Format:          flags.Format,
   107  				Config:          bpPackageCfg,
   108  				Publish:         flags.Publish,
   109  				PullPolicy:      pullPolicy,
   110  				Registry:        flags.BuildpackRegistry,
   111  				Flatten:         flags.Flatten,
   112  				FlattenExclude:  flags.FlattenExclude,
   113  				Labels:          flags.Label,
   114  			}); err != nil {
   115  				return err
   116  			}
   117  
   118  			action := "created"
   119  			location := "docker daemon"
   120  			if flags.Publish {
   121  				action = "published"
   122  				location = "registry"
   123  			}
   124  			if flags.Format == client.FormatFile {
   125  				location = "file"
   126  			}
   127  			logger.Infof("Successfully %s package %s and saved to %s", action, style.Symbol(name), location)
   128  			return nil
   129  		}),
   130  	}
   131  
   132  	cmd.Flags().StringVarP(&flags.PackageTomlPath, "config", "c", "", "Path to package TOML config")
   133  	cmd.Flags().StringVarP(&flags.Format, "format", "f", "", `Format to save package as ("image" or "file")`)
   134  	cmd.Flags().BoolVar(&flags.Publish, "publish", false, `Publish the buildpack directly to the container registry specified in <name>, instead of the daemon (applies to "--format=image" only).`)
   135  	cmd.Flags().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always")
   136  	cmd.Flags().StringVarP(&flags.Path, "path", "p", "", "Path to the Buildpack that needs to be packaged")
   137  	cmd.Flags().StringVarP(&flags.BuildpackRegistry, "buildpack-registry", "r", "", "Buildpack Registry name")
   138  	cmd.Flags().BoolVar(&flags.Flatten, "flatten", false, "Flatten the buildpack into a single layer")
   139  	cmd.Flags().StringSliceVarP(&flags.FlattenExclude, "flatten-exclude", "e", nil, "Buildpacks to exclude from flattening, in the form of '<buildpack-id>@<buildpack-version>'")
   140  	cmd.Flags().StringToStringVarP(&flags.Label, "label", "l", nil, "Labels to add to packaged Buildpack, in the form of '<name>=<value>'")
   141  	if !cfg.Experimental {
   142  		cmd.Flags().MarkHidden("flatten")
   143  		cmd.Flags().MarkHidden("flatten-exclude")
   144  	}
   145  	AddHelpFlag(cmd, "package")
   146  	return cmd
   147  }
   148  
   149  func validateBuildpackPackageFlags(cfg config.Config, p *BuildpackPackageFlags) error {
   150  	if p.Publish && p.Policy == image.PullNever.String() {
   151  		return errors.Errorf("--publish and --pull-policy never cannot be used together. The --publish flag requires the use of remote images.")
   152  	}
   153  	if p.PackageTomlPath != "" && p.Path != "" {
   154  		return errors.Errorf("--config and --path cannot be used together. Please specify the relative path to the Buildpack directory in the package config file.")
   155  	}
   156  
   157  	if p.Flatten {
   158  		if !cfg.Experimental {
   159  			return client.NewExperimentError("Flattening a buildpack package is currently experimental.")
   160  		}
   161  
   162  		if len(p.FlattenExclude) > 0 {
   163  			for _, exclude := range p.FlattenExclude {
   164  				if strings.Count(exclude, "@") != 1 {
   165  					return errors.Errorf("invalid format %s; please use '<buildpack-id>@<buildpack-version>' to exclude buildpack from flattening", exclude)
   166  				}
   167  			}
   168  		}
   169  	}
   170  	return nil
   171  }