diff --git a/cmd/artifact/artifact.go b/cmd/artifact/artifact.go new file mode 100644 index 0000000..3e4ecf9 --- /dev/null +++ b/cmd/artifact/artifact.go @@ -0,0 +1,76 @@ +package artifact + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +const ( + dockerArtifactV1 string = "docker/v1" +) + +type RegistryTypeEnum string + +const ( + Docker RegistryTypeEnum = "Docker" + ECR RegistryTypeEnum = "ECR" + GCR RegistryTypeEnum = "GCR" +) + +type ( + Image struct { + Image string `json:"image"` + Digest string `json:"digest"` + } + Data struct { + RegistryType RegistryTypeEnum `json:"registryType"` + RegistryUrl string `json:"registryUrl"` + Images []Image `json:"images"` + } + DockerArtifact struct { + Kind string `json:"kind"` + Data Data `json:"data"` + } +) + +func WritePluginArtifactFile(registryType RegistryTypeEnum, artifactFilePath, registryUrl, imageName, digest string, tags []string) error { + var images []Image + for _, tag := range tags { + images = append(images, Image{ + Image: fmt.Sprintf("%s:%s", imageName, tag), + Digest: digest, + }) + } + data := Data{ + RegistryType: registryType, + RegistryUrl: registryUrl, + Images: images, + } + + dockerArtifact := DockerArtifact{ + Kind: dockerArtifactV1, + Data: data, + } + + b, err := json.MarshalIndent(dockerArtifact, "", "\t") + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to marshal output %+v", dockerArtifact)) + } + + dir := filepath.Dir(artifactFilePath) + err = os.MkdirAll(dir, 0644) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to create %s directory for artifact file", dir)) + } + + err = ioutil.WriteFile(artifactFilePath, b, 0644) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to write artifact to artifact file %s", artifactFilePath)) + } + return nil +} diff --git a/cmd/artifact/artifact.json b/cmd/artifact/artifact.json new file mode 100644 index 0000000..eff6dbc --- /dev/null +++ b/cmd/artifact/artifact.json @@ -0,0 +1,17 @@ +{ + "kind": "docker/v1", + "data": { + "registryType": "Docker", + "registryUrl": "https://index.docker.io/", + "images": [ + { + "image": "image:a1", + "digest": "sha256:22332233" + }, + { + "image": "image:latest", + "digest": "sha256:22332233" + } + ] + } +} \ No newline at end of file diff --git a/cmd/artifact/artifact_test.go b/cmd/artifact/artifact_test.go new file mode 100644 index 0000000..33cdc78 --- /dev/null +++ b/cmd/artifact/artifact_test.go @@ -0,0 +1,38 @@ +package artifact + +import ( + "io/ioutil" + "testing" +) + +func TestWritePluginArtifactFile(t *testing.T) { + + testFile := t.TempDir() + "got.json" + + err := WritePluginArtifactFile(Docker, testFile, "https://index.docker.io/", "image", "sha256:22332233", []string{"a1", "latest"}) + if err != nil { + t.Error(err) + t.FailNow() + } + + gotBytes, err := ioutil.ReadFile(testFile) + if err != nil { + t.Error(err) + t.FailNow() + } + + wantBytes, err := ioutil.ReadFile("./artifact.json") + if err != nil { + t.Error(err) + t.FailNow() + } + + got := string(gotBytes) + want := string(wantBytes) + + if got != want { + t.Logf("got:%s", got) + t.Logf("want:%s", want) + t.FailNow() + } +} diff --git a/cmd/kaniko-docker/main.go b/cmd/kaniko-docker/main.go index 990c5c1..5547fde 100644 --- a/cmd/kaniko-docker/main.go +++ b/cmd/kaniko-docker/main.go @@ -12,6 +12,7 @@ import ( "github.com/urfave/cli" kaniko "github.com/drone/drone-kaniko" + "github.com/drone/drone-kaniko/cmd/artifact" ) const ( @@ -21,6 +22,8 @@ const ( v1Registry string = "https://index.docker.io/v1/" // Default registry v2Registry string = "https://index.docker.io/v2/" // v2 registry is not supported + + defaultDigestFile string = "/kaniko/digest-file" ) var ( @@ -119,6 +122,11 @@ func main() { Usage: "Cache timeout in hours. Defaults to two weeks.", EnvVar: "PLUGIN_CACHE_TTL", }, + cli.StringFlag{ + Name: "artifact-file", + Usage: "Artifact file location that will be generated by the plugin. This file will include information of docker images that are uploaded by the plugin.", + EnvVar: "PLUGIN_ARTIFACT_FILE", + }, } if err := app.Run(os.Args); err != nil { @@ -146,6 +154,14 @@ func run(c *cli.Context) error { EnableCache: c.Bool("enable-cache"), CacheRepo: c.String("cache-repo"), CacheTTL: c.Int("cache-ttl"), + DigestFile: defaultDigestFile, + }, + Artifact: kaniko.Artifact{ + Tags: c.StringSlice("tags"), + Repo: c.String("repo"), + Registry: c.String("registry"), + ArtifactFile: c.String("artifact-file"), + RegistryType: artifact.Docker, }, } return plugin.Exec() diff --git a/cmd/kaniko-ecr/main.go b/cmd/kaniko-ecr/main.go index f46f68c..4d82704 100644 --- a/cmd/kaniko-ecr/main.go +++ b/cmd/kaniko-ecr/main.go @@ -11,12 +11,15 @@ import ( "github.com/urfave/cli" kaniko "github.com/drone/drone-kaniko" + "github.com/drone/drone-kaniko/cmd/artifact" ) const ( accessKeyEnv string = "AWS_ACCESS_KEY_ID" secretKeyEnv string = "AWS_SECRET_ACCESS_KEY" dockerConfigPath string = "/kaniko/.docker/config.json" + + defaultDigestFile string = "/kaniko/digest-file" ) var ( @@ -109,6 +112,11 @@ func main() { Usage: "Cache timeout in hours. Defaults to two weeks.", EnvVar: "PLUGIN_CACHE_TTL", }, + cli.StringFlag{ + Name: "artifact-file", + Usage: "Artifact file location that will be generated by the plugin. This file will include information of docker images that are uploaded by the plugin.", + EnvVar: "PLUGIN_ARTIFACT_FILE", + }, } if err := app.Run(os.Args); err != nil { @@ -135,6 +143,14 @@ func run(c *cli.Context) error { EnableCache: c.Bool("enable-cache"), CacheRepo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("cache-repo")), CacheTTL: c.Int("cache-ttl"), + DigestFile: defaultDigestFile, + }, + Artifact: kaniko.Artifact{ + Tags: c.StringSlice("tags"), + Repo: c.String("repo"), + Registry: c.String("registry"), + ArtifactFile: c.String("artifact-file"), + RegistryType: artifact.ECR, }, } return plugin.Exec() @@ -142,7 +158,7 @@ func run(c *cli.Context) error { func setupECRAuth(accessKey, secretKey, registry string) error { if registry == "" { - return fmt.Errorf("Registry must be specified") + return fmt.Errorf("registry must be specified") } // If IAM role is used, access key & secret key are not required diff --git a/cmd/kaniko-gcr/main.go b/cmd/kaniko-gcr/main.go index 6b2ab4d..20aac4d 100644 --- a/cmd/kaniko-gcr/main.go +++ b/cmd/kaniko-gcr/main.go @@ -11,12 +11,15 @@ import ( "github.com/urfave/cli" kaniko "github.com/drone/drone-kaniko" + "github.com/drone/drone-kaniko/cmd/artifact" ) const ( // GCR JSON key file path gcrKeyPath string = "/kaniko/config.json" gcrEnvVariable string = "GOOGLE_APPLICATION_CREDENTIALS" + + defaultDigestFile string = "/kaniko/digest-file" ) var ( @@ -105,6 +108,11 @@ func main() { Usage: "Cache timeout in hours. Defaults to two weeks.", EnvVar: "PLUGIN_CACHE_TTL", }, + cli.StringFlag{ + Name: "artifact-file", + Usage: "Artifact file location that will be generated by the plugin. This file will include information of docker images that are uploaded by the plugin.", + EnvVar: "PLUGIN_ARTIFACT_FILE", + }, } if err := app.Run(os.Args); err != nil { @@ -135,6 +143,14 @@ func run(c *cli.Context) error { EnableCache: c.Bool("enable-cache"), CacheRepo: fmt.Sprintf("%s/%s", c.String("registry"), c.String("cache-repo")), CacheTTL: c.Int("cache-ttl"), + DigestFile: defaultDigestFile, + }, + Artifact: kaniko.Artifact{ + Tags: c.StringSlice("tags"), + Repo: c.String("repo"), + Registry: c.String("registry"), + ArtifactFile: c.String("artifact-file"), + RegistryType: artifact.GCR, }, } return plugin.Exec() diff --git a/kaniko.go b/kaniko.go index 1ca0351..d9be685 100644 --- a/kaniko.go +++ b/kaniko.go @@ -2,9 +2,12 @@ package kaniko import ( "fmt" + "io/ioutil" "os" "os/exec" "strings" + + "github.com/drone/drone-kaniko/cmd/artifact" ) type ( @@ -22,11 +25,22 @@ type ( EnableCache bool // Whether to enable kaniko cache CacheRepo string // Remote repository that will be used to store cached layers CacheTTL int // Cache timeout in hours + DigestFile string // Digest file location + } + // Artifact defines content of artifact file + Artifact struct { + Tags []string // Docker artifact tags + Repo string // Docker artifact repository + Registry string // Docker artifact registry + RegistryType artifact.RegistryTypeEnum // Rocker artifact registry type + ArtifactFile string // Artifact file location + } // Plugin defines the Docker plugin parameters. Plugin struct { - Build Build // Docker build configuration + Build Build // Docker build configuration + Artifact Artifact // Artifact file content } ) @@ -82,13 +96,32 @@ func (p Plugin) Exec() error { cmdArgs = append(cmdArgs, fmt.Sprintf("--cache-ttl=%d", p.Build.CacheTTL)) } + if p.Build.DigestFile != "" { + cmdArgs = append(cmdArgs, fmt.Sprintf("--digest-file=%s", p.Build.DigestFile)) + } + cmd := exec.Command("/kaniko/executor", cmdArgs...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr trace(cmd) err := cmd.Run() - return err + if err != nil { + return err + } + + if p.Build.DigestFile != "" && p.Artifact.ArtifactFile != "" { + content, err := ioutil.ReadFile(p.Build.DigestFile) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to read digest file contents at path: %s with error: %s\n", p.Build.DigestFile, err) + } + err = artifact.WritePluginArtifactFile(p.Artifact.RegistryType, p.Artifact.ArtifactFile, p.Artifact.Registry, p.Artifact.Repo, string(content), p.Artifact.Tags) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to write plugin artifact file at path: %s with error: %s\n", p.Artifact.ArtifactFile, err) + } + } + + return nil } // trace writes each command to stdout with the command wrapped in an xml