Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of multi-arch support for extension packages #2320

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/commands/buildpack_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,13 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa

if len(multiArchCfg.Targets()) == 0 {
if isCompositeBP {
logger.Infof("Pro tip: use --targets flag OR [[targets]] in package.toml to specify the desired platform (os/arch/variant); using os %s", style.Symbol(bpPackageCfg.Platform.OS))
logger.Infof("Pro tip: use --target flag OR [[targets]] in package.toml to specify the desired platform (os/arch/variant); using os %s", style.Symbol(bpPackageCfg.Platform.OS))
} else {
logger.Infof("Pro tip: use --targets flag OR [[targets]] in buildpack.toml to specify the desired platform (os/arch/variant); using os %s", style.Symbol(bpPackageCfg.Platform.OS))
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(bpPackageCfg.Platform.OS))
}
} else if !isCompositeBP {
// FIXME: Check if we can copy the config files during layers creation.
filesToClean, err := multiArchCfg.CopyConfigFiles(bpPath)
filesToClean, err := multiArchCfg.CopyConfigFiles(bpPath, "buildpack")
if err != nil {
return err
}
Expand Down
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\npack extension package registry/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
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
}
48 changes: 48 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,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() {
Expand Down Expand Up @@ -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'")
})
})
})
}

Expand Down
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
21 changes: 17 additions & 4 deletions pkg/buildpack/multi_architecture_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ func (m *MultiArchConfig) Targets() []dist.Target {
// CopyConfigFiles will, given a base directory (which is expected to be the root folder of a single buildpack),
// copy the buildpack.toml file from the base directory into the corresponding platform root folder for each target.
// It will return an array with all the platform root folders where the buildpack.toml file was copied.
func (m *MultiArchConfig) CopyConfigFiles(baseDir string) ([]string, error) {
func (m *MultiArchConfig) CopyConfigFiles(baseDir string, buildpackType string) ([]string, error) {
var filesToClean []string
if buildpackType == "" {
buildpackType = "buildpack"
}
targets := dist.ExpandTargetsDistributions(m.Targets()...)
for _, target := range targets {
path, err := CopyConfigFile(baseDir, target)
path, err := CopyConfigFile(baseDir, target, buildpackType)
if err != nil {
return nil, err
}
Expand All @@ -56,9 +59,16 @@ func (m *MultiArchConfig) CopyConfigFiles(baseDir string) ([]string, error) {

// CopyConfigFile will copy the buildpack.toml file from the base directory into the corresponding platform folder
// for the specified target and desired distribution version.
func CopyConfigFile(baseDir string, target dist.Target) (string, error) {
func CopyConfigFile(baseDir string, target dist.Target, buildpackType string) (string, error) {
var path string
var err error

if ok, platformRootFolder := PlatformRootFolder(baseDir, target); ok {
path, err := copyBuildpackTOML(baseDir, platformRootFolder)
if buildpackType == "extension" {
path, err = copyExtensionTOML(baseDir, platformRootFolder)
} else {
path, err = copyBuildpackTOML(baseDir, platformRootFolder)
}
if err != nil {
return "", err
}
Expand Down Expand Up @@ -120,6 +130,9 @@ func copyBuildpackTOML(src string, dest string) (string, error) {
return copyFile(src, dest, "buildpack.toml")
}

func copyExtensionTOML(src string, dest string) (string, error) {
return copyFile(src, dest, "extension.toml")
}
func copyFile(src, dest, fileName string) (string, error) {
filePath := filepath.Join(dest, fileName)
fileToCopy, err := os.Create(filePath)
Expand Down
28 changes: 27 additions & 1 deletion pkg/buildpack/multi_architecture_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func testMultiArchConfig(t *testing.T, when spec.G, it spec.S) {
logger *logging.LogWithWriters
multiArchConfig *buildpack.MultiArchConfig
targetsFromBuildpack []dist.Target
targetsFromExtension []dist.Target
targetsFromFlags []dist.Target
tmpDir string
)
Expand Down Expand Up @@ -109,13 +110,38 @@ func testMultiArchConfig(t *testing.T, when spec.G, it spec.S) {
})

it("copies the buildpack.toml to each target platform folder", func() {
paths, err := multiArchConfig.CopyConfigFiles(rootFolder)
paths, err := multiArchConfig.CopyConfigFiles(rootFolder, "buildpack")
h.AssertNil(t, err)
h.AssertEq(t, len(paths), 2)
h.AssertPathExists(t, filepath.Join(rootFolder, "linux", "amd64", "buildpack.toml"))
h.AssertPathExists(t, filepath.Join(rootFolder, "linux", "arm64", "v8", "buildpack.toml"))
})
})

when("extension root folder exists", func() {
var rootFolder string

it.Before(func() {
rootFolder = filepath.Join(tmpDir, "some-extension")
targetsFromExtension = []dist.Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "arm64", ArchVariant: "v8"}}
multiArchConfig, err = buildpack.NewMultiArchConfig(targetsFromExtension, []dist.Target{}, logger)
h.AssertNil(t, err)

// dummy multi-platform extension structure
os.MkdirAll(filepath.Join(rootFolder, "linux", "amd64"), 0755)
os.MkdirAll(filepath.Join(rootFolder, "linux", "arm64", "v8"), 0755)
_, err = os.Create(filepath.Join(rootFolder, "extension.toml"))
h.AssertNil(t, err)
})

it("copies the extension.toml to each target platform folder", func() {
paths, err := multiArchConfig.CopyConfigFiles(rootFolder, "extension")
h.AssertNil(t, err)
h.AssertEq(t, len(paths), 2)
h.AssertPathExists(t, filepath.Join(rootFolder, "linux", "amd64", "extension.toml"))
h.AssertPathExists(t, filepath.Join(rootFolder, "linux", "arm64", "v8", "extension.toml"))
})
})
})

when("#PlatformRootFolder", func() {
Expand Down
Loading
Loading