Skip to content
Draft
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
1 change: 1 addition & 0 deletions internal/builder/image_fetcher_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ImageFetcher interface {
// the behavior is dictated by the pull policy, which can have the following behavior
// - PullNever: returns false
// - PullAlways Or PullIfNotPresent: it will check read access for the remote image.
// - PullIfAvailable: it will check read access for the remote image, if the image is not found then false.
// When FetchOptions.Daemon is false it will check read access for the remote image.
CheckReadAccess(repo string, options image.FetchOptions) bool
}
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ This option may set DOCKER_HOST environment variable for the build container if
`)
cmd.Flags().StringVar(&buildFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`)
cmd.Flags().StringVar(&buildFlags.Platform, "platform", "", `Platform to build on (e.g., "linux/amd64").`)
cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`)
cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, if-not-present and try-always. (default "always")`)
cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name")
cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)")
cmd.Flags().StringSliceVarP(&buildFlags.AdditionalTags, "tag", "t", nil, "Additional tags to push the output image to.\nTags should be in the format 'image:tag' or 'repository/image:tag'."+stringSliceHelp("tag"))
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/builder_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Creating a custom builder allows you to control what buildpacks are used and wha
cmd.Flags().StringVarP(&flags.BuilderTomlPath, "config", "c", "", "Path to builder TOML file (required)")
cmd.Flags().BoolVar(&flags.Publish, "publish", false, "Publish the builder directly to the container registry specified in <image-name>, instead of the daemon.")
cmd.Flags().BoolVar(&flags.AppendImageNameSuffix, "append-image-name-suffix", false, "When publishing to a registry that doesn't allow overwrite existing tags use this flag to append a [os]-[arch] suffix to <image-name>")
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().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, if-not-present and try-always. The default is always")
cmd.Flags().StringArrayVar(&flags.Flatten, "flatten", nil, "List of buildpacks to flatten together into a single layer (format: '<buildpack-id>@<buildpack-version>,<buildpack-id>@<buildpack-version>'")
cmd.Flags().StringToStringVarP(&flags.Label, "label", "l", nil, "Labels to add to the builder image, in the form of '<name>=<value>'")
cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil,
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/buildpack_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa
cmd.Flags().StringVarP(&flags.Format, "format", "f", "", `Format to save package as ("image" or "file")`)
cmd.Flags().BoolVar(&flags.Publish, "publish", false, `Publish the buildpack directly to the container registry specified in <name>, instead of the daemon (applies to "--format=image" only).`)
cmd.Flags().BoolVar(&flags.AppendImageNameSuffix, "append-image-name-suffix", false, "When publishing to a registry that doesn't allow overwrite existing tags use this flag to append a [os]-[arch] suffix to package <name>")
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().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, if-not-present and try-always. The default is always")
cmd.Flags().StringVarP(&flags.Path, "path", "p", "", "Path to the Buildpack that needs to be packaged")
cmd.Flags().StringVarP(&flags.BuildpackRegistry, "buildpack-registry", "r", "", "Buildpack Registry name")
cmd.Flags().BoolVar(&flags.Flatten, "flatten", false, "Flatten the buildpack into a single layer")
Expand Down
4 changes: 2 additions & 2 deletions internal/commands/config_pull_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ func ConfigPullPolicy(logger logging.Logger, cfg config.Config, cfgPath string)
var unset bool

cmd := &cobra.Command{
Use: "pull-policy <always | if-not-present | never>",
Use: "pull-policy <always | if-not-present | never | try-always>",
Args: cobra.MaximumNArgs(1),
Short: "List, set and unset the global pull policy used by other commands",
Long: "You can use this command to list, set, and unset the default pull policy that will be used when working with containers:\n" +
"* To list your pull policy, run `pack config pull-policy`.\n" +
"* To set your pull policy, run `pack config pull-policy <always | if-not-present | never>`.\n" +
"* To set your pull policy, run `pack config pull-policy <always | if-not-present | never | try-always>`.\n" +
"* To unset your pull policy, run `pack config pull-policy --unset`.\n" +
fmt.Sprintf("Unsetting the pull policy will reset the policy to the default, which is %s", style.Symbol("always")),
RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
Expand Down
12 changes: 12 additions & 0 deletions internal/commands/config_pull_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ func testConfigPullPolicyCommand(t *testing.T, when spec.G, it spec.S) {
assert.Contains(outBuf.String(), "if-not-present")
})
})

when("policy set to try-always in config", func() {
it("lists try-always as pull policy", func() {
cfg.PullPolicy = "try-always"
command = commands.ConfigPullPolicy(logger, cfg, configFile)
command.SetArgs([]string{})

h.AssertNil(t, command.Execute())

assert.Contains(outBuf.String(), "try-always")
})
})
})
when("set", func() {
when("policy provided is the same as configured pull policy", func() {
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/create_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ Creating a custom builder allows you to control what buildpacks are used and wha
}
cmd.Flags().StringVarP(&flags.BuilderTomlPath, "config", "c", "", "Path to builder TOML file (required)")
cmd.Flags().BoolVar(&flags.Publish, "publish", false, "Publish the builder directly to the container registry specified in <image-name>, instead of the daemon.")
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().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, if-not-present and try-always. The default is always")
return cmd
}
2 changes: 1 addition & 1 deletion internal/commands/extension_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi
cmd.Flags().StringVarP(&flags.PackageTomlPath, "config", "c", "", "Path to package TOML config")
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().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, if-not-present and try-always. 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/package_buildpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func PackageBuildpack(logger logging.Logger, cfg config.Config, packager Buildpa

cmd.Flags().StringVarP(&flags.Format, "format", "f", "", `Format to save package as ("image" or "file")`)
cmd.Flags().BoolVar(&flags.Publish, "publish", false, `Publish the buildpack 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().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, if-not-present and try-always. The default is always")
cmd.Flags().StringVarP(&flags.BuildpackRegistry, "buildpack-registry", "r", "", "Buildpack Registry name")

AddHelpFlag(cmd, "package-buildpack")
Expand Down
11 changes: 11 additions & 0 deletions internal/commands/package_buildpack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ func testPackageBuildpackCommand(t *testing.T, when spec.G, it spec.S) {
receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
h.AssertEq(t, receivedOptions.PullPolicy, image.PullAlways)
})

it("pull-policy=try-always sets policy", func() {
args = append(args, "--pull-policy", "try-always")
cmd.SetArgs(args)

err := cmd.Execute()
h.AssertNil(t, err)

receivedOptions := fakeBuildpackPackager.CreateCalledWithOptions
h.AssertEq(t, receivedOptions.PullPolicy, image.PullIfAvailable)
})
it("takes precedence over a configured pull policy", func() {
logger := logging.NewLogWithWriters(&bytes.Buffer{}, &bytes.Buffer{})
configReader := fakes.NewFakePackageConfigReader()
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/rebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func Rebase(logger logging.Logger, cfg config.Config, pack PackClient) *cobra.Co

cmd.Flags().BoolVar(&opts.Publish, "publish", false, "Publish the rebased application image directly to the container registry specified in <image-name>, instead of the daemon. The previous application image must also reside in the registry.")
cmd.Flags().StringVar(&opts.RunImage, "run-image", "", "Run image to use for rebasing")
cmd.Flags().StringVar(&policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always")
cmd.Flags().StringVar(&policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, if-not-present and try-always. The default is always")
cmd.Flags().StringVar(&opts.PreviousImage, "previous-image", "", "Image to rebase. Set to a particular tag reference, digest reference, or (when performing a daemon build) image ID. Use this flag in combination with <image-name> to avoid replacing the original image.")
cmd.Flags().StringVar(&opts.ReportDestinationDir, "report-output-dir", "", "Path to export build report.toml.\nOmitting the flag yield no report file.")
cmd.Flags().BoolVar(&opts.Force, "force", false, "Perform rebase operation without target validation (only available for API >= 0.12)")
Expand Down
2 changes: 1 addition & 1 deletion internal/fakes/fake_image_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,5 @@ func shouldPull(localFound, remoteFound bool, policy image.PullPolicy) bool {
return true
}

return remoteFound && policy == image.PullAlways
return remoteFound && (policy == image.PullAlways || policy == image.PullIfAvailable)
}
2 changes: 2 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type ImageFetcher interface {
// - PullNever: try to use the daemon to return a `local.Image`.
// - PullIfNotPResent: try look to use the daemon to return a `local.Image`, if none is found fetch a remote image.
// - PullAlways: it will only try to fetch a remote image.
// - PullIfAvailable: it will try to fetch a remote image, if none is found it will try to use the daemon to return a `local.Image`.
//
// These PullPolicies that these interact with the daemon argument.
// PullIfNotPresent and daemon = false, gives us the same behavior as PullAlways.
Expand All @@ -65,6 +66,7 @@ type ImageFetcher interface {
// the behavior is dictated by the pull policy, which can have the following behavior
// - PullNever: returns false
// - PullAlways Or PullIfNotPresent: it will check read access for the remote image.
// - PullIfAvailable: it will check read access for the remote image, if the image is not found then false.
// When FetchOptions.Daemon is false it will check read access for the remote image.
CheckReadAccess(repo string, options image.FetchOptions) bool
}
Expand Down
7 changes: 5 additions & 2 deletions pkg/image/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ func (f *Fetcher) Fetch(ctx context.Context, name string, options FetchOptions)
err = f.pullImage(ctx, name, "")
}
}
if err != nil && !errors.Is(err, ErrNotFound) {

if err != nil {
if errors.Is(err, ErrNotFound) && options.PullPolicy == PullIfAvailable {
return f.fetchDaemonImage(name)
}
return nil, err
}

return f.fetchDaemonImage(name)
}

Expand Down
130 changes: 108 additions & 22 deletions pkg/image/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,29 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) {
})
})
})

when("PullIfAvailable", func() {
when("there is a remote image", func() {
it.Before(func() {
img, err := remote.NewImage(repoName, authn.DefaultKeychain)
h.AssertNil(t, err)

h.AssertNil(t, img.Save())
})

it("returns the remote image", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullIfAvailable})
h.AssertNil(t, err)
})
})

when("there is no remote image", func() {
it("returns an error", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: false, PullPolicy: image.PullIfAvailable})
h.AssertError(t, err, fmt.Sprintf("image '%s' does not exist in registry", repoName))
})
})
})
})

when("daemon is true", func() {
Expand Down Expand Up @@ -233,6 +256,70 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) {
})
})

when("image platform is specified", func() {
it("passes the platform argument to the daemon", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "some-unsupported-platform"}})
h.AssertError(t, err, "unknown operating system or architecture")
})

when("remote platform does not match", func() {
it.Before(func() {
img, err := remote.NewImage(repoName, authn.DefaultKeychain, remote.WithDefaultPlatform(imgutil.Platform{OS: osType, Architecture: ""}))
h.AssertNil(t, err)
h.AssertNil(t, img.Save())
})

it("retries without setting platform", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: osType, Arch: runtime.GOARCH}})
h.AssertNil(t, err)
})
})
})
})

when("PullIfAvailable", func() {
when("there is a remote image", func() {
var (
logger *logging.LogWithWriters
output func() string
)

it.Before(func() {
// Instantiate a pull-able local image
// as opposed to a remote image so that the image
// is created with the OS of the docker daemon
img, err := local.NewImage(repoName, docker)
h.AssertNil(t, err)
defer h.DockerRmi(docker, repoName)

h.AssertNil(t, img.Save())

h.AssertNil(t, h.PushImage(docker, img.Name(), registryConfig))

var outCons *color.Console
outCons, output = h.MockWriterAndOutput()
logger = logging.NewLogWithWriters(outCons, outCons)
imageFetcher = image.NewFetcher(logger, docker)
})

it.After(func() {
h.DockerRmi(docker, repoName)
})

it("pull the image and return the local copy", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfAvailable})
h.AssertNil(t, err)
h.AssertNotEq(t, output(), "")
})

it("doesn't log anything in quiet mode", func() {
logger.WantQuiet(true)
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfAvailable})
h.AssertNil(t, err)
h.AssertEq(t, output(), "")
})
})

when("there is no remote image", func() {
when("there is a local image", func() {
it.Before(func() {
Expand All @@ -247,38 +334,18 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) {
})

it("returns the local image", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways})
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfAvailable})
h.AssertNil(t, err)
})
})

when("there is no local image", func() {
it("returns an error", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways})
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullIfAvailable})
h.AssertError(t, err, fmt.Sprintf("image '%s' does not exist on the daemon", repoName))
})
})
})

when("image platform is specified", func() {
it("passes the platform argument to the daemon", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: "some-unsupported-platform"}})
h.AssertError(t, err, "unknown operating system or architecture")
})

when("remote platform does not match", func() {
it.Before(func() {
img, err := remote.NewImage(repoName, authn.DefaultKeychain, remote.WithDefaultPlatform(imgutil.Platform{OS: osType, Architecture: ""}))
h.AssertNil(t, err)
h.AssertNil(t, img.Save())
})

it("retries without setting platform", func() {
_, err := imageFetcher.Fetch(context.TODO(), repoName, image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways, Target: &dist.Target{OS: osType, Arch: runtime.GOARCH}})
h.AssertNil(t, err)
})
})
})
})

when("PullIfNotPresent", func() {
Expand Down Expand Up @@ -477,6 +544,13 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) {
h.AssertContains(t, outBuf.String(), "failed reading image 'pack.test/dummy' from the daemon")
})
})

when("PullIfAvailable", func() {
it("read access must be false", func() {
h.AssertFalse(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfAvailable}))
h.AssertContains(t, outBuf.String(), "failed reading image 'pack.test/dummy' from the daemon")
})
})
})

when("image exists only in the daemon", func() {
Expand All @@ -502,6 +576,12 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) {
h.AssertTrue(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfNotPresent}))
})
})

when("PullIfAvailable", func() {
it("read access must be true", func() {
h.AssertTrue(t, imageFetcher.CheckReadAccess("pack.test/dummy", image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfAvailable}))
})
})
})

when("image doesn't exist in the daemon but in remote", func() {
Expand All @@ -527,6 +607,12 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) {
h.AssertTrue(t, imageFetcher.CheckReadAccess(repoName, image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfNotPresent}))
})
})

when("PullIfAvailable", func() {
it("read access must be true", func() {
h.AssertTrue(t, imageFetcher.CheckReadAccess(repoName, image.FetchOptions{Daemon: daemon, PullPolicy: image.PullIfAvailable}))
})
})
})
})

Expand Down
Loading