From b43c62e6e0d03d94925f1a5e525ffe628ce7e8c7 Mon Sep 17 00:00:00 2001 From: Dinesh Yeduguru Date: Fri, 16 Jun 2017 14:19:37 -0700 Subject: [PATCH] Add ability to specify SLS info to docker image (#149) * Add ability to specify SLS info to docker image * Base64 Encode the sls info string * simple commit to force update --- apps/distgo/cmd/dist/slsdist.go | 45 +------- apps/distgo/cmd/docker/build.go | 40 +++---- apps/distgo/cmd/docker/build_test.go | 124 ++++++++++++++++++++- apps/distgo/cmd/docker/order_specs.go | 2 +- apps/distgo/cmd/docker/order_specs_test.go | 22 ++-- apps/distgo/cmd/docker/publish.go | 4 +- apps/distgo/config/config.go | 71 ++++++++++-- apps/distgo/config/config_test.go | 64 +++++++++++ apps/distgo/params/docker.go | 105 +++++++++++++++-- apps/distgo/params/manifest.go | 63 +++++++++++ apps/distgo/params/product.go | 4 + apps/distgo/pkg/gocd_imports.json | 4 +- 12 files changed, 442 insertions(+), 106 deletions(-) create mode 100644 apps/distgo/params/manifest.go diff --git a/apps/distgo/cmd/dist/slsdist.go b/apps/distgo/cmd/dist/slsdist.go index 1323c033..f9cff0c6 100644 --- a/apps/distgo/cmd/dist/slsdist.go +++ b/apps/distgo/cmd/dist/slsdist.go @@ -19,12 +19,10 @@ import ( "io" "io/ioutil" "path" - "strings" "text/template" "github.com/palantir/pkg/specdir" "github.com/pkg/errors" - "gopkg.in/yaml.v2" "github.com/palantir/godel/apps/distgo/params" "github.com/palantir/godel/apps/distgo/pkg/binspec" @@ -261,7 +259,7 @@ func (s *slsDister) writeSLSManifest(buildSpec params.ProductBuildSpec, distCfg manifestTemplateString = manifestBuf.String() } else { var err error - manifestTemplateString, err = manifest(distCfg.Publish.GroupID, buildSpec.ProductName, buildSpec.ProductVersion, s.ProductType, s.ManifestExtensions) + manifestTemplateString, err = params.GetManifest(distCfg.Publish.GroupID, buildSpec.ProductName, buildSpec.ProductVersion, s.ProductType, s.ManifestExtensions) if err != nil { return errors.Wrapf(err, "failed to create manifest for SLS distribution") } @@ -272,47 +270,6 @@ func (s *slsDister) writeSLSManifest(buildSpec params.ProductBuildSpec, distCfg return nil } -type slsManifest struct { - ManifestVersion string `yaml:"manifest-version"` - ProductGroup string `yaml:"product-group"` - ProductName string `yaml:"product-name"` - ProductVersion string `yaml:"product-version"` - ProductType string `yaml:"product-type,omitempty"` - Extensions map[string]interface{} `yaml:"extensions,omitempty"` -} - -func manifest(groupID, name, version, productType string, extensions map[string]interface{}) (string, error) { - var missingRequired []string - if groupID == "" { - missingRequired = append(missingRequired, "group-id") - } - if name == "" { - missingRequired = append(missingRequired, "product-name") - } - if version == "" { - missingRequired = append(missingRequired, "product-version") - } - if len(missingRequired) > 0 { - return "", errors.Errorf("required properties were missing: " + strings.Join(missingRequired, ", ")) - } - - m := slsManifest{ - ManifestVersion: "1.0", - ProductGroup: groupID, - ProductName: name, - ProductVersion: version, - Extensions: extensions, - } - if productType != "" { - m.ProductType = productType - } - manifestBytes, err := yaml.Marshal(m) - if err != nil { - return "", errors.Wrapf(err, "failed to marshal %v as YAML", m) - } - return string(manifestBytes), nil -} - func (s *slsDister) writeSLSInitSh(buildSpec params.ProductBuildSpec, distCfg params.Dist, specDir specdir.SpecDir) error { var initShTemplateBytes []byte if s.InitShTemplateFile != "" { diff --git a/apps/distgo/cmd/docker/build.go b/apps/distgo/cmd/docker/build.go index fdc28b2d..e11aed77 100644 --- a/apps/distgo/cmd/docker/build.go +++ b/apps/distgo/cmd/docker/build.go @@ -18,7 +18,6 @@ import ( "fmt" "io" "os" - "os/exec" "path" "github.com/pkg/errors" @@ -39,7 +38,7 @@ func Build(cfg params.Project, wd string, baseRepo string, stdout io.Writer) err productsToBuildImage[productName] = struct{}{} } for _, image := range productSpec.DockerImages { - for _, dep := range image.Deps { + for _, dep := range image.Dependencies() { if isDist(dep.Type) { productsToDist[dep.Product] = struct{}{} } @@ -65,8 +64,8 @@ func Build(cfg params.Project, wd string, baseRepo string, stdout io.Writer) err // if base repo is specified, join it to each image's repo for i := range orderedSpecs { for j := range orderedSpecs[i].Spec.DockerImages { - orderedSpecs[i].Spec.DockerImages[j].Repository = path.Join(baseRepo, - orderedSpecs[i].Spec.DockerImages[j].Repository) + repo, _ := orderedSpecs[i].Spec.DockerImages[j].Coordinates() + orderedSpecs[i].Spec.DockerImages[j].SetRepository(path.Join(baseRepo, repo)) } } } @@ -86,20 +85,13 @@ func RunBuild(buildSpecsWithDeps []params.ProductBuildSpecWithDeps, stdout io.Wr } func buildImage(image params.DockerImage, buildSpecsWithDeps params.ProductBuildSpecWithDeps, specsMap map[string]params.ProductBuildSpecWithDeps, stdout io.Writer) error { - if image.Repository == "" { - image.Repository = buildSpecsWithDeps.Spec.ProductName - } - if image.Tag == "" { - image.Tag = buildSpecsWithDeps.Spec.ProductVersion - } - - fmt.Fprintf(stdout, "Building docker image for %s and tagging it as %s:%s\n", buildSpecsWithDeps.Spec.ProductName, image.Repository, image.Tag) + repo, tag := image.Coordinates() + fmt.Fprintf(stdout, "Building docker image for %s and tagging it as %s:%s\n", buildSpecsWithDeps.Spec.ProductName, repo, tag) - completeTag := fmt.Sprintf("%s:%s", image.Repository, image.Tag) - contextDir := path.Join(buildSpecsWithDeps.Spec.ProjectDir, image.ContextDir) + contextDir := path.Join(buildSpecsWithDeps.Spec.ProjectDir, image.ContextDirectory()) // link dependent dist artifacts into the context directory - for depProduct, depTypes := range image.Deps.ToMap() { + for depProduct, depTypes := range dockerDepsToMap(image.Dependencies()) { for depType, targetFile := range depTypes { if !isDist(depType) { continue @@ -134,16 +126,18 @@ func buildImage(image params.DockerImage, buildSpecsWithDeps params.ProductBuild } } - var args []string - args = append(args, "build") - args = append(args, "--tag", completeTag) - args = append(args, contextDir) + return image.Build(buildSpecsWithDeps) +} - buildCmd := exec.Command("docker", args...) - if output, err := buildCmd.CombinedOutput(); err != nil { - return errors.Wrap(err, fmt.Sprintf("docker build failed with error:\n%s\n", string(output))) +func dockerDepsToMap(deps []params.DockerDep) map[string]map[params.DockerDepType]string { + m := make(map[string]map[params.DockerDepType]string) + for _, dep := range deps { + if m[dep.Product] == nil { + m[dep.Product] = make(map[params.DockerDepType]string) + } + m[dep.Product][dep.Type] = dep.TargetFile } - return nil + return m } func isDist(dep params.DockerDepType) bool { diff --git a/apps/distgo/cmd/docker/build_test.go b/apps/distgo/cmd/docker/build_test.go index c7cb35ff..c110987e 100644 --- a/apps/distgo/cmd/docker/build_test.go +++ b/apps/distgo/cmd/docker/build_test.go @@ -16,6 +16,7 @@ package docker_test import ( "context" + "encoding/base64" "fmt" "io/ioutil" "math/rand" @@ -51,6 +52,9 @@ func main() { } ` dockerfile = `FROM alpine:3.5 +` + configFile = `test_key: 'test_value' +test_key_2: test_value_2 ` slsDepDockerFile = `FROM alpine:3.5 @@ -134,7 +138,7 @@ func TestDockerDist(t *testing.T) { }, }, DockerImages: []params.DockerImage{ - { + ¶ms.DefaultDockerImage{ Repository: fullRepoName("bar", pad), Tag: "0.1.0", ContextDir: "bar/docker", @@ -164,7 +168,7 @@ func TestDockerDist(t *testing.T) { }, }, DockerImages: []params.DockerImage{ - { + ¶ms.DefaultDockerImage{ Repository: fullRepoName("foo", pad), Tag: "0.1.0", ContextDir: "foo/docker", @@ -212,6 +216,120 @@ func TestDockerDist(t *testing.T) { require.True(t, len(images) > 0, "Case %d: %s", caseNum, name) }, }, + { + name: "sls docker dist", + setupProject: func(projectDir, pad string) error { + gittest.InitGitDir(t, projectDir) + // initialize foo + fooDir := path.Join(projectDir, "foo") + if err := os.Mkdir(fooDir, 0777); err != nil { + return err + } + if err := ioutil.WriteFile(path.Join(fooDir, "main.go"), []byte(testMain), 0644); err != nil { + return err + } + fooDockerDir := path.Join(fooDir, "docker") + if err = os.Mkdir(fooDockerDir, 0777); err != nil { + return err + } + if err = ioutil.WriteFile(path.Join(fooDockerDir, "Dockerfile"), []byte(dockerfile), 0777); err != nil { + return err + } + if err = ioutil.WriteFile(path.Join(fooDockerDir, params.ConfigurationFileName), []byte(configFile), 0777); err != nil { + return err + } + + // commit + gittest.CommitAllFiles(t, projectDir, "Commit") + return nil + }, + spec: func(projectDir string, pad string) []params.ProductBuildSpecWithDeps { + allSpec := make(map[string]params.ProductBuildSpec) + fooSpec := params.NewProductBuildSpec( + projectDir, + "foo", + git.ProjectInfo{ + Version: "0.1.0", + }, + params.Product{ + Build: params.Build{ + MainPkg: "./foo", + OSArchs: []osarch.OSArch{ + { + OS: "linux", + Arch: "amd64", + }, + }, + }, + DockerImages: []params.DockerImage{ + ¶ms.SLSDockerImage{ + DefaultDockerImage: params.DefaultDockerImage{ + Repository: fullRepoName("foo", pad), + Tag: "0.1.0", + ContextDir: "foo/docker", + Deps: []params.DockerDep{ + { + Product: "bar", + Type: params.DockerDepDocker, + TargetFile: "", + }, + }, + }, + ProuductType: "test_type", + GroupID: "com.palantir.godel", + Extensions: map[string]interface{}{ + "test_key": "test_value", + }, + }, + }, + }, + params.Project{ + GroupID: "com.test.group", + }, + ) + allSpec["foo"] = fooSpec + fooSpecWithDeps, err := params.NewProductBuildSpecWithDeps(fooSpec, allSpec) + require.NoError(t, err) + + return []params.ProductBuildSpecWithDeps{fooSpecWithDeps} + }, + cleanup: func(cli *dockercli.Client, projectDir, pad string) { + images := []string{fmt.Sprintf("%v:0.1.0", fullRepoName("foo", pad))} + err := removeImages(cli, images) + if err != nil { + t.Logf("Failed to remove images: %v", err) + } + }, + preDistAction: func(projectDir string, buildSpec []params.ProductBuildSpecWithDeps) { + gittest.CreateGitTag(t, projectDir, "0.1.0") + }, + validate: func(caseNum int, name string, pad string, cli *dockercli.Client) { + filter := filters.NewArgs() + filter.Add("reference", fmt.Sprintf("%v:0.1.0", fullRepoName("foo", pad))) + images, err := cli.ImageList(context.Background(), types.ImageListOptions{Filters: filter}) + require.NoError(t, err, "Case %d: %s", caseNum, name) + require.True(t, len(images) > 0, "Case %d: %s", caseNum, name) + image := images[0] + inspect, _, err := cli.ImageInspectWithRaw(context.Background(), image.ID) + require.NoError(t, err, "Case %d: %s", caseNum, name) + actualManifestEncoded, ok := inspect.Config.Labels[params.ManifestLabel] + require.True(t, ok, "Case %d: %s", caseNum, name) + actualManifest, err := base64.StdEncoding.DecodeString(actualManifestEncoded) + require.NoError(t, err, "Case %d: %s", caseNum, name) + expectedManifest := "manifest-version: \"1.0\"\n" + + "product-group: com.palantir.godel\n" + + "product-name: foo\n" + + "product-version: 0.1.0\n" + + "product-type: test_type\n" + + "extensions:\n test_key: test_value\n" + require.Equal(t, expectedManifest, string(actualManifest), "Case %d: %s", caseNum, name) + actualConfigEncoded, ok := inspect.Config.Labels[params.ConfigurationLabel] + require.True(t, ok, "Case %d: %s", caseNum, name) + actualConfig, err := base64.StdEncoding.DecodeString(actualConfigEncoded) + require.NoError(t, err, "Case %d: %s", caseNum, name) + require.Equal(t, configFile, string(actualConfig), "Case %d: %s", caseNum, name) + }, + }, { name: "docker dist with dependent sls dist", setupProject: func(projectDir, pad string) error { @@ -296,7 +414,7 @@ func TestDockerDist(t *testing.T) { }, }, DockerImages: []params.DockerImage{ - { + ¶ms.DefaultDockerImage{ Repository: fullRepoName("bar", pad), Tag: "0.1.0", ContextDir: "bar/docker", diff --git a/apps/distgo/cmd/docker/order_specs.go b/apps/distgo/cmd/docker/order_specs.go index 7b5844d6..45a0f66d 100644 --- a/apps/distgo/cmd/docker/order_specs.go +++ b/apps/distgo/cmd/docker/order_specs.go @@ -35,7 +35,7 @@ func OrderBuildSpecs(specsWithDeps []params.ProductBuildSpecWithDeps) ([]params. graph[product] = make(map[string]struct{}) } for _, curImage := range curSpec.Spec.DockerImages { - for depProduct, depTypes := range curImage.Deps.ToMap() { + for depProduct, depTypes := range dockerDepsToMap(curImage.Dependencies()) { if !hasDockerDep(depTypes) { // only add edge if its a docker image dependency continue diff --git a/apps/distgo/cmd/docker/order_specs_test.go b/apps/distgo/cmd/docker/order_specs_test.go index 6253f65a..2167737e 100644 --- a/apps/distgo/cmd/docker/order_specs_test.go +++ b/apps/distgo/cmd/docker/order_specs_test.go @@ -24,12 +24,12 @@ import ( "github.com/palantir/godel/apps/distgo/params" ) -func generateSpec(product string, deps params.DockerDeps) params.ProductBuildSpec { +func generateSpec(product string, deps []params.DockerDep) params.ProductBuildSpec { return params.ProductBuildSpec{ ProductName: product, Product: params.Product{ DockerImages: []params.DockerImage{ - { + ¶ms.DefaultDockerImage{ Deps: deps, }, }, @@ -38,31 +38,31 @@ func generateSpec(product string, deps params.DockerDeps) params.ProductBuildSpe } func TestOrderBuildSpecs(t *testing.T) { - A := generateSpec("A", params.DockerDeps{ + A := generateSpec("A", []params.DockerDep{ {Product: "B", Type: params.DockerDepDocker, TargetFile: ""}, {Product: "C", Type: params.DockerDepDocker, TargetFile: ""}, }) - B := generateSpec("B", params.DockerDeps{ + B := generateSpec("B", []params.DockerDep{ {Product: "D", Type: params.DockerDepDocker, TargetFile: ""}, }) - C := generateSpec("C", params.DockerDeps{ + C := generateSpec("C", []params.DockerDep{ {Product: "D", Type: params.DockerDepDocker, TargetFile: ""}, }) - D := generateSpec("D", params.DockerDeps{}) - E := generateSpec("E", params.DockerDeps{ + D := generateSpec("D", []params.DockerDep{}) + E := generateSpec("E", []params.DockerDep{ {Product: "DepE", Type: params.DockerDepDocker, TargetFile: ""}, }) - DepE := generateSpec("DepE", params.DockerDeps{ + DepE := generateSpec("DepE", []params.DockerDep{ {Product: "E", Type: params.DockerDepDocker, TargetFile: ""}, }) - X := generateSpec("X", params.DockerDeps{ + X := generateSpec("X", []params.DockerDep{ {Product: "Y", Type: params.DockerDepDocker, TargetFile: ""}, }) - Y := generateSpec("Y", params.DockerDeps{ + Y := generateSpec("Y", []params.DockerDep{ {Product: "Z", Type: params.DockerDepDocker, TargetFile: ""}, }) - Z := generateSpec("Z", params.DockerDeps{}) + Z := generateSpec("Z", []params.DockerDep{}) for _, testcase := range []struct { input []params.ProductBuildSpecWithDeps diff --git a/apps/distgo/cmd/docker/publish.go b/apps/distgo/cmd/docker/publish.go index b061b7ea..4e110673 100644 --- a/apps/distgo/cmd/docker/publish.go +++ b/apps/distgo/cmd/docker/publish.go @@ -41,11 +41,11 @@ func Publish(cfg params.Project, wd string, baseRepo string, stdout io.Writer) e for _, specWithDeps := range buildSpecsWithDeps { versionTag := specWithDeps.Spec.ProductVersion for _, image := range specWithDeps.Spec.DockerImages { - repo := image.Repository + repo, tag := image.Coordinates() if baseRepo != "" { repo = path.Join(baseRepo, repo) } - buildTag := fmt.Sprintf("%s:%s", repo, image.Tag) + buildTag := fmt.Sprintf("%s:%s", repo, tag) publishTag := fmt.Sprintf("%s:%s", repo, versionTag) if err := tagImage(buildTag, publishTag); err != nil { return err diff --git a/apps/distgo/config/config.go b/apps/distgo/config/config.go index fb8800ea..7c8e9792 100644 --- a/apps/distgo/config/config.go +++ b/apps/distgo/config/config.go @@ -257,10 +257,22 @@ type DockerDep struct { } type DockerImage struct { - Repository string `yaml:"repository" json:"repository"` - Tag string `yaml:"tag" json:"tag"` - ContextDir string `yaml:"context-dir" json:"context-dir"` - Deps []DockerDep `yaml:"dependencies" json:"dependencies"` + Repository string `yaml:"repository" json:"repository"` + Tag string `yaml:"tag" json:"tag"` + ContextDir string `yaml:"context-dir" json:"context-dir"` + Deps []DockerDep `yaml:"dependencies" json:"dependencies"` + Info DockerImageInfo `yaml:"info" json:"info"` +} + +type DockerImageInfo struct { + Type string `yaml:"type" json:"type"` + Data interface{} `yaml:"data" json:"data"` +} + +type SLSDockerImageInfo struct { + GroupID string `yaml:"group-id" json:"group-id"` + ProuductType string `yaml:"product-type" json:"product-type"` + Extensions map[string]interface{} `yaml:"manifest-extensions" json:"manifest-extensions"` } type Publish struct { @@ -552,12 +564,37 @@ func (cfg *RPMDist) ToParams() params.RPMDistInfo { } } +func (cfg *DockerImageInfo) UnmarshalYAML(unmarshal func(interface{}) error) error { + // unmarshal type alias (uses default unmarshal strategy) + type rawImageInfoConfigAlias DockerImageInfo + var rawAliasConfig rawImageInfoConfigAlias + if err := unmarshal(&rawAliasConfig); err != nil { + return err + } + + rawImageInfoConfig := DockerImageInfo(rawAliasConfig) + switch rawImageInfoConfig.Type { + case "sls": + type typedRawConfig struct { + Type string + Data SLSDockerImageInfo + } + var rawSLS typedRawConfig + if err := unmarshal(&rawSLS); err != nil { + return err + } + rawImageInfoConfig.Data = rawSLS.Data + } + *cfg = rawImageInfoConfig + return nil +} + func (cfg *DockerImage) ToParams() (params.DockerImage, error) { - var deps params.DockerDeps + var deps []params.DockerDep for _, dep := range cfg.Deps { depType, err := params.ToDockerDepType(dep.Type) if err != nil { - return params.DockerImage{}, nil + return nil, nil } deps = append(deps, params.DockerDep{ Product: dep.Product, @@ -565,12 +602,30 @@ func (cfg *DockerImage) ToParams() (params.DockerImage, error) { TargetFile: dep.TargetFile, }) } - return params.DockerImage{ + dockerImage := params.DefaultDockerImage{ Repository: cfg.Repository, Tag: cfg.Tag, ContextDir: cfg.ContextDir, Deps: deps, - }, nil + } + switch cfg.Info.Type { + case "sls": + slsInfo := SLSDockerImageInfo{} + if err := mapstructure.Decode(cfg.Info.Data, &slsInfo); err != nil { + return nil, errors.Wrap(err, "Failed to unmarshal the sls image info") + } + return ¶ms.SLSDockerImage{ + DefaultDockerImage: dockerImage, + ProuductType: slsInfo.ProuductType, + GroupID: slsInfo.GroupID, + Extensions: slsInfo.Extensions, + }, nil + case "": + return &dockerImage, nil + default: + return nil, errors.New("Unknown Info type specified") + + } } func (cfg *Publish) ToParams() params.Publish { diff --git a/apps/distgo/config/config_test.go b/apps/distgo/config/config_test.go index f136aa01..effa8c7c 100644 --- a/apps/distgo/config/config_test.go +++ b/apps/distgo/config/config_test.go @@ -378,6 +378,70 @@ echo "main.year=$YEAR" } }, }, + { + yml: ` + products: + test: + docker: + - + repository: docker.hub/test + tag: test + context-dir: context/dir/path + dependencies: + - + product: dep1 + type: sls + target-file: dep1-sls.tgz + - + product: dep2 + type: rpm + target-file: dep2-rpm.tgz + info: + type: sls + data: + group-id: com.palantir.test + product-type: test-type + manifest-extensions: + test_key: test_value + `, + want: func() config.Project { + return config.Project{ + Products: map[string]config.Product{ + "test": { + DockerImages: []config.DockerImage{ + { + Repository: "docker.hub/test", + Tag: "test", + ContextDir: "context/dir/path", + Deps: []config.DockerDep{ + { + Product: "dep1", + Type: "sls", + TargetFile: "dep1-sls.tgz", + }, + { + Product: "dep2", + Type: "rpm", + TargetFile: "dep2-rpm.tgz", + }, + }, + Info: config.DockerImageInfo{ + Type: "sls", + Data: config.SLSDockerImageInfo{ + GroupID: "com.palantir.test", + ProuductType: "test-type", + Extensions: map[string]interface{}{ + "test_key": "test_value", + }, + }, + }, + }, + }, + }, + }, + } + }, + }, } { // load config got, err := config.LoadRawConfig(unindent(currCase.yml), currCase.json) diff --git a/apps/distgo/params/docker.go b/apps/distgo/params/docker.go index 16a06a31..f0efae40 100644 --- a/apps/distgo/params/docker.go +++ b/apps/distgo/params/docker.go @@ -15,6 +15,13 @@ package params import ( + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "github.com/pkg/errors" ) @@ -25,6 +32,10 @@ const ( DockerDepBin DockerDepType = "bin" DockerDepRPM DockerDepType = "rpm" DockerDepDocker DockerDepType = "docker" + + ManifestLabel = "com.palantir.sls.manifest" + ConfigurationLabel = "com.palantir.sls.configuration" + ConfigurationFileName = "configuration.yml" ) type DockerDep struct { @@ -32,20 +43,17 @@ type DockerDep struct { Type DockerDepType TargetFile string } -type DockerDeps []DockerDep -func (d DockerDeps) ToMap() map[string]map[DockerDepType]string { - m := make(map[string]map[DockerDepType]string) - for _, dep := range d { - if m[dep.Product] == nil { - m[dep.Product] = make(map[DockerDepType]string) - } - m[dep.Product][dep.Type] = dep.TargetFile - } - return m +type DockerImage interface { + SetRepository(repo string) + SetDefaults(repo, tag string) + Coordinates() (string, string) + ContextDirectory() string + Dependencies() []DockerDep + Build(buildSpec ProductBuildSpecWithDeps) error } -type DockerImage struct { +type DefaultDockerImage struct { // Repository and Tag are the part of the image coordinates. // For example, in alpine:latest, alpine is the repository // and the latest is the tag @@ -60,7 +68,80 @@ type DockerImage struct { // This will be used to order the dist tasks such that all the dependent // products' dist tasks will be executed first, after which the dist tasks for the // current product are executed. - Deps DockerDeps + Deps []DockerDep +} + +type SLSDockerImage struct { + DefaultDockerImage + GroupID string + ProuductType string + Extensions map[string]interface{} +} + +func (sdi *SLSDockerImage) Build(buildSpec ProductBuildSpecWithDeps) error { + contextDir := path.Join(buildSpec.Spec.ProjectDir, sdi.ContextDir) + configFile := path.Join(contextDir, ConfigurationFileName) + var args []string + args = append(args, "build") + args = append(args, "--tag", fmt.Sprintf("%s:%s", sdi.Repository, sdi.Tag)) + manifest, err := GetManifest(sdi.GroupID, buildSpec.Spec.ProductName, buildSpec.Spec.ProductVersion, sdi.ProuductType, sdi.Extensions) + if err != nil { + return errors.Wrap(err, "Failed to get manifest for the image") + } + args = append(args, "--label", fmt.Sprintf("%s=%s", ManifestLabel, base64.StdEncoding.EncodeToString([]byte(manifest)))) + if _, err := os.Stat(configFile); err == nil { + content, err := ioutil.ReadFile(configFile) + if err != nil { + return errors.Wrapf(err, "Failed to read the file %s", configFile) + } + args = append(args, "--label", fmt.Sprintf("%s=%s", ConfigurationLabel, base64.StdEncoding.EncodeToString(content))) + } + args = append(args, contextDir) + + buildCmd := exec.Command("docker", args...) + if output, err := buildCmd.CombinedOutput(); err != nil { + return errors.Wrap(err, fmt.Sprintf("docker build failed with error:\n%s\n", string(output))) + } + return nil +} + +func (di *DefaultDockerImage) ContextDirectory() string { + return di.ContextDir +} + +func (di *DefaultDockerImage) Dependencies() []DockerDep { + return di.Deps +} + +func (di *DefaultDockerImage) Build(buildSpec ProductBuildSpecWithDeps) error { + contextDir := path.Join(buildSpec.Spec.ProjectDir, di.ContextDir) + var args []string + args = append(args, "build") + args = append(args, "--tag", fmt.Sprintf("%s:%s", di.Repository, di.Tag)) + args = append(args, contextDir) + + buildCmd := exec.Command("docker", args...) + if output, err := buildCmd.CombinedOutput(); err != nil { + return errors.Wrap(err, fmt.Sprintf("docker build failed with error:\n%s\n", string(output))) + } + return nil +} + +func (di *DefaultDockerImage) Coordinates() (string, string) { + return di.Repository, di.Tag +} + +func (di *DefaultDockerImage) SetDefaults(repo, tag string) { + if di.Repository == "" { + di.Repository = repo + } + if di.Tag == "" { + di.Tag = tag + } +} + +func (di *DefaultDockerImage) SetRepository(repo string) { + di.Repository = repo } func ToDockerDepType(dep string) (DockerDepType, error) { diff --git a/apps/distgo/params/manifest.go b/apps/distgo/params/manifest.go new file mode 100644 index 00000000..e4cf53d5 --- /dev/null +++ b/apps/distgo/params/manifest.go @@ -0,0 +1,63 @@ +// Copyright 2016 Palantir Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package params + +import ( + "strings" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +type slsManifest struct { + ManifestVersion string `yaml:"manifest-version"` + ProductGroup string `yaml:"product-group"` + ProductName string `yaml:"product-name"` + ProductVersion string `yaml:"product-version"` + ProductType string `yaml:"product-type,omitempty"` + Extensions map[string]interface{} `yaml:"extensions,omitempty"` +} + +func GetManifest(groupID, name, version, productType string, extensions map[string]interface{}) (string, error) { + var missingRequired []string + if groupID == "" { + missingRequired = append(missingRequired, "group-id") + } + if name == "" { + missingRequired = append(missingRequired, "product-name") + } + if version == "" { + missingRequired = append(missingRequired, "product-version") + } + if len(missingRequired) > 0 { + return "", errors.Errorf("required properties were missing: %s", strings.Join(missingRequired, ", ")) + } + + m := slsManifest{ + ManifestVersion: "1.0", + ProductGroup: groupID, + ProductName: name, + ProductVersion: version, + Extensions: extensions, + } + if productType != "" { + m.ProductType = productType + } + manifestBytes, err := yaml.Marshal(m) + if err != nil { + return "", errors.Wrapf(err, "failed to marshal %v as YAML", m) + } + return string(manifestBytes), nil +} diff --git a/apps/distgo/params/product.go b/apps/distgo/params/product.go index e7f36a1b..a50603d8 100644 --- a/apps/distgo/params/product.go +++ b/apps/distgo/params/product.go @@ -143,6 +143,10 @@ func NewProductBuildSpec(projectDir, productName string, gitProductInfo git.Proj } } + for i := range buildSpec.DockerImages { + buildSpec.DockerImages[i].SetDefaults(productName, gitProductInfo.Version) + } + return buildSpec } diff --git a/apps/distgo/pkg/gocd_imports.json b/apps/distgo/pkg/gocd_imports.json index 261476b0..553d3dd6 100644 --- a/apps/distgo/pkg/gocd_imports.json +++ b/apps/distgo/pkg/gocd_imports.json @@ -2,8 +2,8 @@ "imports": [ { "path": "github.com/palantir/godel/apps/distgo/params", - "numGoFiles": 7, - "numImportedGoFiles": 9, + "numGoFiles": 8, + "numImportedGoFiles": 22, "importedFrom": [ "github.com/palantir/godel/apps/distgo/pkg/script" ]