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

Add buildpack pull command #935

Merged
merged 7 commits into from Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/cmd.go
Expand Up @@ -91,6 +91,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))
}

rootCmd.AddCommand(commands.CompletionCommand(logger))
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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could you add a Long: description clarifying why people would use it, and what valid inputs look like?

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused by this in the beginning, until I realized I didn't know enough about the formal definition of a URI (which it should definitely fit):

A URI is an identifier of a specific resource. Like a page, or book, or a document. A URL is special type of identifier that also tells you how to access it, such as HTTPs, FTP, etc.—like https://www.google.com.
https://danielmiessler.com/study/difference-between-uri-url/

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:
dfreilich marked this conversation as resolved.
Show resolved Hide resolved
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",
}))
})
})
}