Skip to content

Commit

Permalink
Merge pull request #1877 from colincasey/add_image_labels_to_buildpack
Browse files Browse the repository at this point in the history
Add custom label metadata to packaged buildpacks
  • Loading branch information
jkutner authored Sep 6, 2023
2 parents 2f00a05 + 9c328bd commit 6329e89
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 23 deletions.
3 changes: 3 additions & 0 deletions internal/commands/buildpack_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type BuildpackPackageFlags struct {
BuildpackRegistry string
Path string
FlattenExclude []string
Label map[string]string
Publish bool
Flatten bool
Depth int
Expand Down Expand Up @@ -111,6 +112,7 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa
Flatten: flags.Flatten,
FlattenExclude: flags.FlattenExclude,
Depth: flags.Depth,
Labels: flags.Label,
}); err != nil {
return err
}
Expand Down Expand Up @@ -138,6 +140,7 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa
cmd.Flags().BoolVar(&flags.Flatten, "flatten", false, "Flatten the buildpack into a single layer")
cmd.Flags().StringSliceVarP(&flags.FlattenExclude, "flatten-exclude", "e", nil, "Buildpacks to exclude from flattening, in the form of '<buildpack-id>@<buildpack-version>'")
cmd.Flags().IntVar(&flags.Depth, "depth", -1, "Max depth to flatten.\nOmission of this flag or values < 0 will flatten the entire tree.")
cmd.Flags().StringToStringVarP(&flags.Label, "label", "l", nil, "Labels to add to packaged Buildpack, in the form of '<name>=<value>'")
if !cfg.Experimental {
cmd.Flags().MarkHidden("flatten")
cmd.Flags().MarkHidden("depth")
Expand Down
14 changes: 14 additions & 0 deletions internal/commands/buildpack_package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,20 @@ func testPackageCommand(t *testing.T, when spec.G, it spec.S) {
h.AssertError(t, cmd.Execute(), "parsing pull policy")
})
})

when("--label cannot be parsed", func() {
it("errors with a descriptive message", func() {
cmd := packageCommand()
cmd.SetArgs([]string{
"some-image-name", "--config", "/path/to/some/file",
"--label", "name+value",
})

err := cmd.Execute()
h.AssertNotNil(t, err)
h.AssertError(t, err, "invalid argument \"name+value\" for \"-l, --label\" flag: name+value must be formatted as key=value")
})
})
})
}

Expand Down
19 changes: 17 additions & 2 deletions pkg/buildpack/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ func (b *PackageBuilder) resolvedStacks() []dist.Stack {
return stacks
}

func (b *PackageBuilder) SaveAsFile(path, imageOS string) error {
func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]string) error {
if err := b.validate(); err != nil {
return err
}
Expand All @@ -348,6 +348,13 @@ func (b *PackageBuilder) SaveAsFile(path, imageOS string) error {
return errors.Wrap(err, "creating layout image")
}

for labelKey, labelValue := range labels {
err = layoutImage.SetLabel(labelKey, labelValue)
if err != nil {
return errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue)
}
}

tempDirName := ""
if b.buildpack != nil {
tempDirName = "package-buildpack"
Expand Down Expand Up @@ -430,7 +437,7 @@ func newLayoutImage(imageOS string) (*layoutImage, error) {
return &layoutImage{Image: i}, nil
}

func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS string) (imgutil.Image, error) {
func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS string, labels map[string]string) (imgutil.Image, error) {
if err := b.validate(); err != nil {
return nil, err
}
Expand All @@ -439,6 +446,14 @@ func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS stri
if err != nil {
return nil, errors.Wrapf(err, "creating image")
}

for labelKey, labelValue := range labels {
err = image.SetLabel(labelKey, labelValue)
if err != nil {
return nil, errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue)
}
}

tempDirName := ""
if b.buildpack != nil {
tempDirName = "package-buildpack"
Expand Down
97 changes: 80 additions & 17 deletions pkg/buildpack/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"path/filepath"
"testing"

"github.com/pkg/errors"

"github.com/buildpacks/imgutil/fakes"
"github.com/buildpacks/imgutil/layer"
"github.com/buildpacks/lifecycle/api"
Expand Down Expand Up @@ -76,18 +78,18 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
fn func(*buildpack.PackageBuilder) error
}{
{name: "SaveAsImage", expectedImageOS: "linux", fn: func(builder *buildpack.PackageBuilder) error {
_, err := builder.SaveAsImage("some/package", false, "linux")
_, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{})
return err
}},
{name: "SaveAsImage", expectedImageOS: "windows", fn: func(builder *buildpack.PackageBuilder) error {
_, err := builder.SaveAsImage("some/package", false, "windows")
_, err := builder.SaveAsImage("some/package", false, "windows", map[string]string{})
return err
}},
{name: "SaveAsFile", expectedImageOS: "linux", fn: func(builder *buildpack.PackageBuilder) error {
return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "linux")
return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "linux", map[string]string{})
}},
{name: "SaveAsFile", expectedImageOS: "windows", fn: func(builder *buildpack.PackageBuilder) error {
return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "windows")
return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "windows", map[string]string{})
}},
} {
// always use copies to avoid stale refs
Expand Down Expand Up @@ -391,7 +393,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
h.AssertNil(t, err)
builder.AddDependency(dependency2)

img, err := builder.SaveAsImage("some/package", false, expectedImageOS)
img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{})
h.AssertNil(t, err)

metadata := buildpack.Metadata{}
Expand Down Expand Up @@ -453,7 +455,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
h.AssertNil(t, err)
builder.AddDependency(dependency2)

img, err := builder.SaveAsImage("some/package", false, expectedImageOS)
img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{})
h.AssertNil(t, err)

metadata := buildpack.Metadata{}
Expand Down Expand Up @@ -518,7 +520,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {

builder.AddDependency(dependencyNestedNested)

img, err := builder.SaveAsImage("some/package", false, expectedImageOS)
img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{})
h.AssertNil(t, err)

metadata := buildpack.Metadata{}
Expand Down Expand Up @@ -563,7 +565,9 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder := buildpack.NewBuilder(mockImageFactory("linux"))
builder.SetBuildpack(buildpack1)

packageImage, err := builder.SaveAsImage("some/package", false, "linux")
var customLabels = map[string]string{"test.label.one": "1", "test.label.two": "2"}

packageImage, err := builder.SaveAsImage("some/package", false, "linux", customLabels)
h.AssertNil(t, err)

labelData, err := packageImage.Label("io.buildpacks.buildpackage.metadata")
Expand All @@ -586,6 +590,11 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
osVal, err := packageImage.OS()
h.AssertNil(t, err)
h.AssertEq(t, osVal, "linux")

imageLabels, err := packageImage.Labels()
h.AssertNil(t, err)
h.AssertEq(t, imageLabels["test.label.one"], "1")
h.AssertEq(t, imageLabels["test.label.two"], "2")
})

it("sets extension metadata", func() {
Expand All @@ -609,7 +618,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
h.AssertNil(t, err)
builder := buildpack.NewBuilder(mockImageFactory("linux"))
builder.SetExtension(extension1)
packageImage, err := builder.SaveAsImage("some/package", false, "linux")
packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{})
h.AssertNil(t, err)
labelData, err := packageImage.Label("io.buildpacks.buildpackage.metadata")
h.AssertNil(t, err)
Expand Down Expand Up @@ -642,7 +651,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder := buildpack.NewBuilder(mockImageFactory("linux"))
builder.SetBuildpack(buildpack1)

packageImage, err := builder.SaveAsImage("some/package", false, "linux")
packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{})
h.AssertNil(t, err)

var bpLayers dist.ModuleLayers
Expand All @@ -666,7 +675,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder := buildpack.NewBuilder(mockImageFactory("linux"))
builder.SetBuildpack(buildpack1)

packageImage, err := builder.SaveAsImage("some/package", false, "linux")
packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{})
h.AssertNil(t, err)

buildpackExists := func(name, version string) {
Expand Down Expand Up @@ -713,10 +722,51 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder := buildpack.NewBuilder(mockImageFactory("windows"))
builder.SetBuildpack(buildpack1)

_, err = builder.SaveAsImage("some/package", false, "windows")
_, err = builder.SaveAsImage("some/package", false, "windows", map[string]string{})
h.AssertNil(t, err)
})

it("should report an error when custom label cannot be set", func() {
mockImageFactory = func(expectedImageOS string) *testmocks.MockImageFactory {
var imageWithLabelError = &imageWithLabelError{Image: fakes.NewImage("some/package", "", nil)}
imageFactory := testmocks.NewMockImageFactory(mockController)
imageFactory.EXPECT().NewImage("some/package", true, expectedImageOS).Return(imageWithLabelError, nil).MaxTimes(1)
return imageFactory
}

buildpack1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{
WithAPI: api.MustParse("0.2"),
WithInfo: dist.ModuleInfo{
ID: "bp.1.id",
Version: "bp.1.version",
Name: "One",
Description: "some description",
Homepage: "https://example.com/homepage",
Keywords: []string{"some-keyword"},
Licenses: []dist.License{
{
Type: "MIT",
URI: "https://example.com/license",
},
},
},
WithStacks: []dist.Stack{
{ID: "stack.id.1"},
{ID: "stack.id.2"},
},
WithOrder: nil,
}, 0644)
h.AssertNil(t, err)

builder := buildpack.NewBuilder(mockImageFactory("linux"))
builder.SetBuildpack(buildpack1)

var customLabels = map[string]string{"test.label.fail": "true"}

_, err = builder.SaveAsImage("some/package", false, "linux", customLabels)
h.AssertError(t, err, "adding label test.label.fail=true")
})

when("flatten is set", func() {
var (
buildpack1 buildpack.BuildModule
Expand Down Expand Up @@ -841,7 +891,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder.AddDependencies(bp1, nil)
builder.AddDependencies(compositeBP2, []buildpack.BuildModule{bp21, bp22, compositeBP3, bp31})

packageImage, err := builder.SaveAsImage("some/package", false, "linux")
packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{})
h.AssertNil(t, err)

fakePackageImage := packageImage.(*fakes.Image)
Expand All @@ -864,7 +914,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder.AddDependencies(bp1, nil)
builder.AddDependencies(compositeBP2, []buildpack.BuildModule{bp21, bp22, compositeBP3, bp31})

packageImage, err := builder.SaveAsImage("some/package", false, "linux")
packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{})
h.AssertNil(t, err)

fakePackageImage := packageImage.(*fakes.Image)
Expand All @@ -888,8 +938,10 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder := buildpack.NewBuilder(mockImageFactory(""))
builder.SetBuildpack(buildpack1)

var customLabels = map[string]string{"test.label.one": "1", "test.label.two": "2"}

outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10)))
h.AssertNil(t, builder.SaveAsFile(outputFile, "linux"))
h.AssertNil(t, builder.SaveAsFile(outputFile, "linux", customLabels))

withContents := func(fn func(data []byte)) h.TarEntryAssertion {
return func(t *testing.T, header *tar.Header, data []byte) {
Expand Down Expand Up @@ -928,6 +980,9 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
h.ContentContains(`"io.buildpacks.buildpack.layers":"{\"bp.1.id\":{\"bp.1.version\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"stack.id.1\"},{\"id\":\"stack.id.2\"}],\"layerDiffID\":\"sha256:44447e95b06b73496d1891de5afb01936e9999b97ea03dad6337d9f5610807a7\"}}`),
// image os
h.ContentContains(`"os":"linux"`),
// custom labels
h.ContentContains(`"test.label.one":"1"`),
h.ContentContains(`"test.label.two":"2"`),
)
}))
}))
Expand All @@ -946,7 +1001,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder.SetBuildpack(buildpack1)

outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10)))
h.AssertNil(t, builder.SaveAsFile(outputFile, "linux"))
h.AssertNil(t, builder.SaveAsFile(outputFile, "linux", map[string]string{}))

h.AssertOnTarEntry(t, outputFile, "/blobs",
h.IsDirectory(),
Expand Down Expand Up @@ -996,7 +1051,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) {
builder.SetBuildpack(buildpack1)

outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10)))
h.AssertNil(t, builder.SaveAsFile(outputFile, "windows"))
h.AssertNil(t, builder.SaveAsFile(outputFile, "windows", map[string]string{}))

// Windows baselayer content is constant
expectedBaseLayerReader, err := layer.WindowsBaseLayer()
Expand Down Expand Up @@ -1047,3 +1102,11 @@ func computeLayerSHA(reader io.ReadCloser) (string, error) {

return digest.Hex, nil
}

type imageWithLabelError struct {
*fakes.Image
}

func (i *imageWithLabelError) SetLabel(string, string) error {
return errors.New("Label could not be set")
}
7 changes: 5 additions & 2 deletions pkg/client/package_buildpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type PackageBuildpackOptions struct {

// List of buildpack images to exclude from the package been flatten.
FlattenExclude []string

// Map of labels to add to the Buildpack
Labels map[string]string
}

// PackageBuildpack packages buildpack(s) into either an image or file.
Expand Down Expand Up @@ -124,9 +127,9 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti

switch opts.Format {
case FormatFile:
return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS)
return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS, opts.Labels)
case FormatImage:
_, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS)
_, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS, opts.Labels)
return errors.Wrapf(err, "saving image")
default:
return errors.Errorf("unknown format: %s", style.Symbol(opts.Format))
Expand Down
4 changes: 2 additions & 2 deletions pkg/client/package_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ func (c *Client) PackageExtension(ctx context.Context, opts PackageBuildpackOpti

switch opts.Format {
case FormatFile:
return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS)
return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS, map[string]string{})
case FormatImage:
_, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS)
_, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS, map[string]string{})
return errors.Wrapf(err, "saving image")
default:
return errors.Errorf("unknown format: %s", style.Symbol(opts.Format))
Expand Down

0 comments on commit 6329e89

Please sign in to comment.