commit fe707ccfb1209b19041f1a710faba9fc5e1a1a65 Author: Shubham Agrawal Date: Thu May 13 21:07:16 2021 +0530 Buildah docker build & push diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..3f95605 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,9 @@ + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ + diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 0000000..5450fa7 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,69 @@ +repository: + name: drone-buildah + description: Drone plugin for publishing Docker images via buildah tool + homepage: http://plugins.drone.io/drone-plugins/drone-buildah + topics: drone, drone-plugin + + private: false + has_issues: true + has_wiki: false + has_downloads: false + + default_branch: master + + allow_squash_merge: true + allow_merge_commit: true + allow_rebase_merge: true + +labels: + - name: bug + color: d73a4a + description: Something isn't working + - name: duplicate + color: cfd3d7 + description: This issue or pull request already exists + - name: enhancement + color: a2eeef + description: New feature or request + - name: good first issue + color: 7057ff + description: Good for newcomers + - name: help wanted + color: 008672 + description: Extra attention is needed + - name: invalid + color: e4e669 + description: This doesn't seem right + - name: question + color: d876e3 + description: Further information is requested + - name: renovate + color: e99695 + description: Automated action from Renovate + - name: wontfix + color: ffffff + description: This will not be worked on + +teams: + - name: Admins + permission: admin + +branches: + - name: master + protection: + required_pull_request_reviews: + required_approving_review_count: 1 + dismiss_stale_reviews: false + require_code_owner_reviews: false + dismissal_restrictions: + teams: + - Admins + required_status_checks: + strict: true + contexts: + - continuous-integration/drone/pr + enforce_admins: false + restrictions: + users: [] + teams: + - Admins diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5077f86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +release +coverage.out +vendor diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ab548a --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# drone-buildah + +[![Build Status](http://cloud.drone.io/api/badges/drone-plugins/drone-buildah/status.svg)](http://cloud.drone.io/drone-plugins/drone-buildah) +[![Gitter chat](https://badges.gitter.im/drone/drone.png)](https://gitter.im/drone/drone) +[![Join the discussion at https://discourse.drone.io](https://img.shields.io/badge/discourse-forum-orange.svg)](https://discourse.drone.io) +[![Drone questions at https://stackoverflow.com](https://img.shields.io/badge/drone-stackoverflow-orange.svg)](https://stackoverflow.com/questions/tagged/drone.io) +[![](https://images.microbadger.com/badges/image/plugins/docker.svg)](https://microbadger.com/images/plugins/docker "Get your own image badge on microbadger.com") +[![Go Doc](https://godoc.org/github.com/drone-plugins/drone-buildah?status.svg)](http://godoc.org/github.com/drone-plugins/drone-buildah) +[![Go Report](https://goreportcard.com/badge/github.com/drone-plugins/drone-buildah)](https://goreportcard.com/report/github.com/drone-plugins/drone-buildah) + +Drone plugin uses buildah to build and publish Docker images to a container registry. For the usage information and a listing of the available options please take a look at [the docs](http://plugins.drone.io/drone-plugins/drone-buildah/). + +## Build + +Build the binaries with the following commands: + +```console +export GOOS=linux +export GOARCH=amd64 +export CGO_ENABLED=0 +export GO111MODULE=on + +go build -v -a -tags netgo -o release/linux/amd64/drone-docker ./cmd/drone-docker +go build -v -a -tags netgo -o release/linux/amd64/drone-gcr ./cmd/drone-gcr +go build -v -a -tags netgo -o release/linux/amd64/drone-ecr ./cmd/drone-ecr +go build -v -a -tags netgo -o release/linux/amd64/drone-acr ./cmd/drone-acr +go build -v -a -tags netgo -o release/linux/amd64/drone-heroku ./cmd/drone-heroku +``` + +## Docker + +Build the Docker images with the following commands: + +```console +docker build \ + --label org.label-schema.build-date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --label org.label-schema.vcs-ref=$(git rev-parse --short HEAD) \ + --file docker/docker/Dockerfile.linux.amd64 --tag plugins/buildah-docker . + +docker build \ + --label org.label-schema.build-date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --label org.label-schema.vcs-ref=$(git rev-parse --short HEAD) \ + --file docker/gcr/Dockerfile.linux.amd64 --tag plugins/buildah-gcr . + +docker build \ + --label org.label-schema.build-date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --label org.label-schema.vcs-ref=$(git rev-parse --short HEAD) \ + --file docker/ecr/Dockerfile.linux.amd64 --tag plugins/buildah-ecr . + +docker build \ + --label org.label-schema.build-date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --label org.label-schema.vcs-ref=$(git rev-parse --short HEAD) \ + --file docker/acr/Dockerfile.linux.amd64 --tag plugins/buildah-acr . + +docker build \ + --label org.label-schema.build-date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ + --label org.label-schema.vcs-ref=$(git rev-parse --short HEAD) \ + --file docker/heroku/Dockerfile.linux.amd64 --tag plugins/buildah-heroku . +``` + +## Usage + +```console +docker run --rm \ + -e PLUGIN_TAG=latest \ + -e PLUGIN_REPO=octocat/hello-world \ + -e DRONE_COMMIT_SHA=d8dbe4d94f15fe89232e0402c6e8a0ddf21af3ab \ + --cap-add=SYS_ADMIN \ + -v /var/lib/containers/:/var/lib/containers/:Z \ + -v $(pwd):$(pwd) \ + -w $(pwd) \ + plugins/buildah-docker --dry-run +``` diff --git a/cmd/drone-acr/main.go b/cmd/drone-acr/main.go new file mode 100644 index 0000000..62e65c5 --- /dev/null +++ b/cmd/drone-acr/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/joho/godotenv" +) + +func main() { + // Load env-file if it exists first + if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { + godotenv.Load(env) + } + + var ( + repo = getenv("PLUGIN_REPO") + registry = getenv("PLUGIN_REGISTRY") + username = getenv("SERVICE_PRINCIPAL_CLIENT_ID") + password = getenv("SERVICE_PRINCIPAL_CLIENT_SECRET") + ) + + // default registry value + if registry == "" { + registry = "azurecr.io" + } + + // must use the fully qualified repo name. If the + // repo name does not have the registry prefix we + // should prepend. + if !strings.HasPrefix(repo, registry) { + repo = fmt.Sprintf("%s/%s", registry, repo) + } + + os.Setenv("PLUGIN_REPO", repo) + os.Setenv("PLUGIN_REGISTRY", registry) + os.Setenv("DOCKER_USERNAME", username) + os.Setenv("DOCKER_PASSWORD", password) + + // invoke the base docker plugin binary + cmd := exec.Command("drone-docker") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + os.Exit(1) + } +} + +func getenv(key ...string) (s string) { + for _, k := range key { + s = os.Getenv(k) + if s != "" { + return + } + } + return +} diff --git a/cmd/drone-docker/main.go b/cmd/drone-docker/main.go new file mode 100644 index 0000000..c156e00 --- /dev/null +++ b/cmd/drone-docker/main.go @@ -0,0 +1,248 @@ +package main + +import ( + "os" + + "github.com/joho/godotenv" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" + + docker "github.com/drone-plugins/drone-buildah" +) + +var ( + version = "unknown" +) + +func main() { + // Load env-file if it exists first + if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { + godotenv.Load(env) + } + + app := cli.NewApp() + app.Name = "buildah plugin" + app.Usage = "buildah plugin" + app.Action = run + app.Version = version + app.Flags = []cli.Flag{ + cli.BoolFlag{ + Name: "dry-run", + Usage: "dry run disables docker push", + EnvVar: "PLUGIN_DRY_RUN", + }, + cli.StringFlag{ + Name: "remote.url", + Usage: "git remote url", + EnvVar: "DRONE_REMOTE_URL", + }, + cli.StringFlag{ + Name: "commit.sha", + Usage: "git commit sha", + EnvVar: "DRONE_COMMIT_SHA", + Value: "00000000", + }, + cli.StringFlag{ + Name: "commit.ref", + Usage: "git commit ref", + EnvVar: "DRONE_COMMIT_REF", + }, + cli.StringFlag{ + Name: "dockerfile", + Usage: "build dockerfile", + Value: "Dockerfile", + EnvVar: "PLUGIN_DOCKERFILE", + }, + cli.StringFlag{ + Name: "context", + Usage: "build context", + Value: ".", + EnvVar: "PLUGIN_CONTEXT", + }, + cli.StringSliceFlag{ + Name: "tags", + Usage: "build tags", + Value: &cli.StringSlice{"latest"}, + EnvVar: "PLUGIN_TAG,PLUGIN_TAGS", + FilePath: ".tags", + }, + cli.BoolFlag{ + Name: "tags.auto", + Usage: "default build tags", + EnvVar: "PLUGIN_DEFAULT_TAGS,PLUGIN_AUTO_TAG", + }, + cli.StringFlag{ + Name: "tags.suffix", + Usage: "default build tags with suffix", + EnvVar: "PLUGIN_DEFAULT_SUFFIX,PLUGIN_AUTO_TAG_SUFFIX", + }, + cli.StringSliceFlag{ + Name: "args", + Usage: "build args", + EnvVar: "PLUGIN_BUILD_ARGS", + }, + cli.StringSliceFlag{ + Name: "args-from-env", + Usage: "build args", + EnvVar: "PLUGIN_BUILD_ARGS_FROM_ENV", + }, + cli.BoolFlag{ + Name: "quiet", + Usage: "quiet docker build", + EnvVar: "PLUGIN_QUIET", + }, + cli.StringFlag{ + Name: "target", + Usage: "build target", + EnvVar: "PLUGIN_TARGET", + }, + cli.BoolFlag{ + Name: "squash", + Usage: "squash the layers at build time", + EnvVar: "PLUGIN_SQUASH", + }, + cli.BoolTFlag{ + Name: "pull-image", + Usage: "force pull base image at build time", + EnvVar: "PLUGIN_PULL_IMAGE", + }, + cli.BoolFlag{ + Name: "compress", + Usage: "compress the build context using gzip", + EnvVar: "PLUGIN_COMPRESS", + }, + cli.StringFlag{ + Name: "repo", + Usage: "docker repository", + EnvVar: "PLUGIN_REPO", + }, + cli.StringSliceFlag{ + Name: "custom-labels", + Usage: "additional k=v labels", + EnvVar: "PLUGIN_CUSTOM_LABELS", + }, + cli.StringSliceFlag{ + Name: "label-schema", + Usage: "label-schema labels", + EnvVar: "PLUGIN_LABEL_SCHEMA", + }, + cli.BoolTFlag{ + Name: "auto-label", + Usage: "auto-label true|false", + EnvVar: "PLUGIN_AUTO_LABEL", + }, + cli.StringFlag{ + Name: "link", + Usage: "link https://example.com/org/repo-name", + EnvVar: "PLUGIN_REPO_LINK,DRONE_REPO_LINK", + }, + cli.StringFlag{ + Name: "docker.registry", + Usage: "docker registry", + Value: "https://index.docker.io/v1/", + EnvVar: "PLUGIN_REGISTRY,DOCKER_REGISTRY", + }, + cli.StringFlag{ + Name: "docker.username", + Usage: "docker username", + EnvVar: "PLUGIN_USERNAME,DOCKER_USERNAME", + }, + cli.StringFlag{ + Name: "docker.password", + Usage: "docker password", + EnvVar: "PLUGIN_PASSWORD,DOCKER_PASSWORD", + }, + cli.StringFlag{ + Name: "docker.email", + Usage: "docker email", + EnvVar: "PLUGIN_EMAIL,DOCKER_EMAIL", + }, + cli.StringFlag{ + Name: "docker.config", + Usage: "docker json dockerconfig content", + EnvVar: "PLUGIN_CONFIG,DOCKER_PLUGIN_CONFIG", + }, + cli.BoolTFlag{ + Name: "docker.purge", + Usage: "docker should cleanup images", + EnvVar: "PLUGIN_PURGE", + }, + cli.StringFlag{ + Name: "repo.branch", + Usage: "repository default branch", + EnvVar: "DRONE_REPO_BRANCH", + }, + cli.BoolFlag{ + Name: "no-cache", + Usage: "do not use cached intermediate containers", + EnvVar: "PLUGIN_NO_CACHE", + }, + cli.StringSliceFlag{ + Name: "add-host", + Usage: "additional host:IP mapping", + EnvVar: "PLUGIN_ADD_HOST", + }, + } + + if err := app.Run(os.Args); err != nil { + logrus.Fatal(err) + } +} + +func run(c *cli.Context) error { + plugin := docker.Plugin{ + Dryrun: c.Bool("dry-run"), + Cleanup: c.BoolT("docker.purge"), + Login: docker.Login{ + Registry: c.String("docker.registry"), + Username: c.String("docker.username"), + Password: c.String("docker.password"), + Email: c.String("docker.email"), + Config: c.String("docker.config"), + }, + Build: docker.Build{ + Remote: c.String("remote.url"), + Name: c.String("commit.sha"), + Dockerfile: c.String("dockerfile"), + Context: c.String("context"), + Tags: c.StringSlice("tags"), + Args: c.StringSlice("args"), + ArgsEnv: c.StringSlice("args-from-env"), + Target: c.String("target"), + Squash: c.Bool("squash"), + Pull: c.BoolT("pull-image"), + CacheFrom: c.StringSlice("cache-from"), + Compress: c.Bool("compress"), + Repo: c.String("repo"), + Labels: c.StringSlice("custom-labels"), + LabelSchema: c.StringSlice("label-schema"), + AutoLabel: c.BoolT("auto-label"), + Link: c.String("link"), + NoCache: c.Bool("no-cache"), + AddHost: c.StringSlice("add-host"), + Quiet: c.Bool("quiet"), + }, + } + + if c.Bool("tags.auto") { + if docker.UseDefaultTag( // return true if tag event or default branch + c.String("commit.ref"), + c.String("repo.branch"), + ) { + tag, err := docker.DefaultTagSuffix( + c.String("commit.ref"), + c.String("tags.suffix"), + ) + if err != nil { + logrus.Printf("cannot build docker image for %s, invalid semantic version", c.String("commit.ref")) + return err + } + plugin.Build.Tags = tag + } else { + logrus.Printf("skipping automated docker build for %s", c.String("commit.ref")) + return nil + } + } + + return plugin.Exec() +} diff --git a/cmd/drone-ecr/main.go b/cmd/drone-ecr/main.go new file mode 100644 index 0000000..9de37a1 --- /dev/null +++ b/cmd/drone-ecr/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/joho/godotenv" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecr" +) + +const defaultRegion = "us-east-1" + +func main() { + // Load env-file if it exists first + if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { + godotenv.Load(env) + } + + var ( + repo = getenv("PLUGIN_REPO") + registry = getenv("PLUGIN_REGISTRY") + region = getenv("PLUGIN_REGION", "ECR_REGION", "AWS_REGION") + key = getenv("PLUGIN_ACCESS_KEY", "ECR_ACCESS_KEY", "AWS_ACCESS_KEY_ID") + secret = getenv("PLUGIN_SECRET_KEY", "ECR_SECRET_KEY", "AWS_SECRET_ACCESS_KEY") + create = parseBoolOrDefault(false, getenv("PLUGIN_CREATE_REPOSITORY", "ECR_CREATE_REPOSITORY")) + lifecyclePolicy = getenv("PLUGIN_LIFECYCLE_POLICY") + repositoryPolicy = getenv("PLUGIN_REPOSITORY_POLICY") + assumeRole = getenv("PLUGIN_ASSUME_ROLE") + scanOnPush = parseBoolOrDefault(false, getenv("PLUGIN_SCAN_ON_PUSH")) + ) + + // set the region + if region == "" { + region = defaultRegion + } + + os.Setenv("AWS_REGION", region) + + if key != "" && secret != "" { + os.Setenv("AWS_ACCESS_KEY_ID", key) + os.Setenv("AWS_SECRET_ACCESS_KEY", secret) + } + + sess, err := session.NewSession(&aws.Config{Region: ®ion}) + if err != nil { + log.Fatal(fmt.Sprintf("error creating aws session: %v", err)) + } + + svc := getECRClient(sess, assumeRole) + username, password, defaultRegistry, err := getAuthInfo(svc) + + if registry == "" { + registry = defaultRegistry + } + + if err != nil { + log.Fatal(fmt.Sprintf("error getting ECR auth: %v", err)) + } + + if !strings.HasPrefix(repo, registry) { + repo = fmt.Sprintf("%s/%s", registry, repo) + } + + if create { + err = ensureRepoExists(svc, trimHostname(repo, registry), scanOnPush) + if err != nil { + log.Fatal(fmt.Sprintf("error creating ECR repo: %v", err)) + } + err = updateImageScannningConfig(svc, trimHostname(repo, registry), scanOnPush) + if err != nil { + log.Fatal(fmt.Sprintf("error updating scan on push for ECR repo: %v", err)) + } + } + + if lifecyclePolicy != "" { + p, err := ioutil.ReadFile(lifecyclePolicy) + if err != nil { + log.Fatal(err) + } + if err := uploadLifeCyclePolicy(svc, string(p), trimHostname(repo, registry)); err != nil { + log.Fatal(fmt.Sprintf("error uploading ECR lifecycle policy: %v", err)) + } + } + + if repositoryPolicy != "" { + p, err := ioutil.ReadFile(repositoryPolicy) + if err != nil { + log.Fatal(err) + } + if err := uploadRepositoryPolicy(svc, string(p), trimHostname(repo, registry)); err != nil { + log.Fatal(fmt.Sprintf("error uploading ECR repository policy. %v", err)) + } + } + + os.Setenv("PLUGIN_REPO", repo) + os.Setenv("PLUGIN_REGISTRY", registry) + os.Setenv("DOCKER_USERNAME", username) + os.Setenv("DOCKER_PASSWORD", password) + + // invoke the base docker plugin binary + cmd := exec.Command("drone-docker") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err = cmd.Run(); err != nil { + os.Exit(1) + } +} + +func trimHostname(repo, registry string) string { + repo = strings.TrimPrefix(repo, registry) + repo = strings.TrimLeft(repo, "/") + return repo +} + +func ensureRepoExists(svc *ecr.ECR, name string, scanOnPush bool) (err error) { + input := &ecr.CreateRepositoryInput{} + input.SetRepositoryName(name) + input.SetImageScanningConfiguration(&ecr.ImageScanningConfiguration{ScanOnPush: &scanOnPush}) + _, err = svc.CreateRepository(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ecr.ErrCodeRepositoryAlreadyExistsException { + // eat it, we skip checking for existing to save two requests + err = nil + } + } + + return +} + +func updateImageScannningConfig(svc *ecr.ECR, name string, scanOnPush bool) (err error) { + input := &ecr.PutImageScanningConfigurationInput{} + input.SetRepositoryName(name) + input.SetImageScanningConfiguration(&ecr.ImageScanningConfiguration{ScanOnPush: &scanOnPush}) + _, err = svc.PutImageScanningConfiguration(input) + + return err +} + +func uploadLifeCyclePolicy(svc *ecr.ECR, lifecyclePolicy string, name string) (err error) { + input := &ecr.PutLifecyclePolicyInput{} + input.SetLifecyclePolicyText(lifecyclePolicy) + input.SetRepositoryName(name) + _, err = svc.PutLifecyclePolicy(input) + + return err +} + +func uploadRepositoryPolicy(svc *ecr.ECR, repositoryPolicy string, name string) (err error) { + input := &ecr.SetRepositoryPolicyInput{} + input.SetPolicyText(repositoryPolicy) + input.SetRepositoryName(name) + _, err = svc.SetRepositoryPolicy(input) + + return err +} + +func getAuthInfo(svc *ecr.ECR) (username, password, registry string, err error) { + var result *ecr.GetAuthorizationTokenOutput + var decoded []byte + + result, err = svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{}) + if err != nil { + return + } + + auth := result.AuthorizationData[0] + token := *auth.AuthorizationToken + decoded, err = base64.StdEncoding.DecodeString(token) + if err != nil { + return + } + + registry = strings.TrimPrefix(*auth.ProxyEndpoint, "https://") + creds := strings.Split(string(decoded), ":") + username = creds[0] + password = creds[1] + return +} + +func parseBoolOrDefault(defaultValue bool, s string) (result bool) { + var err error + result, err = strconv.ParseBool(s) + if err != nil { + result = false + } + + return +} + +func getenv(key ...string) (s string) { + for _, k := range key { + s = os.Getenv(k) + if s != "" { + return + } + } + return +} + +func getECRClient(sess *session.Session, role string) *ecr.ECR { + if role == "" { + return ecr.New(sess) + } + return ecr.New(sess, &aws.Config{ + Credentials: stscreds.NewCredentials(sess, role), + }) +} diff --git a/cmd/drone-ecr/main_test.go b/cmd/drone-ecr/main_test.go new file mode 100644 index 0000000..5c50ba5 --- /dev/null +++ b/cmd/drone-ecr/main_test.go @@ -0,0 +1,20 @@ +package main + +import "testing" + +func TestTrimHostname(t *testing.T) { + registry := "000000000000.dkr.ecr.us-east-1.amazonaws.com" + // map full repo path to expected repo name + repos := map[string]string{ + registry + "/repo": "repo", + registry + "/namespace/repo": "namespace/repo", + registry + "/namespace/namespace/repo": "namespace/namespace/repo", + } + + for repo, name := range repos { + splitName := trimHostname(repo, registry) + if splitName != name { + t.Errorf("%s is not equal to %s.", splitName, name) + } + } +} diff --git a/cmd/drone-gcr/main.go b/cmd/drone-gcr/main.go new file mode 100644 index 0000000..addddcb --- /dev/null +++ b/cmd/drone-gcr/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "encoding/base64" + "os" + "os/exec" + "path" + "strings" + + "github.com/joho/godotenv" +) + +// gcr default username +const username = "_json_key" + +func main() { + // Load env-file if it exists first + if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { + godotenv.Load(env) + } + + var ( + repo = getenv("PLUGIN_REPO") + registry = getenv("PLUGIN_REGISTRY") + password = getenv( + "PLUGIN_JSON_KEY", + "GCR_JSON_KEY", + "GOOGLE_CREDENTIALS", + "TOKEN", + ) + ) + + // decode the token if base64 encoded + decoded, err := base64.StdEncoding.DecodeString(password) + if err == nil { + password = string(decoded) + } + + // default registry value + if registry == "" { + registry = "gcr.io" + } + + // must use the fully qualified repo name. If the + // repo name does not have the registry prefix we + // should prepend. + if !strings.HasPrefix(repo, registry) { + repo = path.Join(registry, repo) + } + + os.Setenv("PLUGIN_REPO", repo) + os.Setenv("PLUGIN_REGISTRY", registry) + os.Setenv("DOCKER_USERNAME", username) + os.Setenv("DOCKER_PASSWORD", password) + + // invoke the base docker plugin binary + cmd := exec.Command("drone-docker") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + os.Exit(1) + } +} + +func getenv(key ...string) (s string) { + for _, k := range key { + s = os.Getenv(k) + if s != "" { + return + } + } + return +} diff --git a/cmd/drone-heroku/main.go b/cmd/drone-heroku/main.go new file mode 100644 index 0000000..7a5ca49 --- /dev/null +++ b/cmd/drone-heroku/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "os" + "os/exec" + "path" + + "github.com/joho/godotenv" +) + +func main() { + // Load env-file if it exists first + if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { + godotenv.Load(env) + } + + var ( + registry = "registry.heroku.com" + process = getenv("PLUGIN_PROCESS_TYPE") + app = getenv("PLUGIN_APP") + email = getenv("PLUGIN_EMAIL", "HEROKU_EMAIL") + key = getenv("PLUGIN_API_KEY", "HEROKU_API_KEY") + ) + + if process == "" { + process = "web" + } + + os.Setenv("PLUGIN_REGISTRY", registry) + os.Setenv("PLUGIN_REPO", path.Join(registry, app, process)) + + os.Setenv("DOCKER_PASSWORD", key) + os.Setenv("DOCKER_USERNAME", email) + os.Setenv("DOCKER_EMAIL", email) + + cmd := exec.Command("drone-docker") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + os.Exit(1) + } +} + +func getenv(key ...string) (s string) { + for _, k := range key { + s = os.Getenv(k) + if s != "" { + return + } + } + return +} diff --git a/docker.go b/docker.go new file mode 100644 index 0000000..2f0835a --- /dev/null +++ b/docker.go @@ -0,0 +1,328 @@ +package docker + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +const buildahExe = "buildah" + +type ( + // Login defines Docker login parameters. + Login struct { + Registry string // Docker registry address + Username string // Docker registry username + Password string // Docker registry password + Email string // Docker registry email + Config string // Docker Auth Config + } + + // Build defines Docker build parameters. + Build struct { + Remote string // Git remote URL + Name string // Docker build using default named tag + Dockerfile string // Docker build Dockerfile + Context string // Docker build context + Tags []string // Docker build tags + Args []string // Docker build args + ArgsEnv []string // Docker build args from env + Target string // Docker build target + Squash bool // Docker build squash + Pull bool // Docker build pull + CacheFrom []string // Docker build cache-from. It is a NOOP in buildah + Compress bool // Docker build compress + Repo string // Docker build repository + LabelSchema []string // label-schema Label map + AutoLabel bool // auto-label bool + Labels []string // Label map + Link string // Git repo link + NoCache bool // Docker build no-cache + AddHost []string // Docker build add-host + Quiet bool // Docker build quiet + } + + // Plugin defines the Docker plugin parameters. + Plugin struct { + Login Login // Docker login configuration + Build Build // Docker build configuration + Dryrun bool // Docker push is skipped + Cleanup bool // Docker purge is enabled + } +) + +// Exec executes the plugin step +func (p Plugin) Exec() error { + // Create Auth Config File + if p.Login.Config != "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to find home directory: %s", err) + } + dockerHome := fmt.Sprintf("%s/.docker/config.json", homeDir) + os.MkdirAll(dockerHome, 0600) + + path := filepath.Join(dockerHome, "config.json") + err = ioutil.WriteFile(path, []byte(p.Login.Config), 0600) + if err != nil { + return fmt.Errorf("Error writing config.json: %s", err) + } + } + + // login to the Docker registry + if p.Login.Password != "" { + cmd := commandLogin(p.Login) + err := cmd.Run() + if err != nil { + return fmt.Errorf("Error authenticating: %s", err) + } + } + + switch { + case p.Login.Password != "": + fmt.Println("Detected registry credentials") + case p.Login.Config != "": + fmt.Println("Detected registry credentials file") + default: + fmt.Println("Registry credentials or Docker config not provided. Guest mode enabled.") + } + + // add proxy build args + addProxyBuildArgs(&p.Build) + + var cmds []*exec.Cmd + cmds = append(cmds, commandVersion()) // docker version + cmds = append(cmds, commandInfo()) // docker info + + // pre-pull cache images + for _, img := range p.Build.CacheFrom { + cmds = append(cmds, commandPull(img)) + } + + cmds = append(cmds, commandBuild(p.Build)) // docker build + + for _, tag := range p.Build.Tags { + cmds = append(cmds, commandTag(p.Build, tag)) // docker tag + + if p.Dryrun == false { + cmds = append(cmds, commandPush(p.Build, tag)) // docker push + } + } + + if p.Cleanup { + cmds = append(cmds, commandRmi(p.Build.Name)) // buildah rmi + } + + // execute all commands in batch mode. + for _, cmd := range cmds { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + trace(cmd) + + err := cmd.Run() + if err != nil && isCommandPull(cmd.Args) { + fmt.Printf("Could not pull cache-from image %s. Ignoring...\n", cmd.Args[2]) + } else if err != nil && isCommandPrune(cmd.Args) { + fmt.Printf("Could not prune system containers. Ignoring...\n") + } else if err != nil && isCommandRmi(cmd.Args) { + fmt.Printf("Could not remove image %s. Ignoring...\n", cmd.Args[2]) + } else if err != nil { + return err + } + } + + return nil +} + +// helper function to create the docker login command. +func commandLogin(login Login) *exec.Cmd { + if login.Email != "" { + return commandLoginEmail(login) + } + return exec.Command( + buildahExe, "login", + "-u", login.Username, + "-p", login.Password, + login.Registry, + ) +} + +// helper to check if args match "docker pull " +func isCommandPull(args []string) bool { + return len(args) > 2 && args[1] == "pull" +} + +func commandPull(repo string) *exec.Cmd { + return exec.Command(buildahExe, "pull", repo) +} + +func commandLoginEmail(login Login) *exec.Cmd { + return exec.Command( + buildahExe, "login", + "-u", login.Username, + "-p", login.Password, + "-e", login.Email, + login.Registry, + ) +} + +// helper function to create the docker info command. +func commandVersion() *exec.Cmd { + return exec.Command(buildahExe, "version") +} + +// helper function to create the docker info command. +func commandInfo() *exec.Cmd { + return exec.Command(buildahExe, "info") +} + +// helper function to create the docker build command. +func commandBuild(build Build) *exec.Cmd { + args := []string{ + "bud", + "-f", build.Dockerfile, + } + + if build.Squash { + args = append(args, "--squash") + } + if build.Compress { + args = append(args, "--compress") + } + if build.Pull { + args = append(args, "--pull=true") + } + if build.NoCache { + args = append(args, "--no-cache") + } + for _, arg := range build.CacheFrom { + args = append(args, "--cache-from", arg) + } + for _, arg := range build.ArgsEnv { + addProxyValue(&build, arg) + } + for _, arg := range build.Args { + args = append(args, "--build-arg", arg) + } + for _, host := range build.AddHost { + args = append(args, "--add-host", host) + } + if build.Target != "" { + args = append(args, "--target", build.Target) + } + if build.Quiet { + args = append(args, "--quiet") + } + + if build.AutoLabel { + labelSchema := []string{ + fmt.Sprintf("created=%s", time.Now().Format(time.RFC3339)), + fmt.Sprintf("revision=%s", build.Name), + fmt.Sprintf("source=%s", build.Remote), + fmt.Sprintf("url=%s", build.Link), + } + labelPrefix := "org.opencontainers.image" + + if len(build.LabelSchema) > 0 { + labelSchema = append(labelSchema, build.LabelSchema...) + } + + for _, label := range labelSchema { + args = append(args, "--label", fmt.Sprintf("%s.%s", labelPrefix, label)) + } + } + + if len(build.Labels) > 0 { + for _, label := range build.Labels { + args = append(args, "--label", label) + } + } + + args = append(args, "-t", build.Name) + args = append(args, build.Context) + return exec.Command(buildahExe, args...) +} + +// helper function to add proxy values from the environment +func addProxyBuildArgs(build *Build) { + addProxyValue(build, "http_proxy") + addProxyValue(build, "https_proxy") + addProxyValue(build, "no_proxy") +} + +// helper function to add the upper and lower case version of a proxy value. +func addProxyValue(build *Build, key string) { + value := getProxyValue(key) + + if len(value) > 0 && !hasProxyBuildArg(build, key) { + build.Args = append(build.Args, fmt.Sprintf("%s=%s", key, value)) + build.Args = append(build.Args, fmt.Sprintf("%s=%s", strings.ToUpper(key), value)) + } +} + +// helper function to get a proxy value from the environment. +// +// assumes that the upper and lower case versions of are the same. +func getProxyValue(key string) string { + value := os.Getenv(key) + + if len(value) > 0 { + return value + } + + return os.Getenv(strings.ToUpper(key)) +} + +// helper function that looks to see if a proxy value was set in the build args. +func hasProxyBuildArg(build *Build, key string) bool { + keyUpper := strings.ToUpper(key) + + for _, s := range build.Args { + if strings.HasPrefix(s, key) || strings.HasPrefix(s, keyUpper) { + return true + } + } + + return false +} + +// helper function to create the docker tag command. +func commandTag(build Build, tag string) *exec.Cmd { + var ( + source = build.Name + target = fmt.Sprintf("%s:%s", build.Repo, tag) + ) + return exec.Command( + buildahExe, "tag", source, target, + ) +} + +// helper function to create the docker push command. +func commandPush(build Build, tag string) *exec.Cmd { + target := fmt.Sprintf("%s:%s", build.Repo, tag) + return exec.Command(buildahExe, "push", target) +} + +// helper to check if args match "docker prune" +func isCommandPrune(args []string) bool { + return len(args) > 3 && args[2] == "prune" +} + +// helper to check if args match "docker rmi" +func isCommandRmi(args []string) bool { + return len(args) > 2 && args[1] == "rmi" +} + +func commandRmi(tag string) *exec.Cmd { + return exec.Command(buildahExe, "rmi", tag) +} + +// trace writes each command to stdout with the command wrapped in an xml +// tag so that it can be extracted and displayed in the logs. +func trace(cmd *exec.Cmd) { + fmt.Fprintf(os.Stdout, "+ %s\n", strings.Join(cmd.Args, " ")) +} diff --git a/docker/acr/Dockerfile.linux.amd64 b/docker/acr/Dockerfile.linux.amd64 new file mode 100644 index 0000000..c2e6485 --- /dev/null +++ b/docker/acr/Dockerfile.linux.amd64 @@ -0,0 +1,22 @@ +# Source for dockerfile: +# https://github.com/containers/buildah/blob/master/docs/tutorials/05-openshift-rootless-bud.md +FROM quay.io/buildah/stable:v1.14.8 + +RUN touch /etc/subgid /etc/subuid \ + && chmod g=u /etc/subgid /etc/subuid /etc/passwd \ + && echo build:10000:65536 > /etc/subuid \ + && echo build:10000:65536 > /etc/subgid + +# Use chroot since the default runc does not work when running rootless +RUN echo "export BUILDAH_ISOLATION=chroot" >> /home/build/.bashrc + +# Use VFS since fuse does not work +RUN mkdir -p /home/build/.config/containers \ + && echo "driver=\"vfs\"" > /home/build/.config/containers/storage.conf + +USER build +WORKDIR /home/build + +# Add plugin binary +ADD release/linux/amd64/drone-acr /bin/ +ENTRYPOINT ["/bin/drone-acr"] diff --git a/docker/acr/manifest.tmpl b/docker/acr/manifest.tmpl new file mode 100644 index 0000000..65e3e5b --- /dev/null +++ b/docker/acr/manifest.tmpl @@ -0,0 +1,13 @@ +image: plugins/buildah-acr:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - + image: plugins/buildah-acr:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux \ No newline at end of file diff --git a/docker/docker/Dockerfile.linux.amd64 b/docker/docker/Dockerfile.linux.amd64 new file mode 100644 index 0000000..fab76ef --- /dev/null +++ b/docker/docker/Dockerfile.linux.amd64 @@ -0,0 +1,22 @@ +# Source for dockerfile: +# https://github.com/containers/buildah/blob/master/docs/tutorials/05-openshift-rootless-bud.md +FROM quay.io/buildah/stable:v1.14.8 + +RUN touch /etc/subgid /etc/subuid \ + && chmod g=u /etc/subgid /etc/subuid /etc/passwd \ + && echo build:10000:65536 > /etc/subuid \ + && echo build:10000:65536 > /etc/subgid + +# Use chroot since the default runc does not work when running rootless +RUN echo "export BUILDAH_ISOLATION=chroot" >> /home/build/.bashrc + +# Use VFS since fuse does not work +RUN mkdir -p /home/build/.config/containers \ + && echo "driver=\"vfs\"" > /home/build/.config/containers/storage.conf + +USER build +WORKDIR /home/build + +# Add plugin binary +ADD release/linux/amd64/drone-docker /bin/ +ENTRYPOINT ["/bin/drone-docker"] diff --git a/docker/docker/manifest.tmpl b/docker/docker/manifest.tmpl new file mode 100644 index 0000000..3d1a0a1 --- /dev/null +++ b/docker/docker/manifest.tmpl @@ -0,0 +1,13 @@ +image: plugins/buildah-docker:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - + image: plugins/buildah-docker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux \ No newline at end of file diff --git a/docker/ecr/Dockerfile.linux.amd64 b/docker/ecr/Dockerfile.linux.amd64 new file mode 100644 index 0000000..455e58d --- /dev/null +++ b/docker/ecr/Dockerfile.linux.amd64 @@ -0,0 +1,22 @@ +# Source for dockerfile: +# https://github.com/containers/buildah/blob/master/docs/tutorials/05-openshift-rootless-bud.md +FROM quay.io/buildah/stable:v1.14.8 + +RUN touch /etc/subgid /etc/subuid \ + && chmod g=u /etc/subgid /etc/subuid /etc/passwd \ + && echo build:10000:65536 > /etc/subuid \ + && echo build:10000:65536 > /etc/subgid + +# Use chroot since the default runc does not work when running rootless +RUN echo "export BUILDAH_ISOLATION=chroot" >> /home/build/.bashrc + +# Use VFS since fuse does not work +RUN mkdir -p /home/build/.config/containers \ + && echo "driver=\"vfs\"" > /home/build/.config/containers/storage.conf + +USER build +WORKDIR /home/build + +# Add plugin binary +ADD release/linux/amd64/drone-ecr /bin/ +ENTRYPOINT ["/bin/drone-ecr"] diff --git a/docker/ecr/manifest.tmpl b/docker/ecr/manifest.tmpl new file mode 100644 index 0000000..7ecc015 --- /dev/null +++ b/docker/ecr/manifest.tmpl @@ -0,0 +1,13 @@ +image: plugins/buildah-ecr:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - + image: plugins/buildah-ecr:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux diff --git a/docker/gcr/Dockerfile.linux.amd64 b/docker/gcr/Dockerfile.linux.amd64 new file mode 100644 index 0000000..9d8f83c --- /dev/null +++ b/docker/gcr/Dockerfile.linux.amd64 @@ -0,0 +1,22 @@ +# Source for dockerfile: +# https://github.com/containers/buildah/blob/master/docs/tutorials/05-openshift-rootless-bud.md +FROM quay.io/buildah/stable:v1.14.8 + +RUN touch /etc/subgid /etc/subuid \ + && chmod g=u /etc/subgid /etc/subuid /etc/passwd \ + && echo build:10000:65536 > /etc/subuid \ + && echo build:10000:65536 > /etc/subgid + +# Use chroot since the default runc does not work when running rootless +RUN echo "export BUILDAH_ISOLATION=chroot" >> /home/build/.bashrc + +# Use VFS since fuse does not work +RUN mkdir -p /home/build/.config/containers \ + && echo "driver=\"vfs\"" > /home/build/.config/containers/storage.conf + +USER build +WORKDIR /home/build + +# Add plugin binary +ADD release/linux/amd64/drone-gcr /bin/ +ENTRYPOINT ["/bin/drone-gcr"] diff --git a/docker/gcr/manifest.tmpl b/docker/gcr/manifest.tmpl new file mode 100644 index 0000000..d0f7469 --- /dev/null +++ b/docker/gcr/manifest.tmpl @@ -0,0 +1,13 @@ +image: plugins/buildah-gcr:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - + image: plugins/buildah-gcr:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux diff --git a/docker/heroku/Dockerfile.linux.amd64 b/docker/heroku/Dockerfile.linux.amd64 new file mode 100644 index 0000000..7fb258b --- /dev/null +++ b/docker/heroku/Dockerfile.linux.amd64 @@ -0,0 +1,22 @@ +# Source for dockerfile: +# https://github.com/containers/buildah/blob/master/docs/tutorials/05-openshift-rootless-bud.md +FROM quay.io/buildah/stable:v1.14.8 + +RUN touch /etc/subgid /etc/subuid \ + && chmod g=u /etc/subgid /etc/subuid /etc/passwd \ + && echo build:10000:65536 > /etc/subuid \ + && echo build:10000:65536 > /etc/subgid + +# Use chroot since the default runc does not work when running rootless +RUN echo "export BUILDAH_ISOLATION=chroot" >> /home/build/.bashrc + +# Use VFS since fuse does not work +RUN mkdir -p /home/build/.config/containers \ + && echo "driver=\"vfs\"" > /home/build/.config/containers/storage.conf + +USER build +WORKDIR /home/build + +# Add plugin binary +ADD release/linux/amd64/drone-heroku /bin/ +ENTRYPOINT ["/bin/drone-heroku"] diff --git a/docker/heroku/manifest.tmpl b/docker/heroku/manifest.tmpl new file mode 100644 index 0000000..ee87926 --- /dev/null +++ b/docker/heroku/manifest.tmpl @@ -0,0 +1,13 @@ +image: plugins/buildah-heroku:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - + image: plugins/buildah-heroku:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux diff --git a/docker_test.go b/docker_test.go new file mode 100644 index 0000000..1cdc3ff --- /dev/null +++ b/docker_test.go @@ -0,0 +1 @@ +package docker diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3b020c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/drone-plugins/drone-buildah + +require ( + github.com/aws/aws-sdk-go v1.26.7 + github.com/coreos/go-semver v0.2.0 + github.com/joho/godotenv v1.3.0 + github.com/sirupsen/logrus v1.3.0 + github.com/urfave/cli v1.22.2 + golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e // indirect + golang.org/x/text v0.3.0 // indirect +) + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e684144 --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aws/aws-sdk-go v1.26.7 h1:ObjEnmzvSdYy8KVd3me7v/UMyCn81inLy2SyoIPoBkg= +github.com/aws/aws-sdk-go v1.26.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..655b8e8 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# force go modules +export GOPATH="" + +# disable cgo +export CGO_ENABLED=0 + +set -e +set -x + +# linux +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-gcr ./cmd/drone-gcr +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-ecr ./cmd/drone-ecr +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-docker ./cmd/drone-docker +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-acr ./cmd/drone-acr +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-heroku ./cmd/drone-heroku + diff --git a/scripts/docker.sh b/scripts/docker.sh new file mode 100755 index 0000000..a28a913 --- /dev/null +++ b/scripts/docker.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +# force go modules +export GOPATH="" + +# disable cgo +export CGO_ENABLED=0 + +# force linux amd64 platform +export GOOS=linux +export GOARCH=amd64 + +set -e +set -x + +# build the binary +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-gcr ./cmd/drone-gcr +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-ecr ./cmd/drone-ecr +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-docker ./cmd/drone-docker +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-acr ./cmd/drone-acr +GOOS=linux GOARCH=amd64 go build -o release/linux/amd64/drone-heroku ./cmd/drone-heroku + +# build the docker image +docker build -f docker/gcr/Dockerfile.linux.amd64 -t plugins/buildah-gcr . +docker build -f docker/ecr/Dockerfile.linux.amd64 -t plugins/buildah-ecr . +docker build -f docker/docker/Dockerfile.linux.amd64 -t plugins/buildah-docker . +docker build -f docker/acr/Dockerfile.linux.amd64 -t plugins/buildah-acr . +docker build -f docker/heroku/Dockerfile.linux.amd64 -t plugins/buildah-heroku . diff --git a/tags.go b/tags.go new file mode 100644 index 0000000..32114d9 --- /dev/null +++ b/tags.go @@ -0,0 +1,93 @@ +package docker + +import ( + "fmt" + "strings" + + "github.com/coreos/go-semver/semver" +) + +// DefaultTagSuffix returns a set of default suggested tags +// based on the commit ref with an attached suffix. +func DefaultTagSuffix(ref, suffix string) ([]string, error) { + tags, err := DefaultTags(ref) + if err != nil { + return nil, err + } + if len(suffix) == 0 { + return tags, nil + } + for i, tag := range tags { + if tag == "latest" { + tags[i] = suffix + } else { + tags[i] = fmt.Sprintf("%s-%s", tag, suffix) + } + } + return tags, nil +} + +func splitOff(input string, delim string) string { + parts := strings.SplitN(input, delim, 2) + + if len(parts) == 2 { + return parts[0] + } + + return input +} + +// DefaultTags returns a set of default suggested tags based on +// the commit ref. +func DefaultTags(ref string) ([]string, error) { + if !strings.HasPrefix(ref, "refs/tags/") { + return []string{"latest"}, nil + } + v := stripTagPrefix(ref) + version, err := semver.NewVersion(v) + if err != nil { + return []string{"latest"}, err + } + if version.PreRelease != "" || version.Metadata != "" { + return []string{ + version.String(), + }, nil + } + + v = stripTagPrefix(ref) + v = splitOff(splitOff(v, "+"), "-") + dotParts := strings.SplitN(v, ".", 3) + + if version.Major == 0 { + return []string{ + fmt.Sprintf("%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor), + fmt.Sprintf("%0*d.%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor, len(dotParts[2]), version.Patch), + }, nil + } + return []string{ + fmt.Sprintf("%0*d", len(dotParts[0]), version.Major), + fmt.Sprintf("%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor), + fmt.Sprintf("%0*d.%0*d.%0*d", len(dotParts[0]), version.Major, len(dotParts[1]), version.Minor, len(dotParts[2]), version.Patch), + }, nil +} + +// UseDefaultTag for keep only default branch for latest tag +func UseDefaultTag(ref, defaultBranch string) bool { + if strings.HasPrefix(ref, "refs/tags/") { + return true + } + if stripHeadPrefix(ref) == defaultBranch { + return true + } + return false +} + +func stripHeadPrefix(ref string) string { + return strings.TrimPrefix(ref, "refs/heads/") +} + +func stripTagPrefix(ref string) string { + ref = strings.TrimPrefix(ref, "refs/tags/") + ref = strings.TrimPrefix(ref, "v") + return ref +} diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000..6c3feb2 --- /dev/null +++ b/tags_test.go @@ -0,0 +1,197 @@ +package docker + +import ( + "reflect" + "testing" +) + +func Test_stripTagPrefix(t *testing.T) { + var tests = []struct { + Before string + After string + }{ + {"refs/tags/1.0.0", "1.0.0"}, + {"refs/tags/v1.0.0", "1.0.0"}, + {"v1.0.0", "1.0.0"}, + } + + for _, test := range tests { + got, want := stripTagPrefix(test.Before), test.After + if got != want { + t.Errorf("Got tag %s, want %s", got, want) + } + } +} + +func TestDefaultTags(t *testing.T) { + var tests = []struct { + Before string + After []string + }{ + {"", []string{"latest"}}, + {"refs/heads/master", []string{"latest"}}, + {"refs/tags/0.9.0", []string{"0.9", "0.9.0"}}, + {"refs/tags/1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0", []string{"1", "1.0", "1.0.0"}}, + {"refs/tags/v1.0.0-alpha.1", []string{"1.0.0-alpha.1"}}, + } + + for _, test := range tests { + tags, err := DefaultTags(test.Before) + if err != nil { + t.Error(err) + continue + } + got, want := tags, test.After + if !reflect.DeepEqual(got, want) { + t.Errorf("Got tag %v, want %v", got, want) + } + } +} + +func TestDefaultTagsError(t *testing.T) { + var tests = []string{ + "refs/tags/x1.0.0", + "refs/tags/20190203", + } + + for _, test := range tests { + _, err := DefaultTags(test) + if err == nil { + t.Errorf("Expect tag error for %s", test) + } + } +} + +func TestDefaultTagSuffix(t *testing.T) { + var tests = []struct { + Before string + Suffix string + After []string + }{ + // without suffix + { + After: []string{"latest"}, + }, + { + Before: "refs/tags/v1.0.0", + After: []string{ + "1", + "1.0", + "1.0.0", + }, + }, + // with suffix + { + Suffix: "linux-amd64", + After: []string{"linux-amd64"}, + }, + { + Before: "refs/tags/v1.0.0", + Suffix: "linux-amd64", + After: []string{ + "1-linux-amd64", + "1.0-linux-amd64", + "1.0.0-linux-amd64", + }, + }, + { + Suffix: "nanoserver", + After: []string{"nanoserver"}, + }, + { + Before: "refs/tags/v1.9.2", + Suffix: "nanoserver", + After: []string{ + "1-nanoserver", + "1.9-nanoserver", + "1.9.2-nanoserver", + }, + }, + { + Before: "refs/tags/v18.06.0", + Suffix: "nanoserver", + After: []string{ + "18-nanoserver", + "18.06-nanoserver", + "18.06.0-nanoserver", + }, + }, + } + + for _, test := range tests { + tag, err := DefaultTagSuffix(test.Before, test.Suffix) + if err != nil { + t.Error(err) + continue + } + got, want := tag, test.After + if !reflect.DeepEqual(got, want) { + t.Errorf("Got tag %v, want %v", got, want) + } + } +} + +func Test_stripHeadPrefix(t *testing.T) { + type args struct { + ref string + } + tests := []struct { + args args + want string + }{ + { + args: args{ + ref: "refs/heads/master", + }, + want: "master", + }, + } + for _, tt := range tests { + if got := stripHeadPrefix(tt.args.ref); got != tt.want { + t.Errorf("stripHeadPrefix() = %v, want %v", got, tt.want) + } + } +} + +func TestUseDefaultTag(t *testing.T) { + type args struct { + ref string + defaultBranch string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "latest tag for default branch", + args: args{ + ref: "refs/heads/master", + defaultBranch: "master", + }, + want: true, + }, + { + name: "build from tags", + args: args{ + ref: "refs/tags/v1.0.0", + defaultBranch: "master", + }, + want: true, + }, + { + name: "skip build for not default branch", + args: args{ + ref: "refs/heads/develop", + defaultBranch: "master", + }, + want: false, + }, + } + for _, tt := range tests { + if got := UseDefaultTag(tt.args.ref, tt.args.defaultBranch); got != tt.want { + t.Errorf("%q. UseDefaultTag() = %v, want %v", tt.name, got, tt.want) + } + } +}