commit 20da369c908bf2ce01fda48913d16d45a36a6868 Author: Thomas Boerger Date: Wed Jul 18 17:05:45 2018 +0200 Initial commit diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..7eb3519 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,68 @@ +version: '{build}' +image: 'Visual Studio 2017' +platform: 'x64' + +clone_folder: 'c:\gopath\src\github.com\drone-plugins\drone-ansible' +max_jobs: 1 + +environment: + GOPATH: c:\gopath + DOCKER_USERNAME: + secure: '4YzzahbEiMZQJpOCOd1LAw==' + DOCKER_PASSWORD: + secure: 'VqO/G3Zfslu6zSLdwHKO+Q==' + +install: + - ps: | + docker version + go version + - ps: | + $env:Path = "c:\gopath\bin;$env:Path" + +build_script: + - ps: | + go get -u github.com/golang/dep/cmd/dep + dep ensure + + if ( $env:APPVEYOR_REPO_TAG -eq 'false' ) { + go build -ldflags "-X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o release/drone-ansible.exe + } else { + $version = $env:APPVEYOR_REPO_TAG_NAME.substring(1) + go build -ldflags "-X main.version=$version -X main.build=$env:APPVEYOR_BUILD_VERSION" -a -o release/drone-ansible.exe + } + + docker pull microsoft/nanoserver:10.0.14393.1593 + docker build -f Dockerfile.windows -t plugins/ansible:windows-amd64 . + +test_script: + - ps: | + docker run --rm plugins/ansible:windows-amd64 --version + +deploy_script: + - ps: | + $ErrorActionPreference = 'Stop'; + + if ( $env:APPVEYOR_PULL_REQUEST_NUMBER ) { + Write-Host Nothing to deploy. + } else { + echo $env:DOCKER_PASSWORD | docker login --username $env:DOCKER_USERNAME --password-stdin + + if ( $env:APPVEYOR_REPO_TAG -eq 'true' ) { + $major,$minor,$patch = $env:APPVEYOR_REPO_TAG_NAME.substring(1).split('.') + + docker push plugins/ansible:windows-amd64 + + docker tag plugins/ansible:windows-amd64 plugins/ansible:$major.$minor.$patch-windows-amd64 + docker push plugins/ansible:$major.$minor.$patch-windows-amd64 + + docker tag plugins/ansible:windows-amd64 plugins/ansible:$major.$minor-windows-amd64 + docker push plugins/ansible:$major.$minor-windows-amd64 + + docker tag plugins/ansible:windows-amd64 plugins/ansible:$major-windows-amd64 + docker push plugins/ansible:$major-windows-amd64 + } else { + if ( $env:APPVEYOR_REPO_BRANCH -eq 'master' ) { + docker push plugins/ansible:windows-amd64 + } + } + } diff --git a/.dockeringore b/.dockeringore new file mode 100644 index 0000000..c13ca3f --- /dev/null +++ b/.dockeringore @@ -0,0 +1,2 @@ +* +!release/ diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..092f4dd --- /dev/null +++ b/.drone.yml @@ -0,0 +1,149 @@ +workspace: + base: /go + path: src/github.com/drone-plugins/drone-ansible + +pipeline: + deps: + image: golang:1.10 + pull: true + commands: + - go get -u github.com/golang/dep/cmd/dep + - dep ensure + - dep status + + test: + image: golang:1.10 + pull: true + commands: + - go vet ./... + - go test -cover ./... + + build_linux_amd64: + image: golang:1.10 + pull: true + group: build + environment: + - GOOS=linux + - GOARCH=amd64 + - CGO_ENABLED=0 + commands: + - | + if test "${DRONE_TAG}" = ""; then + go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/amd64/drone-ansible + else + go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/amd64/drone-ansible + fi + + # build_linux_i386: + # image: golang:1.10 + # pull: true + # group: build + # environment: + # - GOOS=linux + # - GOARCH=386 + # - CGO_ENABLED=0 + # commands: + # - | + # if test "${DRONE_TAG}" = ""; then + # go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/i386/drone-ansible + # else + # go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/i386/drone-ansible + # fi + + # build_linux_arm64: + # image: golang:1.10 + # pull: true + # group: build + # environment: + # - GOOS=linux + # - GOARCH=arm64 + # - CGO_ENABLED=0 + # commands: + # - | + # if test "${DRONE_TAG}" = ""; then + # go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm64/drone-ansible + # else + # go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm64/drone-ansible + # fi + + # build_linux_arm: + # image: golang:1.10 + # pull: true + # group: build + # environment: + # - GOOS=linux + # - GOARCH=arm + # - CGO_ENABLED=0 + # - GOARM=7 + # commands: + # - | + # if test "${DRONE_TAG}" = ""; then + # go build -v -ldflags "-X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm/drone-ansible + # else + # go build -v -ldflags "-X main.version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}" -a -o release/linux/arm/drone-ansible + # fi + + publish_linux_amd64: + image: plugins/docker:17.12 + pull: true + secrets: [ docker_username, docker_password ] + group: docker + repo: plugins/ansible + auto_tag: true + auto_tag_suffix: linux-amd64 + dockerfile: Dockerfile + when: + event: [ push, tag ] + + # publish_linux_i386: + # image: plugins/docker:17.12 + # pull: true + # secrets: [ docker_username, docker_password ] + # group: docker + # repo: plugins/ansible + # auto_tag: true + # auto_tag_suffix: linux-i386 + # dockerfile: Dockerfile.i386 + # when: + # event: [ push, tag ] + + # publish_linux_arm64: + # image: plugins/docker:17.12 + # pull: true + # secrets: [ docker_username, docker_password ] + # group: docker + # repo: plugins/ansible + # auto_tag: true + # auto_tag_suffix: linux-arm64 + # dockerfile: Dockerfile.arm64 + # when: + # event: [ push, tag ] + + # publish_linux_arm: + # image: plugins/docker:17.12 + # pull: true + # secrets: [ docker_username, docker_password ] + # group: docker + # repo: plugins/ansible + # auto_tag: true + # auto_tag_suffix: linux-arm + # dockerfile: Dockerfile.arm + # when: + # event: [ push, tag ] + + manifests: + image: plugins/manifest:1 + pull: true + secrets: [ docker_username, docker_password ] + spec: manifest.tmpl + auto_tag: true + ignore_missing: true + when: + event: [ push, tag ] + + microbadger: + image: plugins/webhook:1 + pull: true + secrets: [ webhook_url ] + when: + status: [ success ] diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..e69de29 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d57256f --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +release/ +vendor/ + +coverage.out +drone-ansible diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..776c386 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM plugins/base:amd64 + +LABEL maintainer="Drone.IO Community " \ + org.label-schema.name="Drone Ansible" \ + org.label-schema.vendor="Drone.IO Community" \ + org.label-schema.schema-version="1.0" + +RUN apk add --no-cache bash git curl ansible py-pip py-requests && \ + pip install -U pip + +ADD release/linux/amd64/drone-ansible /bin/ +ENTRYPOINT ["/bin/drone-ansible"] diff --git a/Dockerfile.arm b/Dockerfile.arm new file mode 100644 index 0000000..d699afa --- /dev/null +++ b/Dockerfile.arm @@ -0,0 +1,12 @@ +FROM plugins/base:arm + +LABEL maintainer="Drone.IO Community " \ + org.label-schema.name="Drone Ansible" \ + org.label-schema.vendor="Drone.IO Community" \ + org.label-schema.schema-version="1.0" + +RUN apk add --no-cache bash git curl ansible py-pip && \ + pip install -U pip + +ADD release/linux/arm/drone-ansible /bin/ +ENTRYPOINT ["/bin/drone-ansible"] diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 new file mode 100644 index 0000000..b294b53 --- /dev/null +++ b/Dockerfile.arm64 @@ -0,0 +1,12 @@ +FROM plugins/base:arm64 + +LABEL maintainer="Drone.IO Community " \ + org.label-schema.name="Drone Ansible" \ + org.label-schema.vendor="Drone.IO Community" \ + org.label-schema.schema-version="1.0" + +RUN apk add --no-cache bash git curl ansible py-pip && \ + pip install -U pip + +ADD release/linux/arm64/drone-ansible /bin/ +ENTRYPOINT ["/bin/drone-ansible"] diff --git a/Dockerfile.i386 b/Dockerfile.i386 new file mode 100644 index 0000000..1a6b720 --- /dev/null +++ b/Dockerfile.i386 @@ -0,0 +1,12 @@ +FROM plugins/base:i386 + +LABEL maintainer="Drone.IO Community " \ + org.label-schema.name="Drone Ansible" \ + org.label-schema.vendor="Drone.IO Community" \ + org.label-schema.schema-version="1.0" + +RUN apk add --no-cache bash git curl ansible py-pip && \ + pip install -U pip + +ADD release/linux/i386/drone-ansible /bin/ +ENTRYPOINT ["/bin/drone-ansible"] diff --git a/Dockerfile.windows b/Dockerfile.windows new file mode 100644 index 0000000..79040e9 --- /dev/null +++ b/Dockerfile.windows @@ -0,0 +1,14 @@ +# escape=` +FROM microsoft/nanoserver:10.0.14393.1593 + +LABEL maintainer="Drone.IO Community " ` + org.label-schema.name="Drone Ansible" ` + org.label-schema.vendor="Drone.IO Community" ` + org.label-schema.schema-version="1.0" + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# TODO: install required tools + +ADD release\drone-ansible.exe c:\drone-ansible.exe +ENTRYPOINT [ "c:\\drone-ansible.exe" ] diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..ccf698c --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,21 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/urfave/cli" + packages = ["."] + revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" + version = "v1.20.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "dc16a066ec8e18d633e963b36f9c09f580b9323f1191cd89a69eebb1852abe92" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..712cd51 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,11 @@ +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" + +[[constraint]] + name = "github.com/urfave/cli" + version = "1.20.0" + +[prune] + go-tests = true + unused-packages = true 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..0fc21a8 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# drone-ansible + +[![Build Status](http://beta.drone.io/api/badges/drone-plugins/drone-ansible/status.svg)](http://beta.drone.io/drone-plugins/drone-ansible) +[![Join the discussion at https://www.reddit.com/r/droneci/](https://img.shields.io/badge/reddit-forum-orange.svg)](https://www.reddit.com/r/droneci/) +[![Drone questions at https://stackoverflow.com](https://img.shields.io/badge/drone-stackoverflow-orange.svg)](https://stackoverflow.com/questions/tagged/drone.io) +[![Go Doc](https://godoc.org/github.com/drone-plugins/drone-ansible?status.svg)](http://godoc.org/github.com/drone-plugins/drone-ansible) +[![Go Report](https://goreportcard.com/badge/github.com/drone-plugins/drone-ansible)](https://goreportcard.com/report/github.com/drone-plugins/drone-ansible) +[![](https://images.microbadger.com/badges/image/plugins/ansible.svg)](https://microbadger.com/images/plugins/ansible "Get your own image badge on microbadger.com") + +Drone plugin to provision infrastructure with [Ansible](https://www.ansible.com/). 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-ansible/). + +## Build + +Build the binary with the following commands: + +``` +go build +``` + +## Docker + +Build the Docker image with the following commands: + +``` +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -tags netgo -o release/linux/amd64/drone-ansible +docker build --rm -t plugins/ansible . +``` + +### Usage + +``` +docker run --rm \ + -e PLUGIN_PRIVATE_KEY="$(cat ~/.ssh/id_rsa)" \ + -e PLUGIN_PLAYBOOK="deployment/playbook.yml" \ + -e PLUGIN_INVENTORY="deployment/hosts.yml" \ + -v $(pwd):$(pwd) \ + -w $(pwd) \ + plugins/ansible +``` diff --git a/main.go b/main.go new file mode 100644 index 0000000..949c698 --- /dev/null +++ b/main.go @@ -0,0 +1,245 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +var ( + version = "0.0.0" + build = "0" +) + +func main() { + app := cli.NewApp() + app.Name = "ansible plugin" + app.Usage = "ansible plugin" + app.Version = fmt.Sprintf("%s+%s", version, build) + app.Action = run + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "requirements", + Usage: "path to python requirements", + EnvVar: "PLUGIN_REQUIREMENTS", + }, + cli.StringFlag{ + Name: "galaxy", + Usage: "path to galaxy requirements", + EnvVar: "PLUGIN_GALAXY", + }, + cli.StringSliceFlag{ + Name: "inventory", + Usage: "specify inventory host path", + EnvVar: "PLUGIN_INVENTORY,PLUGIN_INVENTORIES", + }, + cli.StringSliceFlag{ + Name: "playbook", + Usage: "list of playbooks to apply", + EnvVar: "PLUGIN_PLAYBOOK,PLUGIN_PLAYBOOKS", + }, + cli.StringFlag{ + Name: "limit", + Usage: "further limit selected hosts to an additional pattern", + EnvVar: "PLUGIN_LIMIT", + }, + cli.StringFlag{ + Name: "skip-tags", + Usage: "only run plays and tasks whose tags do not match", + EnvVar: "PLUGIN_SKIP_TAGS", + }, + cli.StringFlag{ + Name: "start-at-task", + Usage: "start the playbook at the task matching this name", + EnvVar: "PLUGIN_START_AT_TASK", + }, + cli.StringFlag{ + Name: "tags", + Usage: "only run plays and tasks tagged with these values", + EnvVar: "PLUGIN_TAGS", + }, + cli.StringSliceFlag{ + Name: "extra-vars", + Usage: "set additional variables as key=value", + EnvVar: "PLUGIN_EXTRA_VARS,ANSIBLE_EXTRA_VARS", + }, + cli.StringSliceFlag{ + Name: "module-path", + Usage: "prepend paths to module library", + EnvVar: "PLUGIN_MODULE_PATH", + }, + cli.BoolFlag{ + Name: "check", + Usage: "run a check, do not apply any changes", + EnvVar: "PLUGIN_CHECK", + }, + cli.BoolFlag{ + Name: "diff", + Usage: "show the differences, may print secrets", + EnvVar: "PLUGIN_DIFF", + }, + cli.BoolFlag{ + Name: "flush-cache", + Usage: "clear the fact cache for every host in inventory", + EnvVar: "PLUGIN_FLUSH_CACHE", + }, + cli.BoolFlag{ + Name: "force-handlers", + Usage: "run handlers even if a task fails", + EnvVar: "PLUGIN_FORCE_HANDLERS", + }, + cli.BoolFlag{ + Name: "list-hosts", + Usage: "outputs a list of matching hosts", + EnvVar: "PLUGIN_LIST_HOSTS", + }, + cli.BoolFlag{ + Name: "list-tags", + Usage: "list all available tags", + EnvVar: "PLUGIN_LIST_TAGS", + }, + cli.BoolFlag{ + Name: "list-tasks", + Usage: "list all tasks that would be executed", + EnvVar: "PLUGIN_LIST_TASKS", + }, + cli.BoolFlag{ + Name: "syntax-check", + Usage: "perform a syntax check on the playbook", + EnvVar: "PLUGIN_SYNTAX_CHECK", + }, + cli.IntFlag{ + Name: "forks", + Usage: "specify number of parallel processes to use", + EnvVar: "PLUGIN_FORKS", + Value: 5, + }, + cli.StringFlag{ + Name: "vault-id", + Usage: "the vault identity to use", + EnvVar: "PLUGIN_VAULT_ID,ANSIBLE_VAULT_ID", + }, + cli.StringFlag{ + Name: "vault-password", + Usage: "the vault password to use", + EnvVar: "PLUGIN_VAULT_PASSWORD,ANSIBLE_VAULT_PASSWORD", + }, + cli.IntFlag{ + Name: "verbose", + Usage: "level of verbosity, 0 up to 4", + EnvVar: "PLUGIN_VERBOSE", + }, + cli.StringFlag{ + Name: "private-key", + Usage: "use this key to authenticate the connection", + EnvVar: "PLUGIN_PRIVATE_KEY,ANSIBLE_PRIVATE_KEY", + }, + cli.StringFlag{ + Name: "user", + Usage: "connect as this user", + EnvVar: "PLUGIN_USER,ANSIBLE_USER", + }, + cli.StringFlag{ + Name: "connection", + Usage: "connection type to use", + EnvVar: "PLUGIN_CONNECTION", + }, + cli.IntFlag{ + Name: "timeout", + Usage: "override the connection timeout in seconds", + EnvVar: "PLUGIN_TIMEOUT", + }, + cli.StringFlag{ + Name: "ssh-common-args", + Usage: "specify common arguments to pass to sftp/scp/ssh", + EnvVar: "PLUGIN_SSH_COMMON_ARGS", + }, + cli.StringFlag{ + Name: "sftp-extra-args", + Usage: "specify extra arguments to pass to sftp only", + EnvVar: "PLUGIN_SFTP_EXTRA_ARGS", + }, + cli.StringFlag{ + Name: "scp-extra-args", + Usage: "specify extra arguments to pass to scp only", + EnvVar: "PLUGIN_SCP_EXTRA_ARGS", + }, + cli.StringFlag{ + Name: "ssh-extra-args", + Usage: "specify extra arguments to pass to ssh only", + EnvVar: "PLUGIN_SSH_EXTRA_ARGS", + }, + cli.BoolFlag{ + Name: "become", + Usage: "run operations with become", + EnvVar: "PLUGIN_BECOME", + }, + cli.StringFlag{ + Name: "become-method", + Usage: "privilege escalation method to use", + EnvVar: "PLUGIN_BECOME_METHOD,ANSIBLE_BECOME_METHOD", + }, + cli.StringFlag{ + Name: "become-user", + Usage: "run operations as this user", + EnvVar: "PLUGIN_BECOME_USER,ANSIBLE_BECOME_USER", + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} + +func run(c *cli.Context) error { + plugin := Plugin{ + Config: Config{ + Requirements: c.String("requirements"), + Galaxy: c.String("galaxy"), + Inventories: c.StringSlice("inventory"), + Playbooks: c.StringSlice("playbook"), + Limit: c.String("limit"), + SkipTags: c.String("skip-tags"), + StartAtTask: c.String("start-at-task"), + Tags: c.String("tags"), + ExtraVars: c.StringSlice("extra-vars"), + ModulePath: c.StringSlice("module-path"), + Check: c.Bool("check"), + Diff: c.Bool("diff"), + FlushCache: c.Bool("flush-cache"), + ForceHandlers: c.Bool("force-handlers"), + ListHosts: c.Bool("list-hosts"), + ListTags: c.Bool("list-tags"), + ListTasks: c.Bool("list-tasks"), + SyntaxCheck: c.Bool("syntax-check"), + Forks: c.Int("forks"), + VaultID: c.String("vailt-id"), + VaultPassword: c.String("vault-password"), + Verbose: c.Int("verbose"), + PrivateKey: c.String("private-key"), + User: c.String("user"), + Connection: c.String("connection"), + Timeout: c.Int("timeout"), + SSHCommonArgs: c.String("ssh-common-args"), + SFTPExtraArgs: c.String("sftp-extra-args"), + SCPExtraArgs: c.String("scp-extra-args"), + SSHExtraArgs: c.String("ssh-extra-args"), + Become: c.Bool("become"), + BecomeMethod: c.String("become-method"), + BecomeUser: c.String("become-user"), + }, + } + + if len(plugin.Config.Playbooks) == 0 { + return errors.New("you must provide a playbook") + } + + if len(plugin.Config.Inventories) == 0 { + return errors.New("you must provide an inventory") + } + + return plugin.Exec() +} diff --git a/manifest.tmpl b/manifest.tmpl new file mode 100644 index 0000000..8443387 --- /dev/null +++ b/manifest.tmpl @@ -0,0 +1,33 @@ +image: plugins/ansible:{{#if build.tag}}{{trimPrefix build.tag "v"}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - + image: plugins/ansible:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux + - + image: plugins/ansible:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}linux-i386 + platform: + architecture: 386 + os: linux + - + image: plugins/ansible:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}linux-arm64 + platform: + architecture: arm64 + os: linux + - + image: plugins/ansible:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}linux-arm + platform: + architecture: arm + os: linux + - + image: plugins/ansible:{{#if build.tag}}{{trimPrefix build.tag "v"}}-{{/if}}windows-amd64 + platform: + architecture: amd64 + os: windows diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..f99ea78 --- /dev/null +++ b/plugin.go @@ -0,0 +1,357 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +var ansibleFolder = "/etc/ansible" +var ansibleConfig = "/etc/ansible/ansible.cfg" + +var ansibleContent = ` +[defaults] +host_key_checking = False +` + +type ( + Config struct { + Requirements string + Galaxy string + Inventories []string + Playbooks []string + Limit string + SkipTags string + StartAtTask string + Tags string + ExtraVars []string + ModulePath []string + Check bool + Diff bool + FlushCache bool + ForceHandlers bool + ListHosts bool + ListTags bool + ListTasks bool + SyntaxCheck bool + Forks int + VaultID string + VaultPassword string + VaultPasswordFile string + Verbose int + PrivateKey string + PrivateKeyFile string + User string + Connection string + Timeout int + SSHCommonArgs string + SFTPExtraArgs string + SCPExtraArgs string + SSHExtraArgs string + Become bool + BecomeMethod string + BecomeUser string + } + + Plugin struct { + Config Config + } +) + +func (p *Plugin) Exec() error { + if err := p.ansibleConfig(); err != nil { + return err + } + + if p.Config.PrivateKey != "" { + if err := p.privateKey(); err != nil { + return err + } + + defer os.Remove(p.Config.PrivateKeyFile) + } + + if p.Config.VaultPassword != "" { + if err := p.vaultPass(); err != nil { + return err + } + + defer os.Remove(p.Config.VaultPasswordFile) + } + + commands := []*exec.Cmd{ + p.versionCommand(), + } + + if p.Config.Requirements != "" { + commands = append(commands, p.requirementsCommand()) + } + + if p.Config.Galaxy != "" { + commands = append(commands, p.galaxyCommand()) + } + + for _, inventory := range p.Config.Inventories { + commands = append(commands, p.ansibleCommand(inventory)) + } + + for _, cmd := range commands { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "ANSIBLE_FORCE_COLOR=1") + + trace(cmd) + + if err := cmd.Run(); err != nil { + return err + } + } + + return nil +} + +func (p *Plugin) ansibleConfig() error { + if err := os.MkdirAll(ansibleFolder, os.ModePerm); err != nil { + return errors.Wrap(err, "failed to create ansible directory") + } + + if err := ioutil.WriteFile(ansibleConfig, []byte(ansibleContent), 0600); err != nil { + return errors.Wrap(err, "failed to create ansible config") + } + + return nil +} + +func (p *Plugin) privateKey() error { + tmpfile, err := ioutil.TempFile("", "privateKey") + + if err != nil { + return errors.Wrap(err, "failed to create private key file") + } + + if _, err := tmpfile.Write([]byte(p.Config.PrivateKey)); err != nil { + return errors.Wrap(err, "failed to write private key file") + } + + if err := tmpfile.Close(); err != nil { + return errors.Wrap(err, "failed to close private key file") + } + + p.Config.PrivateKeyFile = tmpfile.Name() + return nil +} + +func (p *Plugin) vaultPass() error { + tmpfile, err := ioutil.TempFile("", "vaultPass") + + if err != nil { + return errors.Wrap(err, "failed to create vault password file") + } + + if _, err := tmpfile.Write([]byte(p.Config.VaultPassword)); err != nil { + return errors.Wrap(err, "failed to write vault password file") + } + + if err := tmpfile.Close(); err != nil { + return errors.Wrap(err, "failed to close vault password file") + } + + p.Config.VaultPasswordFile = tmpfile.Name() + return nil +} + +func (p *Plugin) versionCommand() *exec.Cmd { + args := []string{ + "--version", + } + + return exec.Command( + "ansible", + args..., + ) +} + +func (p *Plugin) requirementsCommand() *exec.Cmd { + args := []string{ + "install", + "--upgrade", + "--requirement", + p.Config.Requirements, + } + + return exec.Command( + "pip3", + args..., + ) +} + +func (p *Plugin) galaxyCommand() *exec.Cmd { + args := []string{ + "install", + "--force", + "--role-file", + p.Config.Galaxy, + } + + if p.Config.Verbose > 0 { + args = append(args, fmt.Sprintf("-%s", strings.Repeat("v", p.Config.Verbose))) + } + + return exec.Command( + "ansible-galaxy", + args..., + ) +} + +func (p *Plugin) ansibleCommand(inventory string) *exec.Cmd { + args := []string{ + "--inventory", + inventory, + } + + if p.Config.SyntaxCheck { + args = append(args, "--syntax-check") + args = append(args, p.Config.Playbooks...) + + return exec.Command( + "ansible-playbook", + args..., + ) + } + + if p.Config.ListHosts { + args = append(args, "--list-hosts") + args = append(args, p.Config.Playbooks...) + + return exec.Command( + "ansible-playbook", + args..., + ) + } + + for _, v := range p.Config.ExtraVars { + args = append(args, "--extra-vars", v) + } + + if p.Config.Check { + args = append(args, "--check") + } + + if p.Config.Diff { + args = append(args, "--diff") + } + + if p.Config.FlushCache { + args = append(args, "--flush-cache") + } + + if p.Config.ForceHandlers { + args = append(args, "--force-handlers") + } + + if p.Config.Forks != 5 { + args = append(args, "--forks", strconv.Itoa(p.Config.Forks)) + } + + if p.Config.Limit != "" { + args = append(args, "--limit", p.Config.Limit) + } + + if p.Config.ListTags { + args = append(args, "--list-tags") + } + + if p.Config.ListTasks { + args = append(args, "--list-tasks") + } + + if len(p.Config.ModulePath) > 0 { + args = append(args, "--module-path", strings.Join(p.Config.ModulePath, ":")) + } + + if p.Config.SkipTags != "" { + args = append(args, "--skip-tags", p.Config.SkipTags) + } + + if p.Config.StartAtTask != "" { + args = append(args, "--start-at-task", p.Config.StartAtTask) + } + + if p.Config.Tags != "" { + args = append(args, "--tags", p.Config.Tags) + } + + if p.Config.VaultID != "" { + args = append(args, "--vault-id", p.Config.VaultID) + } + + if p.Config.VaultPasswordFile != "" { + args = append(args, "--vault-password-file", p.Config.VaultPasswordFile) + } + + if p.Config.PrivateKeyFile != "" { + args = append(args, "--private-key", p.Config.PrivateKeyFile) + } + + if p.Config.User != "" { + args = append(args, "--user", p.Config.User) + } + + if p.Config.Connection != "" { + args = append(args, "--connection", p.Config.Connection) + } + + if p.Config.Timeout != 0 { + args = append(args, "--timeout", strconv.Itoa(p.Config.Timeout)) + } + + if p.Config.SSHCommonArgs != "" { + args = append(args, "--ssh-common-args", p.Config.SSHCommonArgs) + } + + if p.Config.SFTPExtraArgs != "" { + args = append(args, "--sftp-extra-args", p.Config.SFTPExtraArgs) + } + + if p.Config.SCPExtraArgs != "" { + args = append(args, "--scp-extra-args", p.Config.SCPExtraArgs) + } + + if p.Config.SSHExtraArgs != "" { + args = append(args, "--ssh-extra-args", p.Config.SSHExtraArgs) + } + + if p.Config.Become { + args = append(args, "--become") + } + + if p.Config.BecomeMethod != "" { + args = append(args, "--become-method", p.Config.BecomeMethod) + } + + if p.Config.BecomeUser != "" { + args = append(args, "--become-user", p.Config.BecomeUser) + } + + if p.Config.Verbose > 0 { + args = append(args, fmt.Sprintf("-%s", strings.Repeat("v", p.Config.Verbose))) + } + + args = append(args, p.Config.Playbooks...) + + return exec.Command( + "ansible-playbook", + args..., + ) +} + +func trace(cmd *exec.Cmd) { + fmt.Println("$", strings.Join(cmd.Args, " ")) +}