From c368992ffbfd89a9479eedeb3b9d0f36c95d4bf0 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Wed, 25 Jan 2023 00:30:36 -0500 Subject: [PATCH] feat: implement the action in typescript --- login-kubectl.sh | 7 --- setup-kubectl.sh | 10 ---- src/login.ts | 28 +++++++++++ src/main.ts | 14 ++++++ src/setup.ts | 126 +++++++++++++++++++++++++++++++++++++++++++++++ src/teardown.ts | 14 ++++++ 6 files changed, 182 insertions(+), 17 deletions(-) delete mode 100755 login-kubectl.sh delete mode 100755 setup-kubectl.sh create mode 100644 src/login.ts create mode 100644 src/main.ts create mode 100644 src/setup.ts create mode 100644 src/teardown.ts diff --git a/login-kubectl.sh b/login-kubectl.sh deleted file mode 100755 index 1b22e94..0000000 --- a/login-kubectl.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -if [ ! -d "$HOME/.kube" ]; then - mkdir -p $HOME/.kube -fi - -echo "$BASE64_KUBE_CONFIG" | base64 -d > $HOME/.kube/config diff --git a/setup-kubectl.sh b/setup-kubectl.sh deleted file mode 100755 index e101a11..0000000 --- a/setup-kubectl.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" -curl -LO "https://dl.k8s.io/$KUBECTL_VERSION/bin/linux/amd64/kubectl.sha256" -echo "$(cat kubectl.sha256) kubectl" | sha256sum --check -rm -f kubectl.sha256 - -mkdir -p $RUNNER_TEMP/bin -mv kubectl $RUNNER_TEMP/bin -echo "$RUNNER_TEMP/bin" >> $GITHUB_PATH diff --git a/src/login.ts b/src/login.ts new file mode 100644 index 0000000..e26b106 --- /dev/null +++ b/src/login.ts @@ -0,0 +1,28 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { env } from 'node:process' + +import { debug, getInput, saveState, setFailed } from '@actions/core' + +export async function setupKubeconfig() { + debug('Running kubectl-action setupKubeconfig()') + + if (env.HOME === undefined) { + setFailed('$HOME is not defined') + return + } + + const config = getInput('base64-kube-config', { + required: true, + trimWhitespace: true + }) + + const decoded = Buffer.from(config, 'base64') + .toString('utf8') + + const path = join(env.HOME, '.kube') + saveState('kubeconfig-path', path) + + await mkdir(path, { recursive: true }) + await writeFile(join(path, 'config'), decoded, 'utf8') +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..27f00c4 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,14 @@ +/* eslint-disable unicorn/prefer-top-level-await */ +import { debug, getState, setFailed } from '@actions/core' +import { installKubectl } from 'setup' + +const post = Boolean(getState('isPost')) + +if (!post) { + debug('Running kubectl-action setup') + installKubectl() + .catch(error => { + setFailed('Failed to install kubectl (this is a bug in kubectl-action): ') + debug(JSON.stringify(error)) + }) +} diff --git a/src/setup.ts b/src/setup.ts new file mode 100644 index 0000000..5a20c48 --- /dev/null +++ b/src/setup.ts @@ -0,0 +1,126 @@ +import { createHash, randomUUID } from 'node:crypto' +import { createWriteStream } from 'node:fs' +import { mkdir } from 'node:fs/promises' +import { join } from 'node:path' +import { env, stdout } from 'node:process' +import { Readable } from 'node:stream' + +import { addPath, debug, getInput, saveState, setFailed, warning } from '@actions/core' +import { fetch } from 'undici' + +export async function installKubectl() { + debug('Running kubectl-action installKubectl()') + + if (env.RUNNER_TEMP === undefined) { + setFailed('$RUNNER_TEMP is not defined') + return + } + + const input = getInput('kubectl-version', { + required: false, + trimWhitespace: true + }) + + const version = input || await fetchLatestVersion() + + if (!version?.startsWith('v')) { + setFailed('Unable to determine the `kubectl` version to install') + return + } + + console.log(`Installing kubectl version ${version}`) + + const kubectl = await downloadKubectl(version) + + if (!kubectl) { + return + } + + const path = join(env.RUNNER_TEMP, randomUUID()) + await mkdir(path, { recursive: true }) + saveState('kubectl-path', path) + + const stream = createWriteStream(join(path, 'kubectl')) + + kubectl.pipe(stream) + + console.log(`Installing kubectl to ${path}`) + await new Promise((resolve, reject) => { + stream.on('finish', resolve) + stream.on('error', reject) + }) + + addPath(path) +} + +// Fetches the latest kubectl version from the Kubernetes release server +async function fetchLatestVersion() { + const response = await fetch('https://dl.k8s.io/release/stable.txt') + if (!response.ok) { + setFailed(`Failed to fetch latest kubectl version with status ${response.status}`) + return + } + + const version = await response.text() + return version.trim() +} + +// Downloads the kubectl binary from the Kubernetes release server +// Also runs a checksum verification on the downloaded binary +async function downloadKubectl(version: string) { + const url = `https://dl.k8s.io/release/${version}/bin/linux/amd64/kubectl` + const hashUrl = `${url}.sha256` + + console.log(`Downloading kubectl (${url})`) + + const hashResponse = await fetch(hashUrl) + if (!hashResponse.ok) { + warning(`Skipping checksum verification for kubectl ${version}`) + } + + const hash = hashResponse.ok ? await hashResponse.text() : '' + + const response = await fetch(url) + if (!response.ok || !response.body) { + setFailed(`Failed to download kubectl with status ${response.status}`) + return + } + + const hashStream = createHash('sha256') + const body = Readable.fromWeb(response.body) + const size = Number(response.headers.get('content-length')) + + return new Promise((resolve, reject) => { + let downloaded = 0 + let progressed = 0 + + body.on('data', (chunk: Buffer) => { + hashStream.update(chunk) + downloaded += chunk.length + + if (Math.floor((downloaded / size) * 80) > progressed) { + stdout.clearLine(0) + stdout.cursorTo(0) + + progressed++ + stdout.write(`[${'='.repeat(progressed)}>${' '.repeat(80 - progressed)}]`) + } + }) + + body.on('end', () => { + stdout.clearLine(0) + stdout.cursorTo(0) + console.log(`[${'='.repeat(80)}]`) + + const hashSum = hashStream.digest('hex') + if (hashResponse.ok && hashSum !== hash) { + setFailed(`Checksum verification failed for kubectl ${version}`) + resolve() + } + + resolve(body) + }) + + body.on('error', reject) + }) +} diff --git a/src/teardown.ts b/src/teardown.ts new file mode 100644 index 0000000..558a79c --- /dev/null +++ b/src/teardown.ts @@ -0,0 +1,14 @@ +import { rm } from 'node:fs/promises' + +import { debug, getState } from '@actions/core' + +export async function teardown() { + debug('Running kubectl-action teardown()') + console.log('Removing kubectl and kubeconfig') + + const path = getState('kubectl-path') + await rm(path, { recursive: true, force: true }) + + const configPath = getState('kubeconfig-path') + await rm(configPath, { recursive: true, force: true }) +}