diff --git a/cmd/skaffold/app/cmd/init.go b/cmd/skaffold/app/cmd/init.go index 4c36e2a40a8..48fc5257ef8 100644 --- a/cmd/skaffold/app/cmd/init.go +++ b/cmd/skaffold/app/cmd/init.go @@ -25,11 +25,12 @@ import ( ) var ( - composeFile string - cliArtifacts []string - skipBuild bool - force bool - analyze bool + composeFile string + cliArtifacts []string + skipBuild bool + force bool + analyze bool + enableJibInit bool ) // NewCmdInit describes the CLI command to generate a Skaffold configuration. @@ -43,17 +44,20 @@ func NewCmdInit() *cobra.Command { f.StringVar(&composeFile, "compose-file", "", "Initialize from a docker-compose file") f.StringSliceVarP(&cliArtifacts, "artifact", "a", nil, "'='-delimited dockerfile/image pair to generate build artifact\n(example: --artifact=/web/Dockerfile.web=gcr.io/web-project/image)") f.BoolVar(&analyze, "analyze", false, "Print all discoverable Dockerfiles and images in JSON format to stdout") + f.BoolVar(&enableJibInit, "XXenableJibInit", false, "") + f.MarkHidden("XXenableJibInit") }). NoArgs(doInit) } func doInit(out io.Writer) error { return initializer.DoInit(out, initializer.Config{ - ComposeFile: composeFile, - CliArtifacts: cliArtifacts, - SkipBuild: skipBuild, - Force: force, - Analyze: analyze, - Opts: opts, + ComposeFile: composeFile, + CliArtifacts: cliArtifacts, + SkipBuild: skipBuild, + Force: force, + Analyze: analyze, + EnableJibInit: enableJibInit, + Opts: opts, }) } diff --git a/pkg/skaffold/docker/docker_init.go b/pkg/skaffold/docker/docker_init.go index f93f5eb9122..4b800db1453 100644 --- a/pkg/skaffold/docker/docker_init.go +++ b/pkg/skaffold/docker/docker_init.go @@ -34,7 +34,9 @@ var ( ) // Docker is the path to a dockerfile. Implements the InitBuilder interface. -type Docker string +type Docker struct { + File string `json:"path"` +} // Name returns the name of the builder, "Docker" func (d Docker) Name() string { @@ -43,20 +45,19 @@ func (d Docker) Name() string { // Describe returns the initBuilder's string representation, used when prompting the user to choose a builder. func (d Docker) Describe() string { - return fmt.Sprintf("%s (%s)", d.Name(), d) + return fmt.Sprintf("%s (%s)", d.Name(), d.File) } // CreateArtifact creates an Artifact to be included in the generated Build Config func (d Docker) CreateArtifact(manifestImage string) *latest.Artifact { - path := string(d) - workspace := filepath.Dir(path) + workspace := filepath.Dir(d.File) a := &latest.Artifact{ImageName: manifestImage} if workspace != "." { a.Workspace = workspace } - if filepath.Base(path) != constants.DefaultDockerfilePath { + if filepath.Base(d.File) != constants.DefaultDockerfilePath { a.ArtifactType = latest.ArtifactType{ - DockerArtifact: &latest.DockerArtifact{DockerfilePath: path}, + DockerArtifact: &latest.DockerArtifact{DockerfilePath: d.File}, } } @@ -71,7 +72,7 @@ func (d Docker) ConfiguredImage() string { // Path returns the path to the dockerfile func (d Docker) Path() string { - return string(d) + return d.File } // ValidateDockerfile makes sure the given Dockerfile is existing and valid. diff --git a/pkg/skaffold/docker/docker_init_test.go b/pkg/skaffold/docker/docker_init_test.go index c4578cc46db..fa6a4c6ccd1 100644 --- a/pkg/skaffold/docker/docker_init_test.go +++ b/pkg/skaffold/docker/docker_init_test.go @@ -75,7 +75,7 @@ func TestDescribe(t *testing.T) { }{ { description: "Dockerfile prompt", - dockerfile: Docker("path/to/Dockerfile"), + dockerfile: Docker{File: "path/to/Dockerfile"}, expectedPrompt: "Docker (path/to/Dockerfile)", }, } @@ -96,7 +96,7 @@ func TestCreateArtifact(t *testing.T) { }{ { description: "default filename", - dockerfile: Docker(filepath.Join("path", "to", "Dockerfile")), + dockerfile: Docker{File: filepath.Join("path", "to", "Dockerfile")}, manifestImage: "image", expectedArtifact: latest.Artifact{ ImageName: "image", @@ -106,7 +106,7 @@ func TestCreateArtifact(t *testing.T) { }, { description: "non-default filename", - dockerfile: Docker(filepath.Join("path", "to", "Dockerfile1")), + dockerfile: Docker{File: filepath.Join("path", "to", "Dockerfile1")}, manifestImage: "image", expectedArtifact: latest.Artifact{ ImageName: "image", @@ -118,7 +118,7 @@ func TestCreateArtifact(t *testing.T) { }, { description: "ignore workspace", - dockerfile: Docker("Dockerfile"), + dockerfile: Docker{File: "Dockerfile"}, manifestImage: "image", expectedArtifact: latest.Artifact{ ImageName: "image", diff --git a/pkg/skaffold/initializer/init.go b/pkg/skaffold/initializer/init.go index e8e94d117b6..66201f3a8ed 100644 --- a/pkg/skaffold/initializer/init.go +++ b/pkg/skaffold/initializer/init.go @@ -77,12 +77,13 @@ type InitBuilder interface { // Config defines the Initializer Config for Init API of skaffold. type Config struct { - ComposeFile string - CliArtifacts []string - SkipBuild bool - Force bool - Analyze bool - Opts *config.SkaffoldOptions + ComposeFile string + CliArtifacts []string + SkipBuild bool + Force bool + Analyze bool + EnableJibInit bool // TODO: Remove this parameter + Opts *config.SkaffoldOptions } // builderImagePair defines a builder and the image it builds @@ -114,20 +115,25 @@ func DoInit(out io.Writer, c Config) error { return err } images := k.GetImages() + + // Determine which builders/images require prompting + pairs, unresolvedBuilderConfigs, unresolvedImages := autoSelectBuilders(builderConfigs, images) + if c.Analyze { - return printAnalyzeJSON(out, c.SkipBuild, builderConfigs, images) + // TODO: Remove backwards compatibility block + if !c.EnableJibInit { + return printAnalyzeJSONNoJib(out, c.SkipBuild, pairs, unresolvedBuilderConfigs, unresolvedImages) + } + + return printAnalyzeJSON(out, c.SkipBuild, pairs, unresolvedBuilderConfigs, unresolvedImages) } // conditionally generate build artifacts - var pairs []builderImagePair if !c.SkipBuild { if len(builderConfigs) == 0 { return errors.New("one or more valid Dockerfiles must be present to build images with skaffold; please provide at least Dockerfile and try again or run `skaffold init --skip-build`") } - var unresolvedImages []string - pairs, builderConfigs, unresolvedImages = autoSelectBuilders(builderConfigs, images) - if c.CliArtifacts != nil { newPairs, err := processCliArtifacts(c.CliArtifacts) if err != nil { @@ -135,7 +141,7 @@ func DoInit(out io.Writer, c Config) error { } pairs = append(pairs, newPairs...) } else { - pairs = append(pairs, resolveBuilderImages(builderConfigs, unresolvedImages)...) + pairs = append(pairs, resolveBuilderImages(unresolvedBuilderConfigs, unresolvedImages)...) } } @@ -218,7 +224,7 @@ func autoSelectBuilders(builderConfigs []InitBuilder, images []string) ([]builde func detectBuilders(path string) ([]InitBuilder, error) { // Check for Dockerfile if docker.ValidateDockerfileFunc(path) { - results := []InitBuilder{docker.Docker(path)} + results := []InitBuilder{docker.Docker{File: path}} return results, nil } @@ -236,7 +242,7 @@ func processCliArtifacts(artifacts []string) ([]builderImagePair, error) { } pairs = append(pairs, builderImagePair{ - Builder: docker.Docker(parts[0]), + Builder: docker.Docker{File: parts[0]}, ImageName: parts[1], }) } @@ -335,20 +341,89 @@ func generateSkaffoldConfig(k Initializer, buildConfigPairs []builderImagePair) return pipelineStr, nil } -// TODO: make more flexible for non-docker builders -func printAnalyzeJSON(out io.Writer, skipBuild bool, dockerfiles []InitBuilder, images []string) error { - if !skipBuild && len(dockerfiles) == 0 { - return errors.New("one or more valid Dockerfiles must be present to build images with skaffold; please provide at least one Dockerfile and try again or run `skaffold init --skip-build`") +func printAnalyzeJSONNoJib(out io.Writer, skipBuild bool, pairs []builderImagePair, unresolvedBuilders []InitBuilder, unresolvedImages []string) error { + if !skipBuild && len(unresolvedBuilders) == 0 { + return errors.New("one or more valid Dockerfiles must be present to build images with skaffold; please provide at least one Dockerfile and try again, or run `skaffold init --skip-build`") } + a := struct { Dockerfiles []string `json:"dockerfiles,omitempty"` Images []string `json:"images,omitempty"` - }{ - Images: images, + }{Images: unresolvedImages} + + for _, pair := range pairs { + if pair.Builder.Name() == "Docker" { + a.Dockerfiles = append(a.Dockerfiles, pair.Builder.Path()) + } + a.Images = append(a.Images, pair.ImageName) + } + for _, config := range unresolvedBuilders { + if config.Name() == "Docker" { + a.Dockerfiles = append(a.Dockerfiles, config.Path()) + } + } + + contents, err := json.Marshal(a) + if err != nil { + return errors.Wrap(err, "marshalling contents") + } + _, err = out.Write(contents) + return err +} + +// printAnalyzeJSON takes the automatically resolved builder/image pairs, the unresolved images, and the unresolved builders, and generates +// a JSON string containing builder config information, +func printAnalyzeJSON(out io.Writer, skipBuild bool, pairs []builderImagePair, unresolvedBuilders []InitBuilder, unresolvedImages []string) error { + if !skipBuild && len(unresolvedBuilders) == 0 { + return errors.New("one or more valid Dockerfiles must be present to build images with skaffold; please provide at least one Dockerfile and try again, or run `skaffold init --skip-build`") + } + + // Build JSON output. Example schema is below: + // { + // "builders":[ + // { + // "name":"Docker", + // "payload":"path/to/Dockerfile" + // }, + // { + // "name":"Name of Builder", + // "payload": { // Payload structure may vary depending on builder type + // "path":"path/to/builder.config", + // "targetImage":"gcr.io/project/images", + // ... + // } + // }, + // ], + // "images":[ + // {"name":"gcr.io/project/images", "foundMatch":"true"}, // No need to prompt for this image since its builder was automatically resolved + // {"name":"another/image", "foundMatch":"false"}, + // ], + // } + // + // "builders" is the list of builder configurations, and contains a builder name and a builder-specific payload + // "images" contains an image name and a boolean that indicates whether a builder/image pair can be automatically resolved (true) or if it requires prompting (false) + type Builder struct { + Name string `json:"name,omitempty"` + Payload InitBuilder `json:"payload"` + } + type Image struct { + Name string `json:"name"` + FoundMatch bool `json:"foundMatch"` + } + a := struct { + Builders []Builder `json:"builders,omitempty"` + Images []Image `json:"images,omitempty"` + }{} + + for _, pair := range pairs { + a.Builders = append(a.Builders, Builder{Name: pair.Builder.Name(), Payload: pair.Builder}) + a.Images = append(a.Images, Image{Name: pair.ImageName, FoundMatch: true}) + } + for _, config := range unresolvedBuilders { + a.Builders = append(a.Builders, Builder{Name: config.Name(), Payload: config}) } - a.Dockerfiles = make([]string, len(dockerfiles)) - for i, dockerfile := range dockerfiles { - a.Dockerfiles[i] = dockerfile.Path() + for _, image := range unresolvedImages { + a.Images = append(a.Images, Image{Name: image, FoundMatch: false}) } contents, err := json.Marshal(a) diff --git a/pkg/skaffold/initializer/init_test.go b/pkg/skaffold/initializer/init_test.go index 02242dd39ec..b7c9a3f98bc 100644 --- a/pkg/skaffold/initializer/init_test.go +++ b/pkg/skaffold/initializer/init_test.go @@ -28,23 +28,75 @@ import ( func TestPrintAnalyzeJSON(t *testing.T) { tests := []struct { description string - dockerfiles []InitBuilder + pairs []builderImagePair + builders []InitBuilder images []string skipBuild bool shouldErr bool expected string }{ { - description: "dockerfile and image", - dockerfiles: []InitBuilder{docker.Docker("Dockerfile1"), docker.Docker("Dockerfile2")}, + description: "builders and images with pairs", + pairs: []builderImagePair{{docker.Docker{File: "Dockerfile1"}, "image1"}}, + builders: []InitBuilder{docker.Docker{File: "Dockerfile2"}}, + images: []string{"image2"}, + expected: `{"builders":[{"name":"Docker","payload":{"path":"Dockerfile1"}},{"name":"Docker","payload":{"path":"Dockerfile2"}}],"images":[{"name":"image1","foundMatch":true},{"name":"image2","foundMatch":false}]}`, + }, + { + description: "builders and images with no pairs", + builders: []InitBuilder{docker.Docker{File: "Dockerfile1"}, docker.Docker{File: "Dockerfile2"}}, images: []string{"image1", "image2"}, - expected: "{\"dockerfiles\":[\"Dockerfile1\",\"Dockerfile2\"],\"images\":[\"image1\",\"image2\"]}", + expected: `{"builders":[{"name":"Docker","payload":{"path":"Dockerfile1"}},{"name":"Docker","payload":{"path":"Dockerfile2"}}],"images":[{"name":"image1","foundMatch":false},{"name":"image2","foundMatch":false}]}`, }, { description: "no dockerfile, skip build", images: []string{"image1", "image2"}, skipBuild: true, - expected: "{\"images\":[\"image1\",\"image2\"]}"}, + expected: `{"images":[{"name":"image1","foundMatch":false},{"name":"image2","foundMatch":false}]}`, + }, + { + description: "no dockerfile", + images: []string{"image1", "image2"}, + shouldErr: true, + }, + { + description: "no dockerfiles or images", + shouldErr: true, + }, + } + for _, test := range tests { + testutil.Run(t, test.description, func(t *testutil.T) { + var out bytes.Buffer + + err := printAnalyzeJSON(&out, test.skipBuild, test.pairs, test.builders, test.images) + + t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, out.String()) + }) + } +} + +func TestPrintAnalyzeJSONNoJib(t *testing.T) { + tests := []struct { + description string + pairs []builderImagePair + builders []InitBuilder + images []string + skipBuild bool + shouldErr bool + expected string + }{ + { + description: "builders and images (backwards compatibility)", + builders: []InitBuilder{docker.Docker{File: "Dockerfile1"}, docker.Docker{File: "Dockerfile2"}}, + images: []string{"image1", "image2"}, + expected: `{"dockerfiles":["Dockerfile1","Dockerfile2"],"images":["image1","image2"]}`, + }, + { + description: "no dockerfile, skip build (backwards compatibility)", + images: []string{"image1", "image2"}, + skipBuild: true, + expected: `{"images":["image1","image2"]}`, + }, { description: "no dockerfile", images: []string{"image1", "image2"}, @@ -57,9 +109,9 @@ func TestPrintAnalyzeJSON(t *testing.T) { } for _, test := range tests { testutil.Run(t, test.description, func(t *testutil.T) { - out := bytes.NewBuffer([]byte{}) + var out bytes.Buffer - err := printAnalyzeJSON(out, test.skipBuild, test.dockerfiles, test.images) + err := printAnalyzeJSONNoJib(&out, test.skipBuild, test.pairs, test.builders, test.images) t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, out.String()) }) @@ -197,28 +249,28 @@ func TestResolveBuilderImages(t *testing.T) { }, { description: "don't prompt for single dockerfile and image", - buildConfigs: []InitBuilder{docker.Docker("Dockerfile1")}, + buildConfigs: []InitBuilder{docker.Docker{File: "Dockerfile1"}}, images: []string{"image1"}, shouldMakeChoice: false, expectedPairs: []builderImagePair{ { - Builder: docker.Docker("Dockerfile1"), + Builder: docker.Docker{File: "Dockerfile1"}, ImageName: "image1", }, }, }, { description: "prompt for multiple builders and images", - buildConfigs: []InitBuilder{docker.Docker("Dockerfile1"), docker.Docker("Dockerfile2")}, + buildConfigs: []InitBuilder{docker.Docker{File: "Dockerfile1"}, docker.Docker{File: "Dockerfile2"}}, images: []string{"image1", "image2"}, shouldMakeChoice: true, expectedPairs: []builderImagePair{ { - Builder: docker.Docker("Dockerfile1"), + Builder: docker.Docker{File: "Dockerfile1"}, ImageName: "image1", }, { - Builder: docker.Docker("Dockerfile2"), + Builder: docker.Docker{File: "Dockerfile2"}, ImageName: "image2", }, },