Skip to content

Commit

Permalink
feat: Adding targets and path flags when packaging an extension
Browse files Browse the repository at this point in the history
  • Loading branch information
pacostas committed Jan 15, 2025
1 parent 48d9128 commit dcf618a
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 17 deletions.
71 changes: 68 additions & 3 deletions internal/commands/extension_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"context"
"os"
"path/filepath"

"github.com/pkg/errors"
Expand All @@ -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"
)
Expand All @@ -19,8 +21,10 @@ import (
type ExtensionPackageFlags struct {
PackageTomlPath string
Format string
Targets []string
Publish bool
Policy string
Path string
}

// ExtensionPackager packages extensions
Expand All @@ -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",

Check failure on line 42 in internal/commands/extension_package.go

View workflow job for this annotation

GitHub Actions / test (macos)

`registy` is a misspelling of `registry` (misspell)

Check failure on line 42 in internal/commands/extension_package.go

View workflow job for this annotation

GitHub Actions / test (linux)

`registy` is a misspelling of `registry` (misspell)

Check failure on line 42 in internal/commands/extension_package.go

View workflow job for this annotation

GitHub Actions / test (windows-lcow)

`registy` is a misspelling of `registry` (misspell)

Check failure on line 42 in internal/commands/extension_package.go

View workflow job for this annotation

GitHub Actions / test (windows-wcow)

`registy` is a misspelling of `registry` (misspell)
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
Expand All @@ -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)
Expand All @@ -74,13 +91,36 @@ 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,
Format: flags.Format,
Config: exPackageCfg,
Publish: flags.Publish,
PullPolicy: pullPolicy,
Targets: multiArchCfg.Targets(),
}); err != nil {
return err
}
Expand All @@ -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:[email protected]"'
- To specify multiple distribution versions: '--target "linux/arm/v6:[email protected]" --target "linux/arm/v6:[email protected]"'
`)
AddHelpFlag(cmd, "package")
return cmd
}
Expand All @@ -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
}
63 changes: 63 additions & 0 deletions internal/commands/extension_package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands_test
import (
"bytes"
"fmt"
"path/filepath"
"testing"

"github.com/heroku/color"
Expand Down Expand Up @@ -192,6 +193,47 @@ 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.Buildpack.URI, bpPath)
})
})

when("multi-platform", func() {
var (
targets []dist.Target
descriptor dist.ExtensionDescriptor
path string
)

when("single extension", func() {
it.Before(func() {
targets = []dist.Target{
{OS: "linux", Arch: "amd64"},
{OS: "windows", Arch: "amd64"},
}

descriptor = dist.ExtensionDescriptor{WithTargets: targets}
path = "testdata"
})

it("creates a multi-platform extension package", func() {
cmd := packageCommand(withBuildpackPackager(fakeExtensionPackager), withPackageConfigReader(fakes.NewFakePackageConfigReader(whereReadExtensionDescriptor(descriptor, nil))))
cmd.SetArgs([]string{"some-name", "-p", path})

h.AssertNil(t, cmd.Execute())
h.AssertEq(t, fakeExtensionPackager.CreateCalledWithOptions.Targets, targets)
})
})
})
})
})

when("invalid flags", func() {
Expand Down Expand Up @@ -249,6 +291,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'")
})
})
})
}

Expand Down Expand Up @@ -318,3 +374,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
}
}
1 change: 1 addition & 0 deletions internal/commands/fakes/fake_package_config_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type FakePackageConfigReader struct {

ReadBuildpackDescriptorCalledWithArg string
ReadBuildpackDescriptorReturn dist.BuildpackDescriptor
ReadExtensionDescriptorReturn dist.ExtensionDescriptor
ReadBuildpackDescriptorReturnError error
}

Expand Down
87 changes: 73 additions & 14 deletions pkg/client/package_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package client

import (
"context"
"fmt"
"path/filepath"

"github.com/pkg/errors"

Expand All @@ -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.")
targets, err := c.processPackageBuildpackTargets(ctx, opts)
if err != nil {
return err
}
multiArch := len(targets) > 1 && (opts.Publish || opts.Format == FormatFile)

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, opts.Config.Platform.OS, opts.Publish, opts.Format)
err := c.validateOSPlatform(ctx, target.OS, opts.Publish, opts.Format)
if err != nil {
return err
return digest, err
}

writerFactory, err := layer.NewWriterFactory(opts.Config.Platform.OS)
writerFactory, err := layer.NewWriterFactory(target.OS)
if err != nil {
return errors.Wrap(err, "creating layer writer factory")
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
}

0 comments on commit dcf618a

Please sign in to comment.