diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..88972e26 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build docker image +on: + push: + branches: ["master"] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: docker/setup-buildx-action@v2 + + - uses: docker/login-action@v2 + with: + registry: "ghcr.io" + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v4 + id: meta + with: + images: "ghcr.io/vimeda/helm" + tags: | + ${{ github.sha }} + latest + + - uses: actions/checkout@v3 + + - uses: docker/build-push-action@v3 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index d9595117..9b6f0ec0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,15 @@ -FROM alpine:3.10.2 +FROM alpine:3.16.0 ENV BASE_URL="https://get.helm.sh" ENV HELM_2_FILE="helm-v2.17.0-linux-amd64.tar.gz" -ENV HELM_3_FILE="helm-v3.4.2-linux-amd64.tar.gz" +ENV HELM_3_FILE="helm-v3.8.1-linux-amd64.tar.gz" + + +ENV PYTHONPATH "/usr/lib/python3.11/site-packages/" RUN apk add --no-cache ca-certificates \ - --repository http://dl-3.alpinelinux.org/alpine/edge/community/ \ + --repository https://dl-3.alpinelinux.org/alpine/latest-stable/community/ \ jq curl bash nodejs aws-cli && \ # Install helm version 2: curl -L ${BASE_URL}/${HELM_2_FILE} |tar xvz && \ @@ -21,7 +24,6 @@ RUN apk add --no-cache ca-certificates \ # Init version 2 helm: helm init --client-only -ENV PYTHONPATH "/usr/lib/python3.8/site-packages/" COPY . /usr/src/ ENTRYPOINT ["node", "/usr/src/index.js"] diff --git a/README.md b/README.md index 4b4af856..fb19eae4 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ payload if the action was triggered by a deployment. - `namespace`: Kubernetes namespace name. (required) - `chart`: Helm chart path. If set to "app" this will use the built in helm chart found in this repository. (required) -- `chart_version`: The version of the helm chart you want to deploy (distinct from app version) +- `chart-version`: The version of the helm chart you want to deploy (distinct from app version) - `values`: Helm chart values, expected to be a YAML or JSON string. - `track`: Track for the deployment. If the track is not "stable" it activates the canary workflow described below. @@ -34,7 +34,10 @@ payload if the action was triggered by a deployment. - `helm`: Helm binary to execute, one of: [`helm`, `helm3`]. - `version`: Version of the app, usually commit sha works here. - `timeout`: specify a timeout for helm deployment -- `repository`: specify the URL for a helm repo to come from +- `repo`: Helm chart repository to be added. +- `repo-alias`: Helm repository alias that will be used. +- `repo-username`: Helm repository username if authentication is needed. +- `repo-password`: Helm repository password if authentication is needed. - `atomic`: If true, upgrade process rolls back changes made in case of failed upgrade. Defaults to true. Additional parameters: If the action is being triggered by a deployment event @@ -43,8 +46,8 @@ action will execute a `helm delete $service` #### Versions -- `helm`: v2.16.1 -- `helm3`: v3.0.0 +- `helm`: v2.17.0 +- `helm3`: v3.8.1 ### Environment @@ -158,3 +161,34 @@ jobs: env: KUBECONFIG_FILE: '${{ secrets.KUBECONFIG }}' ``` + +## Example add custom repository + +Sometime you may want to add a custom repository, like [chartmuseum](https://github.com/helm/chartmuseum). For th + +```yaml +# .github/workflows/pr-cleanup.yml +name: PRCleanup +on: + pull_request: + types: [closed] + +jobs: + deployment: + runs-on: 'ubuntu-latest' + steps: + - name: 'Deploy' + uses: 'deliverybot/helm@v1' + with: + release: 'nginx' + namespace: 'default' + chart: 'chartmuseum/app' + token: '${{ github.token }}' + repo: 'http://chartmuseum.example.com' + repo-alias: chartmuseum + repo-username: ${{ secrets.CHARTMUSEUM_USERNAME }} + repo-password: ${{ secrets.CHARTMUSEUM_PASSWORD }} + env: + KUBECONFIG_FILE: '${{ secrets.KUBECONFIG }}' +``` + diff --git a/action.yml b/action.yml index 8679e0ea..afd6124b 100644 --- a/action.yml +++ b/action.yml @@ -1,8 +1,9 @@ -name: Deliverybot Helm Action -description: Deploys a helm chart -author: deliverybot -icon: box -color: gray-dark +name: Helm Deploy +description: Deploys a helm chart to Kubernetes +author: lykon +branding: + icon: anchor + color: gray-dark inputs: release: description: Helm release name. Will be combined with track if set. (required) @@ -45,6 +46,24 @@ inputs: version: description: Version of the app, usually commit sha works here. required: false + chart-version: + description: The version of the helm chart you want to deploy (distinct from app version) + required: false + track: + description: Track for the deployment. If the track is not "stable" it activates the canary workflow described in the docs. + required: false + repo: + description: Helm chart repository to be added. + required: false + repo-alias: + description: Helm repository alias that will be used. + required: false + repo-username: + description: Helm repository username if authentication is needed. + required: false + repo-password: + description: Helm repository password if authentication is needed. + required: false runs: using: docker - image: Dockerfile + image: docker://ghcr.io/vimeda/helm diff --git a/index.js b/index.js index e49304e6..7a4ab4dc 100644 --- a/index.js +++ b/index.js @@ -145,123 +145,169 @@ function deleteCmd(helm, namespace, release) { return ["delete", "--purge", release]; } +/* + * Optionally add a helm repository + */ +async function addRepo(helm) { + const repo = getInput("repo"); + const repoAlias = getInput("repo-alias"); + const repoUsername = getInput("repo-username"); + const repoPassword = getInput("repo-password"); + + core.debug(`param: repo = "${repo}"`); + core.debug(`param: repoAlias = "${repoAlias}"`); + core.debug(`param: repoUsername = "${repoUsername}"`); + core.debug(`param: repoPassword = "${repoPassword}"`); + + if (repo !== "") { + if (repoAlias === "") { + throw new Error("repo alias is required when you are setting a repository"); + } + + core.debug(`adding custom repository ${repo} with alias ${repoAlias}`); + + const args = [ + "repo", + "add", + repoAlias, + repo, + ] + + if (repoUsername) args.push(`--username=${repoUsername}`); + if (repoPassword) args.push(`--password=${repoPassword}`); + + await exec.exec(helm, args); + await exec.exec(helm, ["repo", "update"]) + } + + return Promise.resolve() +} + +/* + * Deploy the release + */ +async function deploy(helm) { + const context = github.context; + + const track = getInput("track") || "stable"; + const appName = getInput("release", required); + const release = releaseName(appName, track); + const namespace = getInput("namespace", required); + const chart = chartName(getInput("chart", required)); + const chartVersion = getInput("chart_version"); + const values = getValues(getInput("values")); + const task = getInput("task"); + const version = getInput("version"); + const valueFiles = getValueFiles(getInput("value_files")); + const removeCanary = getInput("remove_canary"); + const timeout = getInput("timeout"); + const dryRun = core.getInput("dry-run"); + const secrets = getSecrets(core.getInput("secrets")); + const atomic = getInput("atomic") || true; + + core.debug(`param: track = "${track}"`); + core.debug(`param: release = "${release}"`); + core.debug(`param: appName = "${appName}"`); + core.debug(`param: namespace = "${namespace}"`); + core.debug(`param: chart = "${chart}"`); + core.debug(`param: chart_version = "${chartVersion}"`); + core.debug(`param: values = "${values}"`); + core.debug(`param: dryRun = "${dryRun}"`); + core.debug(`param: task = "${task}"`); + core.debug(`param: version = "${version}"`); + core.debug(`param: secrets = "${JSON.stringify(secrets)}"`); + core.debug(`param: valueFiles = "${JSON.stringify(valueFiles)}"`); + core.debug(`param: removeCanary = ${removeCanary}`); + core.debug(`param: timeout = "${timeout}"`); + core.debug(`param: atomic = "${atomic}"`); + + // Setup command options and arguments. + let args = [ + "upgrade", + release, + chart, + "--install", + "--wait", + `--namespace=${namespace}`, + ]; + + if (dryRun) args.push("--dry-run"); + if (appName) args.push(`--set=app.name=${appName}`); + if (version) args.push(`--set=app.version=${version}`); + if (chartVersion) args.push(`--version=${chartVersion}`); + if (timeout) args.push(`--timeout=${timeout}`); + + valueFiles.forEach(f => args.push(`--values=${f}`)); + + args.push("--values=./values.yml"); + + // Special behaviour is triggered if the track is labelled 'canary'. The + // service and ingress resources are disabled. Access to the canary + // deployments can be routed via the main stable service resource. + if (track === "canary") { + args.push("--set=service.enabled=false", "--set=ingress.enabled=false"); + } + + // If true upgrade process rolls back changes made in case of failed upgrade. + if (atomic === true) { + args.push("--atomic"); + } + + await writeFile("./values.yml", values); + + core.debug(`env: KUBECONFIG="${process.env.KUBECONFIG}"`); + + // Render value files using github variables. + await renderFiles(valueFiles.concat(["./values.yml"]), { + secrets, + deployment: context.payload.deployment, + }); + + // Remove the canary deployment before continuing. + if (removeCanary) { + core.debug(`removing canary ${appName}-canary`); + await exec.exec(helm, deleteCmd(helm, namespace, `${appName}-canary`), { + ignoreReturnCode: true + }); + } + + // Actually execute the deployment here. + if (task === "remove") { + return exec.exec(helm, deleteCmd(helm, namespace, release), { + ignoreReturnCode: true + }); + } + + return exec.exec(helm, args); +} + /** * Run executes the helm deployment. */ async function run() { + const commands = [addRepo, deploy] + try { - const context = github.context; await status("pending"); - const track = getInput("track") || "stable"; - const appName = getInput("release", required); - const release = releaseName(appName, track); - const namespace = getInput("namespace", required); - const chart = chartName(getInput("chart", required)); - const chartVersion = getInput("chart_version"); - const values = getValues(getInput("values")); - const task = getInput("task"); - const version = getInput("version"); - const valueFiles = getValueFiles(getInput("value_files")); - const removeCanary = getInput("remove_canary"); - const helm = getInput("helm") || "helm"; - const timeout = getInput("timeout"); - const repository = getInput("repository"); - const dryRun = core.getInput("dry-run"); - const secrets = getSecrets(core.getInput("secrets")); - const atomic = getInput("atomic") || true; - - core.debug(`param: track = "${track}"`); - core.debug(`param: release = "${release}"`); - core.debug(`param: appName = "${appName}"`); - core.debug(`param: namespace = "${namespace}"`); - core.debug(`param: chart = "${chart}"`); - core.debug(`param: chart_version = "${chartVersion}"`); - core.debug(`param: values = "${values}"`); - core.debug(`param: dryRun = "${dryRun}"`); - core.debug(`param: task = "${task}"`); - core.debug(`param: version = "${version}"`); - core.debug(`param: secrets = "${JSON.stringify(secrets)}"`); - core.debug(`param: valueFiles = "${JSON.stringify(valueFiles)}"`); - core.debug(`param: removeCanary = ${removeCanary}`); - core.debug(`param: timeout = "${timeout}"`); - core.debug(`param: repository = "${repository}"`); - core.debug(`param: atomic = "${atomic}"`); - - - // Setup command options and arguments. - const args = [ - "upgrade", - release, - chart, - "--install", - "--wait", - `--namespace=${namespace}`, - ]; - - // Per https://helm.sh/docs/faq/#xdg-base-directory-support - if (helm === "helm3") { - process.env.XDG_DATA_HOME = "/root/.helm/" - process.env.XDG_CACHE_HOME = "/root/.helm/" - process.env.XDG_CONFIG_HOME = "/root/.helm/" - } else { - process.env.HELM_HOME = "/root/.helm/" - } - - if (dryRun) args.push("--dry-run"); - if (appName) args.push(`--set=app.name=${appName}`); - if (version) args.push(`--set=app.version=${version}`); - if (chartVersion) args.push(`--version=${chartVersion}`); - if (timeout) args.push(`--timeout=${timeout}`); - if (repository) args.push(`--repo=${repository}`); - valueFiles.forEach(f => args.push(`--values=${f}`)); - args.push("--values=./values.yml"); - - // Special behaviour is triggered if the track is labelled 'canary'. The - // service and ingress resources are disabled. Access to the canary - // deployments can be routed via the main stable service resource. - if (track === "canary") { - args.push("--set=service.enabled=false", "--set=ingress.enabled=false"); - } - - // If true upgrade process rolls back changes made in case of failed upgrade. - if (atomic === true) { - args.push("--atomic"); - } - + process.env.XDG_DATA_HOME = "/root/.helm/" + process.env.XDG_CACHE_HOME = "/root/.helm/" + process.env.XDG_CONFIG_HOME = "/root/.helm/" + // Setup necessary files. if (process.env.KUBECONFIG_FILE) { process.env.KUBECONFIG = "./kubeconfig.yml"; await writeFile(process.env.KUBECONFIG, process.env.KUBECONFIG_FILE); } - await writeFile("./values.yml", values); - - core.debug(`env: KUBECONFIG="${process.env.KUBECONFIG}"`); - - // Render value files using github variables. - await renderFiles(valueFiles.concat(["./values.yml"]), { - secrets, - deployment: context.payload.deployment, - }); - - // Remove the canary deployment before continuing. - if (removeCanary) { - core.debug(`removing canary ${appName}-canary`); - await exec.exec(helm, deleteCmd(helm, namespace, `${appName}-canary`), { - ignoreReturnCode: true - }); - } + + const helm = getInput("helm") || "helm3"; + core.debug(`param: helm = "${helm}"`); - // Actually execute the deployment here. - if (task === "remove") { - await exec.exec(helm, deleteCmd(helm, namespace, release), { - ignoreReturnCode: true - }); - } else { - await exec.exec(helm, args); + for(const command of commands) { + await command(helm); } - await status(task === "remove" ? "inactive" : "success"); + await status("success"); } catch (error) { core.error(error); core.setFailed(error.message);