mirror of https://github.com/buildpacks/pack.git
feat: Adding targets and path flags when packaging an extension
Signed-off-by: Costas Papastathis <papastathiscr@gmail.com>
This commit is contained in:
parent
9caef9000e
commit
8ca5ea9321
|
|
@ -2,6 +2,7 @@ package commands
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"github.com/buildpacks/pack/internal/config"
|
||||
"github.com/buildpacks/pack/internal/style"
|
||||
"github.com/buildpacks/pack/pkg/client"
|
||||
"github.com/buildpacks/pack/pkg/dist"
|
||||
"github.com/buildpacks/pack/pkg/image"
|
||||
"github.com/buildpacks/pack/pkg/logging"
|
||||
)
|
||||
|
|
@ -19,8 +21,10 @@ import (
|
|||
type ExtensionPackageFlags struct {
|
||||
PackageTomlPath string
|
||||
Format string
|
||||
Targets []string
|
||||
Publish bool
|
||||
Policy string
|
||||
Path string
|
||||
}
|
||||
|
||||
// ExtensionPackager packages extensions
|
||||
|
|
@ -32,9 +36,15 @@ type ExtensionPackager interface {
|
|||
func ExtensionPackage(logger logging.Logger, cfg config.Config, packager ExtensionPackager, packageConfigReader PackageConfigReader) *cobra.Command {
|
||||
var flags ExtensionPackageFlags
|
||||
cmd := &cobra.Command{
|
||||
Use: "package <name> --config <config-path>",
|
||||
Short: "Package an extension in OCI format",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
Use: "package <name> --config <config-path>",
|
||||
Short: "Package an extension in OCI format",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
Example: "pack extension package /output/file.cnb --path /extracted/from/tgz/folder --format file\n pack extension package registy/image-name --path /extracted/from/tgz/folder --format image --publish",
|
||||
Long: "extension package allows users to package (an) extension(s) into OCI format, which can then to be hosted in " +
|
||||
"image repositories or persisted on disk as a '.cnb' file." +
|
||||
"Packaged extensions can be used as inputs to `pack build` (using the `--extension` flag), " +
|
||||
"and they can be included in the configs used in `pack builder create` and `pack extension package`. For more " +
|
||||
"on how to package an extension, see: https://buildpacks.io/docs/buildpack-author-guide/package-a-buildpack/.",
|
||||
RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateExtensionPackageFlags(&flags); err != nil {
|
||||
return err
|
||||
|
|
@ -51,6 +61,13 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi
|
|||
}
|
||||
|
||||
exPackageCfg := pubbldpkg.DefaultExtensionConfig()
|
||||
var exPath string
|
||||
if flags.Path != "" {
|
||||
if exPath, err = filepath.Abs(flags.Path); err != nil {
|
||||
return errors.Wrap(err, "resolving extension path")
|
||||
}
|
||||
exPackageCfg.Extension.URI = exPath
|
||||
}
|
||||
relativeBaseDir := ""
|
||||
if flags.PackageTomlPath != "" {
|
||||
exPackageCfg, err = packageConfigReader.Read(flags.PackageTomlPath)
|
||||
|
|
@ -74,6 +91,28 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi
|
|||
}
|
||||
}
|
||||
|
||||
targets, err := processExtensionPackageTargets(flags.Path, packageConfigReader, exPackageCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
daemon := !flags.Publish && flags.Format == ""
|
||||
multiArchCfg, err := processMultiArchitectureConfig(logger, flags.Targets, targets, daemon)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(multiArchCfg.Targets()) == 0 {
|
||||
logger.Infof("Pro tip: use --target flag OR [[targets]] in buildpack.toml to specify the desired platform (os/arch/variant); using os %s", style.Symbol(exPackageCfg.Platform.OS))
|
||||
} else {
|
||||
// FIXME: Check if we can copy the config files during layers creation.
|
||||
filesToClean, err := multiArchCfg.CopyConfigFiles(exPath, "extension")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clean(filesToClean)
|
||||
}
|
||||
|
||||
if err := packager.PackageExtension(cmd.Context(), client.PackageBuildpackOptions{
|
||||
RelativeBaseDir: relativeBaseDir,
|
||||
Name: name,
|
||||
|
|
@ -81,6 +120,7 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi
|
|||
Config: exPackageCfg,
|
||||
Publish: flags.Publish,
|
||||
PullPolicy: pullPolicy,
|
||||
Targets: multiArchCfg.Targets(),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -104,6 +144,14 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi
|
|||
cmd.Flags().StringVarP(&flags.Format, "format", "f", "", `Format to save package as ("image" or "file")`)
|
||||
cmd.Flags().BoolVar(&flags.Publish, "publish", false, `Publish the extension directly to the container registry specified in <name>, instead of the daemon (applies to "--format=image" only).`)
|
||||
cmd.Flags().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always")
|
||||
cmd.Flags().StringVarP(&flags.Path, "path", "p", "", "Path to the Extension that needs to be packaged")
|
||||
cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil,
|
||||
`Target platforms to build for.
|
||||
Targets should be in the format '[os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion]'.
|
||||
- To specify two different architectures: '--target "linux/amd64" --target "linux/arm64"'
|
||||
- To specify the distribution version: '--target "linux/arm/v6:ubuntu@14.04"'
|
||||
- To specify multiple distribution versions: '--target "linux/arm/v6:ubuntu@14.04" --target "linux/arm/v6:ubuntu@16.04"'
|
||||
`)
|
||||
AddHelpFlag(cmd, "package")
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -114,3 +162,20 @@ func validateExtensionPackageFlags(p *ExtensionPackageFlags) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processExtensionPackageTargets returns the list of targets defined on the extension.toml
|
||||
func processExtensionPackageTargets(path string, packageConfigReader PackageConfigReader, bpPackageCfg pubbldpkg.Config) ([]dist.Target, error) {
|
||||
var targets []dist.Target
|
||||
|
||||
// Read targets from buildpack.toml
|
||||
pathToExtensionToml := filepath.Join(path, "extension.toml")
|
||||
if _, err := os.Stat(pathToExtensionToml); err == nil {
|
||||
buildpackCfg, err := packageConfigReader.ReadBuildpackDescriptor(pathToExtensionToml)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets = buildpackCfg.Targets()
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package commands_test
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/heroku/color"
|
||||
|
|
@ -192,6 +193,39 @@ func testExtensionPackageCommand(t *testing.T, when spec.G, it spec.S) {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("a path is specified", func() {
|
||||
when("no multi-platform", func() {
|
||||
it("creates a default config with the appropriate path", func() {
|
||||
cmd := packageExtensionCommand(withExtensionPackager(fakeExtensionPackager))
|
||||
cmd.SetArgs([]string{"some-name", "-p", ".."})
|
||||
h.AssertNil(t, cmd.Execute())
|
||||
bpPath, _ := filepath.Abs("..")
|
||||
receivedOptions := fakeExtensionPackager.CreateCalledWithOptions
|
||||
h.AssertEq(t, receivedOptions.Config.Extension.URI, bpPath)
|
||||
})
|
||||
})
|
||||
|
||||
when("multi-platform", func() {
|
||||
var targets []dist.Target
|
||||
|
||||
when("single extension", func() {
|
||||
it.Before(func() {
|
||||
targets = []dist.Target{
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
{OS: "windows", Arch: "amd64"},
|
||||
}
|
||||
})
|
||||
|
||||
it("creates a multi-platform extension package", func() {
|
||||
cmd := packageExtensionCommand(withExtensionPackager(fakeExtensionPackager))
|
||||
cmd.SetArgs([]string{"some-name", "-p", "some-path", "--target", "linux/amd64", "--target", "windows/amd64", "--format", "image", "--publish"})
|
||||
h.AssertNil(t, cmd.Execute())
|
||||
h.AssertEq(t, fakeExtensionPackager.CreateCalledWithOptions.Targets, targets)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
when("invalid flags", func() {
|
||||
|
|
@ -249,6 +283,20 @@ func testExtensionPackageCommand(t *testing.T, when spec.G, it spec.S) {
|
|||
h.AssertError(t, cmd.Execute(), "parsing pull policy")
|
||||
})
|
||||
})
|
||||
|
||||
when("--target cannot be parsed", func() {
|
||||
it("errors with a descriptive message", func() {
|
||||
cmd := packageCommand()
|
||||
cmd.SetArgs([]string{
|
||||
"some-image-name", "--config", "/path/to/some/file",
|
||||
"--target", "something/wrong", "--publish",
|
||||
})
|
||||
|
||||
err := cmd.Execute()
|
||||
h.AssertNotNil(t, err)
|
||||
h.AssertError(t, err, "unknown target: 'something/wrong'")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -318,3 +366,10 @@ func withExtensionClientConfig(clientCfg config.Config) packageExtensionCommandO
|
|||
config.clientConfig = clientCfg
|
||||
}
|
||||
}
|
||||
|
||||
func whereReadExtensionDescriptor(descriptor dist.ExtensionDescriptor, err error) func(*fakes.FakePackageConfigReader) {
|
||||
return func(r *fakes.FakePackageConfigReader) {
|
||||
r.ReadExtensionDescriptorReturn = descriptor
|
||||
r.ReadBuildpackDescriptorReturnError = err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type FakePackageConfigReader struct {
|
|||
|
||||
ReadBuildpackDescriptorCalledWithArg string
|
||||
ReadBuildpackDescriptorReturn dist.BuildpackDescriptor
|
||||
ReadExtensionDescriptorReturn dist.ExtensionDescriptor
|
||||
ReadBuildpackDescriptorReturnError error
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
|
|
@ -17,47 +19,104 @@ func (c *Client) PackageExtension(ctx context.Context, opts PackageBuildpackOpti
|
|||
opts.Format = FormatImage
|
||||
}
|
||||
|
||||
if opts.Config.Platform.OS == "windows" && !c.experimental {
|
||||
return NewExperimentError("Windows extensionpackage support is currently experimental.")
|
||||
}
|
||||
|
||||
err := c.validateOSPlatform(ctx, opts.Config.Platform.OS, opts.Publish, opts.Format)
|
||||
targets, err := c.processPackageBuildpackTargets(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
multiArch := len(targets) > 1 && (opts.Publish || opts.Format == FormatFile)
|
||||
|
||||
writerFactory, err := layer.NewWriterFactory(opts.Config.Platform.OS)
|
||||
var digests []string
|
||||
targets = dist.ExpandTargetsDistributions(targets...)
|
||||
for _, target := range targets {
|
||||
digest, err := c.packageExtensionTarget(ctx, opts, target, multiArch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
digests = append(digests, digest)
|
||||
}
|
||||
|
||||
if opts.Publish && len(digests) > 1 {
|
||||
// Image Index must be created only when we pushed to registry
|
||||
return c.CreateManifest(ctx, CreateManifestOptions{
|
||||
IndexRepoName: opts.Name,
|
||||
RepoNames: digests,
|
||||
Publish: true,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) packageExtensionTarget(ctx context.Context, opts PackageBuildpackOptions, target dist.Target, multiArch bool) (string, error) {
|
||||
var digest string
|
||||
if target.OS == "windows" && !c.experimental {
|
||||
return "", NewExperimentError("Windows extensionpackage support is currently experimental.")
|
||||
}
|
||||
|
||||
err := c.validateOSPlatform(ctx, target.OS, opts.Publish, opts.Format)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating layer writer factory")
|
||||
return digest, err
|
||||
}
|
||||
|
||||
writerFactory, err := layer.NewWriterFactory(target.OS)
|
||||
if err != nil {
|
||||
return digest, errors.Wrap(err, "creating layer writer factory")
|
||||
}
|
||||
|
||||
packageBuilder := buildpack.NewBuilder(c.imageFactory)
|
||||
|
||||
exURI := opts.Config.Extension.URI
|
||||
if exURI == "" {
|
||||
return errors.New("extension URI must be provided")
|
||||
return digest, errors.New("extension URI must be provided")
|
||||
}
|
||||
|
||||
if ok, platformRootFolder := buildpack.PlatformRootFolder(exURI, target); ok {
|
||||
exURI = platformRootFolder
|
||||
}
|
||||
|
||||
mainBlob, err := c.downloadBuildpackFromURI(ctx, exURI, opts.RelativeBaseDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return digest, err
|
||||
}
|
||||
|
||||
ex, err := buildpack.FromExtensionRootBlob(mainBlob, writerFactory, c.logger)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "creating extension from %s", style.Symbol(exURI))
|
||||
return digest, errors.Wrapf(err, "creating extension from %s", style.Symbol(exURI))
|
||||
}
|
||||
|
||||
packageBuilder.SetExtension(ex)
|
||||
|
||||
target := dist.Target{OS: opts.Config.Platform.OS}
|
||||
switch opts.Format {
|
||||
case FormatFile:
|
||||
return packageBuilder.SaveAsFile(opts.Name, target, map[string]string{})
|
||||
name := opts.Name
|
||||
if multiArch {
|
||||
fileExtension := filepath.Ext(name)
|
||||
origFileName := name[:len(name)-len(filepath.Ext(name))]
|
||||
if target.Arch != "" {
|
||||
name = fmt.Sprintf("%s-%s-%s%s", origFileName, target.OS, target.Arch, fileExtension)
|
||||
} else {
|
||||
name = fmt.Sprintf("%s-%s%s", origFileName, target.OS, fileExtension)
|
||||
}
|
||||
}
|
||||
err = packageBuilder.SaveAsFile(name, target, opts.Labels)
|
||||
if err != nil {
|
||||
return digest, err
|
||||
}
|
||||
case FormatImage:
|
||||
_, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, target, map[string]string{})
|
||||
return errors.Wrapf(err, "saving image")
|
||||
img, err := packageBuilder.SaveAsImage(opts.Name, opts.Publish, target, opts.Labels)
|
||||
if err != nil {
|
||||
return digest, errors.Wrapf(err, "saving image")
|
||||
}
|
||||
if multiArch {
|
||||
// We need to keep the identifier to create the image index
|
||||
id, err := img.Identifier()
|
||||
if err != nil {
|
||||
return digest, errors.Wrapf(err, "determining image manifest digest")
|
||||
}
|
||||
digest = id.String()
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("unknown format: %s", style.Symbol(opts.Format))
|
||||
return digest, errors.Errorf("unknown format: %s", style.Symbol(opts.Format))
|
||||
}
|
||||
return digest, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue