Skip to content

Commit f3f7af1

Browse files
authored
Merge pull request #1198 from importhuman/importhuman
Add previous-image flag
2 parents 0b1f5c8 + f3f073d commit f3f7af1

8 files changed

Lines changed: 174 additions & 2 deletions

File tree

build.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ type BuildOptions struct {
163163

164164
// User's group id used to build the image
165165
GroupID int
166+
167+
// A previous image to set to a particular tag reference, digest reference, or (when performing a daemon build) image ID;
168+
PreviousImage string
166169
}
167170

168171
// ProxyConfig specifies proxy setting to be set as environment variables in a container.
@@ -326,6 +329,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
326329
FileFilter: fileFilter,
327330
Workspace: opts.Workspace,
328331
GID: opts.GroupID,
332+
PreviousImage: opts.PreviousImage,
329333
}
330334

331335
lifecycleVersion := ephemeralBuilder.LifecycleDescriptor().Info.Version

build_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2379,6 +2379,18 @@ func testBuild(t *testing.T, when spec.G, it spec.S) {
23792379
h.AssertEq(t, fakeLifecycle.Opts.RunImage, "10.0.0.1/default/run:latest")
23802380
})
23812381
})
2382+
2383+
when("previous-image option", func() {
2384+
it("previous-image is passed to lifecycle", func() {
2385+
h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{
2386+
Workspace: "app",
2387+
Builder: defaultBuilderName,
2388+
Image: "example.com/some/repo:tag",
2389+
PreviousImage: "example.com/some/new:tag",
2390+
}))
2391+
h.AssertEq(t, fakeLifecycle.Opts.PreviousImage, "example.com/some/new:tag")
2392+
})
2393+
})
23822394
})
23832395
}
23842396

create_builder.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import (
44
"context"
55
"fmt"
66

7-
"github.com/buildpacks/pack/config"
8-
97
"github.com/Masterminds/semver"
108
"github.com/buildpacks/imgutil"
119
"github.com/pkg/errors"
1210

1311
pubbldr "github.com/buildpacks/pack/builder"
12+
"github.com/buildpacks/pack/config"
1413
"github.com/buildpacks/pack/internal/builder"
1514
"github.com/buildpacks/pack/internal/dist"
1615
"github.com/buildpacks/pack/internal/image"

internal/build/lifecycle_execution.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,31 @@ func (l *LifecycleExecution) Create(ctx context.Context, publish bool, dockerHos
188188
flags = append(flags, "-gid", strconv.Itoa(l.opts.GID))
189189
}
190190

191+
if l.opts.PreviousImage != "" {
192+
if l.opts.Image == nil {
193+
return errors.New("image can't be nil")
194+
}
195+
196+
image, err := name.ParseReference(l.opts.Image.Name(), name.WeakValidation)
197+
if err != nil {
198+
return fmt.Errorf("invalid image name: %s", err)
199+
}
200+
201+
prevImage, err := name.ParseReference(l.opts.PreviousImage, name.WeakValidation)
202+
if err != nil {
203+
return fmt.Errorf("invalid previous image name: %s", err)
204+
}
205+
if publish {
206+
if image.Context().RegistryStr() != prevImage.Context().RegistryStr() {
207+
return fmt.Errorf(`when --publish is used, <previous-image> must be in the same image registry as <image>
208+
image registry = %s
209+
previous-image registry = %s`, image.Context().RegistryStr(), prevImage.Context().RegistryStr())
210+
}
211+
}
212+
213+
flags = append(flags, "-previous-image", l.opts.PreviousImage)
214+
}
215+
191216
processType := determineDefaultProcessType(l.platformAPI, l.opts.DefaultProcessType)
192217
if processType != "" {
193218
flags = append(flags, "-process-type", processType)

internal/build/lifecycle_execution_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,99 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
839839
})
840840
})
841841
})
842+
843+
when("-previous-image is used", func() {
844+
when("image is invalid", func() {
845+
it("errors", func() {
846+
var imageName name.Tag
847+
imageName, err := name.NewTag("/x/y/?!z", name.WeakValidation)
848+
h.AssertError(t, err, "repository can only contain the runes `abcdefghijklmnopqrstuvwxyz0123456789_-./`")
849+
lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) {
850+
options.Image = imageName
851+
options.PreviousImage = "previous-image"
852+
})
853+
fakePhaseFactory := fakes.NewFakePhaseFactory()
854+
err = lifecycle.Create(context.Background(), false, "", false, "test", "test", "test", fakeBuildCache, fakeLaunchCache, []string{}, []string{}, fakePhaseFactory)
855+
h.AssertError(t, err, "invalid image name")
856+
})
857+
})
858+
859+
when("previous-image is invalid", func() {
860+
it("errors", func() {
861+
var imageName name.Tag
862+
imageName, err := name.NewTag("/some/image", name.WeakValidation)
863+
h.AssertNil(t, err)
864+
lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) {
865+
options.PreviousImage = "%%%"
866+
options.Image = imageName
867+
})
868+
fakePhaseFactory := fakes.NewFakePhaseFactory()
869+
err = lifecycle.Create(context.Background(), false, "", false, "test", "test", "test", fakeBuildCache, fakeLaunchCache, []string{}, []string{}, fakePhaseFactory)
870+
h.AssertError(t, err, "invalid previous image name")
871+
})
872+
})
873+
874+
when("--publish is false", func() {
875+
it("passes previous-image to creator", func() {
876+
var imageName name.Tag
877+
imageName, err := name.NewTag("/some/image", name.WeakValidation)
878+
h.AssertNil(t, err)
879+
lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) {
880+
options.PreviousImage = "previous-image"
881+
options.Image = imageName
882+
})
883+
fakePhaseFactory := fakes.NewFakePhaseFactory()
884+
err = lifecycle.Create(context.Background(), false, "", false, "test", "test", "test", fakeBuildCache, fakeLaunchCache, []string{}, []string{}, fakePhaseFactory)
885+
h.AssertNil(t, err)
886+
887+
lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1
888+
h.AssertNotEq(t, lastCallIndex, -1)
889+
890+
configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex]
891+
h.AssertEq(t, configProvider.Name(), "creator")
892+
h.AssertIncludeAllExpectedPatterns(t, configProvider.ContainerConfig().Cmd, []string{"-previous-image", "previous-image"})
893+
})
894+
})
895+
896+
when("--publish is true", func() {
897+
when("previous-image and image are in the same registry", func() {
898+
it("successfully passes previous-image to creator", func() {
899+
var imageName name.Tag
900+
imageName, err := name.NewTag("/some/image", name.WeakValidation)
901+
h.AssertNil(t, err)
902+
lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) {
903+
options.PreviousImage = "index.docker.io/some/previous:latest"
904+
options.Image = imageName
905+
})
906+
fakePhaseFactory := fakes.NewFakePhaseFactory()
907+
err = lifecycle.Create(context.Background(), true, "", false, "test", "test", "test", fakeBuildCache, fakeLaunchCache, []string{}, []string{}, fakePhaseFactory)
908+
h.AssertNil(t, err)
909+
910+
lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1
911+
h.AssertNotEq(t, lastCallIndex, -1)
912+
913+
configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex]
914+
h.AssertEq(t, configProvider.Name(), "creator")
915+
h.AssertIncludeAllExpectedPatterns(t, configProvider.ContainerConfig().Cmd, []string{"-previous-image", "index.docker.io/some/previous:latest"})
916+
})
917+
})
918+
919+
when("previous-image and image are not in the same registry", func() {
920+
it("errors", func() {
921+
var imageName name.Tag
922+
imageName, err := name.NewTag("/some/image", name.WeakValidation)
923+
h.AssertNil(t, err)
924+
lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) {
925+
options.PreviousImage = "example.io/some/previous:latest"
926+
options.Image = imageName
927+
})
928+
fakePhaseFactory := fakes.NewFakePhaseFactory()
929+
err = lifecycle.Create(context.Background(), true, "", false, "test", "test", "test", fakeBuildCache, fakeLaunchCache, []string{}, []string{}, fakePhaseFactory)
930+
h.AssertError(t, err, fmt.Sprintf("%s", err))
931+
})
932+
})
933+
})
934+
})
842935
})
843936

844937
when("#Detect", func() {

internal/build/lifecycle_executor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type LifecycleOptions struct {
7575
FileFilter func(string) bool
7676
Workspace string
7777
GID int
78+
PreviousImage string
7879
}
7980

8081
func NewLifecycleExecutor(logger logging.Logger, docker client.CommonAPIClient) *LifecycleExecutor {

internal/commands/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type BuildFlags struct {
4242
AdditionalTags []string
4343
Workspace string
4444
GID int
45+
PreviousImage string
4546
}
4647

4748
// Build an image from source code
@@ -152,6 +153,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
152153
Workspace: flags.Workspace,
153154
LifecycleImage: lifecycleImage,
154155
GroupID: gid,
156+
PreviousImage: flags.PreviousImage,
155157
}); err != nil {
156158
return errors.Wrap(err, "failed to build")
157159
}
@@ -191,6 +193,7 @@ This option may set DOCKER_HOST environment variable for the build container if
191193
cmd.Flags().StringArrayVar(&buildFlags.Volumes, "volume", nil, "Mount host volume into the build container, in the form '<host path>:<target path>[:<options>]'.\n- 'host path': Name of the volume or absolute directory path to mount.\n- 'target path': The path where the file or directory is available in the container.\n- 'options' (default \"ro\"): An optional comma separated list of mount options.\n - \"ro\", volume contents are read-only.\n - \"rw\", volume contents are readable and writeable.\n - \"volume-opt=<key>=<value>\", can be specified more than once, takes a key-value pair consisting of the option name and its value."+multiValueHelp("volume"))
192194
cmd.Flags().StringVar(&buildFlags.Workspace, "workspace", "", "Location at which to mount the app dir in the build image")
193195
cmd.Flags().IntVar(&buildFlags.GID, "gid", 0, `Override GID of user's group in the stack's build and run images. The provided value must be a positive number`)
196+
cmd.Flags().StringVar(&buildFlags.PreviousImage, "previous-image", "", "Set previous image to a particular tag reference, digest reference, or (when performing a daemon build) image ID")
194197
}
195198

196199
func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackClient, logger logging.Logger) error {

internal/commands/build_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,32 @@ builder = "my-builder"
710710
h.AssertNil(t, command.Execute())
711711
})
712712
})
713+
714+
when("previous-image flag is provided", func() {
715+
when("--publish is false", func() {
716+
it("previous-image should be passed to creator", func() {
717+
mockClient.EXPECT().
718+
Build(gomock.Any(), EqBuildOptionsWithPreviousImage("previous-image")).
719+
Return(nil)
720+
721+
command.SetArgs([]string{"--builder", "my-builder", "image", "--previous-image", "previous-image"})
722+
h.AssertNil(t, command.Execute())
723+
})
724+
})
725+
726+
when("--publish is true", func() {
727+
when("image and previous-image are in same registry", func() {
728+
it("succeeds", func() {
729+
mockClient.EXPECT().
730+
Build(gomock.Any(), EqBuildOptionsWithPreviousImage("index.docker.io/some/previous:latest")).
731+
Return(nil)
732+
733+
command.SetArgs([]string{"--builder", "my-builder", "index.docker.io/some/image:latest", "--previous-image", "index.docker.io/some/previous:latest", "--publish"})
734+
h.AssertNil(t, command.Execute())
735+
})
736+
})
737+
})
738+
})
713739
})
714740
}
715741

@@ -840,6 +866,15 @@ func EqBuildOptionsWithOverrideGroupID(gid int) gomock.Matcher {
840866
}
841867
}
842868

869+
func EqBuildOptionsWithPreviousImage(prevImage string) gomock.Matcher {
870+
return buildOptionsMatcher{
871+
description: fmt.Sprintf("Previous image=%s", prevImage),
872+
equals: func(o pack.BuildOptions) bool {
873+
return o.PreviousImage == prevImage
874+
},
875+
}
876+
}
877+
843878
type buildOptionsMatcher struct {
844879
equals func(pack.BuildOptions) bool
845880
description string

0 commit comments

Comments
 (0)