Skip to content

Commit

Permalink
Merge pull request #935 from buildpacks/jkutner/pull-buildpack
Browse files Browse the repository at this point in the history
Add pull-buildpack command
  • Loading branch information
dfreilich committed Dec 3, 2020
2 parents 6474956 + f03e481 commit 2a17472
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/cmd.go
Expand Up @@ -92,6 +92,7 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) {
rootCmd.AddCommand(commands.SetDefaultRegistry(logger, cfg, cfgPath))
rootCmd.AddCommand(commands.RemoveRegistry(logger, cfg, cfgPath))
rootCmd.AddCommand(commands.YankBuildpack(logger, cfg, &packClient))
rootCmd.AddCommand(commands.PullBuildpack(logger, cfg, &packClient))
}

packHome, err := config.PackHome()
Expand Down
1 change: 1 addition & 0 deletions internal/commands/commands.go
Expand Up @@ -27,6 +27,7 @@ type PackClient interface {
RegisterBuildpack(context.Context, pack.RegisterBuildpackOptions) error
YankBuildpack(pack.YankBuildpackOptions) error
InspectBuildpack(pack.InspectBuildpackOptions) (*pack.BuildpackInfo, error)
PullBuildpack(context.Context, pack.PullBuildpackOptions) error
}

func AddHelpFlag(cmd *cobra.Command, commandName string) {
Expand Down
45 changes: 45 additions & 0 deletions internal/commands/pull_buildpack.go
@@ -0,0 +1,45 @@
package commands

import (
"github.com/spf13/cobra"

"github.com/buildpacks/pack"
"github.com/buildpacks/pack/internal/config"
"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/logging"
)

type PullBuildpackFlags struct {
BuildpackRegistry string
}

func PullBuildpack(logger logging.Logger, cfg config.Config, client PackClient) *cobra.Command {
var opts pack.PullBuildpackOptions
var flags PullBuildpackFlags

cmd := &cobra.Command{
Use: "pull-buildpack <uri>",
Args: cobra.ExactArgs(1),
Short: prependExperimental("Pull the buildpack and store it locally"),
Example: "pack pull-buildpack example/my-buildpack@1.0.0",
RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
registry, err := config.GetRegistry(cfg, flags.BuildpackRegistry)
if err != nil {
return err
}
opts.URI = args[0]
opts.RegistryType = registry.Type
opts.RegistryURL = registry.URL
opts.RegistryName = registry.Name

if err := client.PullBuildpack(cmd.Context(), opts); err != nil {
return err
}
logger.Infof("Successfully pulled %s", style.Symbol(opts.URI))
return nil
}),
}
cmd.Flags().StringVarP(&flags.BuildpackRegistry, "buildpack-registry", "r", "", "Buildpack Registry name")
AddHelpFlag(cmd, "pull-buildpack")
return cmd
}
71 changes: 71 additions & 0 deletions internal/commands/pull_buildpack_test.go
@@ -0,0 +1,71 @@
package commands_test

import (
"bytes"
"testing"

"github.com/golang/mock/gomock"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"github.com/spf13/cobra"

"github.com/buildpacks/pack"
"github.com/buildpacks/pack/internal/commands"
"github.com/buildpacks/pack/internal/commands/testmocks"
"github.com/buildpacks/pack/internal/config"
ilogging "github.com/buildpacks/pack/internal/logging"
"github.com/buildpacks/pack/logging"
h "github.com/buildpacks/pack/testhelpers"
)

func TestPullBuildpackCommand(t *testing.T) {
spec.Run(t, "PullBuildpackCommand", testPullBuildpackCommand, spec.Parallel(), spec.Report(report.Terminal{}))
}

func testPullBuildpackCommand(t *testing.T, when spec.G, it spec.S) {
var (
command *cobra.Command
logger logging.Logger
outBuf bytes.Buffer
mockController *gomock.Controller
mockClient *testmocks.MockPackClient
cfg config.Config
)

it.Before(func() {
logger = ilogging.NewLogWithWriters(&outBuf, &outBuf)
mockController = gomock.NewController(t)
mockClient = testmocks.NewMockPackClient(mockController)
cfg = config.Config{}

command = commands.PullBuildpack(logger, cfg, mockClient)
})

when("#PullBuildpackCommand", func() {
when("no buildpack is provided", func() {
it("fails to run", func() {
err := command.Execute()
h.AssertError(t, err, "accepts 1 arg")
})
})

when("buildpack uri is provided", func() {
it("should work for required args", func() {
buildpackImage := "buildpack/image"
opts := pack.PullBuildpackOptions{
URI: buildpackImage,
RegistryType: "github",
RegistryURL: "https://github.com/buildpacks/registry-index",
RegistryName: "official",
}

mockClient.EXPECT().
PullBuildpack(gomock.Any(), opts).
Return(nil)

command.SetArgs([]string{buildpackImage})
h.AssertNil(t, command.Execute())
})
})
})
}
14 changes: 14 additions & 0 deletions internal/commands/testmocks/mock_pack_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions pull_buildpack.go
@@ -0,0 +1,55 @@
package pack

import (
"context"
"fmt"

"github.com/pkg/errors"

"github.com/buildpacks/pack/config"
"github.com/buildpacks/pack/internal/buildpack"
"github.com/buildpacks/pack/internal/dist"
"github.com/buildpacks/pack/internal/style"
)

type PullBuildpackOptions struct {
URI string
RegistryType string
RegistryURL string
RegistryName string
}

func (c *Client) PullBuildpack(ctx context.Context, opts PullBuildpackOptions) error {
locatorType, err := buildpack.GetLocatorType(opts.URI, []dist.BuildpackInfo{})
if err != nil {
return err
}

switch locatorType {
case buildpack.PackageLocator:
imageName := buildpack.ParsePackageLocator(opts.URI)
_, err = c.imageFetcher.Fetch(ctx, imageName, true, config.PullAlways)
if err != nil {
return errors.Wrapf(err, "fetching image %s", style.Symbol(opts.URI))
}
case buildpack.RegistryLocator:
registryCache, err := c.getRegistry(c.logger, opts.RegistryName)
if err != nil {
return errors.Wrapf(err, "invalid registry '%s'", opts.RegistryName)
}

registryBp, err := registryCache.LocateBuildpack(opts.URI)
if err != nil {
return errors.Wrapf(err, "locating in registry %s", style.Symbol(opts.URI))
}

_, err = c.imageFetcher.Fetch(ctx, registryBp.Address, true, config.PullAlways)
if err != nil {
return errors.Wrapf(err, "fetching image %s", style.Symbol(opts.URI))
}
default:
return fmt.Errorf("invalid buildpack URI %s", style.Symbol(opts.URI))
}

return nil
}
155 changes: 155 additions & 0 deletions pull_buildpack_test.go
@@ -0,0 +1,155 @@
package pack_test

import (
"bytes"
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/buildpacks/imgutil/fakes"

"github.com/heroku/color"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"

"github.com/golang/mock/gomock"

"github.com/buildpacks/pack"
"github.com/buildpacks/pack/config"
cfg "github.com/buildpacks/pack/internal/config"
"github.com/buildpacks/pack/internal/logging"
"github.com/buildpacks/pack/internal/registry"
h "github.com/buildpacks/pack/testhelpers"
"github.com/buildpacks/pack/testmocks"
)

func TestPullBuildpack(t *testing.T) {
color.Disable(true)
defer color.Disable(false)
spec.Run(t, "PackageBuildpack", testPullBuildpack, spec.Parallel(), spec.Report(report.Terminal{}))
}

func testPullBuildpack(t *testing.T, when spec.G, it spec.S) {
var (
subject *pack.Client
mockController *gomock.Controller
mockDownloader *testmocks.MockDownloader
mockImageFactory *testmocks.MockImageFactory
mockImageFetcher *testmocks.MockImageFetcher
mockDockerClient *testmocks.MockCommonAPIClient
out bytes.Buffer
)

it.Before(func() {
mockController = gomock.NewController(t)
mockDownloader = testmocks.NewMockDownloader(mockController)
mockImageFactory = testmocks.NewMockImageFactory(mockController)
mockImageFetcher = testmocks.NewMockImageFetcher(mockController)
mockDockerClient = testmocks.NewMockCommonAPIClient(mockController)

var err error
subject, err = pack.NewClient(
pack.WithLogger(logging.NewLogWithWriters(&out, &out)),
pack.WithDownloader(mockDownloader),
pack.WithImageFactory(mockImageFactory),
pack.WithFetcher(mockImageFetcher),
pack.WithDockerClient(mockDockerClient),
)
h.AssertNil(t, err)
})

it.After(func() {
mockController.Finish()
})

when("buildpack has issues", func() {
it("should fail if not in the registry", func() {
err := subject.PullBuildpack(context.TODO(), pack.PullBuildpackOptions{
URI: "invalid/image",
RegistryType: "github",
RegistryURL: registry.DefaultRegistryURL,
RegistryName: registry.DefaultRegistryName,
})
h.AssertError(t, err, "locating in registry")
})

it("should fail if not a valid URI", func() {
err := subject.PullBuildpack(context.TODO(), pack.PullBuildpackOptions{
URI: "foobar",
RegistryType: "github",
RegistryURL: registry.DefaultRegistryURL,
RegistryName: registry.DefaultRegistryName,
})
h.AssertError(t, err, "invalid buildpack URI")
})
})

when("pulling from a docker registry", func() {
it("should fetch the image", func() {
packageImage := fakes.NewImage("example.com/some/package:1.0.0", "", nil)
packageImage.SetLabel("io.buildpacks.buildpackage.metadata", `{}`)
packageImage.SetLabel("io.buildpacks.buildpack.layers", `{}`)
mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), true, config.PullAlways).Return(packageImage, nil)

h.AssertNil(t, subject.PullBuildpack(context.TODO(), pack.PullBuildpackOptions{
URI: "example.com/some/package:1.0.0",
}))
})
})

when("pulling from a buildpack registry", func() {
var (
tmpDir string
registryFixture string
packHome string
)

it.Before(func() {
var err error
tmpDir, err = ioutil.TempDir("", "registry")
h.AssertNil(t, err)

packHome = filepath.Join(tmpDir, ".pack")
err = os.MkdirAll(packHome, 0755)
h.AssertNil(t, err)
os.Setenv("PACK_HOME", packHome)

registryFixture = h.CreateRegistryFixture(t, tmpDir, filepath.Join("testdata", "registry"))

packageImage := fakes.NewImage("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", "", nil)
packageImage.SetLabel("io.buildpacks.buildpackage.metadata", `{}`)
packageImage.SetLabel("io.buildpacks.buildpack.layers", `{}`)
mockImageFetcher.EXPECT().Fetch(gomock.Any(), packageImage.Name(), true, config.PullAlways).Return(packageImage, nil)

packHome := filepath.Join(tmpDir, "packHome")
h.AssertNil(t, os.Setenv("PACK_HOME", packHome))
configPath := filepath.Join(packHome, "config.toml")
h.AssertNil(t, cfg.Write(cfg.Config{
Registries: []cfg.Registry{
{
Name: "some-registry",
Type: "github",
URL: registryFixture,
},
},
}, configPath))
})

it.After(func() {
os.Unsetenv("PACK_HOME")
err := os.RemoveAll(tmpDir)
h.AssertNil(t, err)
})

it("should fetch the image", func() {
h.AssertNil(t, subject.PullBuildpack(context.TODO(), pack.PullBuildpackOptions{
URI: "example/foo@1.1.0",
RegistryType: "github",
RegistryURL: registryFixture,
RegistryName: "some-registry",
}))
})
})
}

0 comments on commit 2a17472

Please sign in to comment.