-
Notifications
You must be signed in to change notification settings - Fork 14
feat(g/infra): Add JAAS auth action #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hk21702
wants to merge
2
commits into
main
Choose a base branch
from
jaas-auth
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| name: JavaScript QA | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - "**/*.js" | ||
| - "**/*.mjs" | ||
| - .github/workflows/js-qa.yaml | ||
| pull_request: | ||
| paths: | ||
| - "**/*.js" | ||
| - "**/*.mjs" | ||
| - .github/workflows/js-qa.yaml | ||
|
|
||
| jobs: | ||
| check-js-syntax: | ||
| name: node --check | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v7 | ||
|
|
||
| - name: Syntax check all JS files | ||
| run: | | ||
| while IFS= read -r file; do | ||
| echo "Checking $file" | ||
| node --check "$file" | ||
|
CarlosNihelton marked this conversation as resolved.
|
||
| done < <(find . -type f \( -name '*.js' -o -name '*.mjs' \) -not -path './.git/*' -print0 | sort -z | tr '\0' '\n') | ||
|
|
||
| unit-tests: | ||
| name: Node.js unit tests | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v7 | ||
|
|
||
| - name: Run unit tests | ||
| run: node --test 'gh-actions/**/*.test.{js,mjs}' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| .vscode | ||
| .idea | ||
| .DS_Store | ||
|
|
||
| # Node dependencies for JS actions | ||
| node_modules/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| name: JAAS auth | ||
| description: >- | ||
| Authenticate to a JAAS controller as a service account using Juju client | ||
| credentials, then automatically clean up (juju logout and remove client | ||
| state) afterwards, even when the job fails. Cleanup can be disabled with the | ||
| `cleanup` input. | ||
|
|
||
| inputs: | ||
| jaas-controller: | ||
| description: Name used to register the JAAS controller in the local Juju client | ||
| required: true | ||
| jaas-controller-host: | ||
| description: JAAS controller host name or alias | ||
| required: true | ||
| juju-client-id: | ||
| description: Juju client ID for the service account | ||
| required: true | ||
| juju-client-secret: | ||
| description: Juju client secret for the service account | ||
| required: true | ||
| clear-ca-cert: | ||
| description: >- | ||
| Clear the CA certificate embedded in controllers.yaml by `juju login | ||
| --trust`, so the client falls back to the system CA store. This is a | ||
| workaround for https://github.com/juju/juju/pull/22931; once that fix is | ||
| released to the stable Juju snap this default can be flipped to false. | ||
| required: false | ||
| default: "true" | ||
| cleanup: | ||
| description: >- | ||
| Run the post-job cleanup step (juju logout and removal of the Juju | ||
| client state). Set to "false" to skip cleanup, e.g. when the runner | ||
| itself is ephemeral or you want to reuse the client state. | ||
| required: false | ||
| default: "true" | ||
|
|
||
| outputs: | ||
| juju-data: | ||
| description: >- | ||
| Directory containing the Juju client state (JUJU_DATA), set by the | ||
| main entrypoint via GITHUB_OUTPUT. | ||
|
|
||
| runs: | ||
| using: node24 | ||
| main: dist/main/index.mjs | ||
| post: dist/post/index.mjs | ||
| post-if: always() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * Clear the embedded controller CA certificate from a Juju controllers file. | ||
| * | ||
| * After `juju login --trust`, the controllers.yaml file embeds the | ||
| * controller's CA certificate. Clearing it makes the Juju client fall back to | ||
| * the runner's system CA store, which is expected to trust the JAAS endpoint | ||
| * certificate. | ||
| * | ||
| * This is a workaround for https://github.com/juju/juju/pull/22931. Once that | ||
| * fix is released to the stable Juju snap, this step can be disabled (see the | ||
| * `clear-ca-cert` input of the jaas-auth action). | ||
| */ | ||
| import fs from "node:fs"; | ||
|
|
||
| const CA_CERT_PREFIX = " ca-cert:"; | ||
| const CA_CERT_INDENT = 8; | ||
|
|
||
| /** | ||
| * Replace the controller's ca-cert value (which may be a multiline block | ||
| * scalar) with an empty string, preserving the file's permissions via an | ||
| * atomic replace. | ||
| * | ||
| * @param {string} controllersFile Path to the controllers.yaml file. | ||
| */ | ||
| export function clearCaCert(controllersFile) { | ||
| const lines = fs | ||
| .readFileSync(controllersFile, "utf8") | ||
| .split(/(?<=\n)/); | ||
|
|
||
| const caCertLines = []; | ||
| lines.forEach((line, index) => { | ||
| if (line.startsWith(CA_CERT_PREFIX)) caCertLines.push(index); | ||
| }); | ||
| if (caCertLines.length !== 1) { | ||
| throw new Error( | ||
| `expected one controller ca-cert entry, found ${caCertLines.length}`, | ||
| ); | ||
| } | ||
|
|
||
| const start = caCertLines[0]; | ||
| let end = start + 1; | ||
| while (end < lines.length) { | ||
| const line = lines[end]; | ||
| const indentation = line.length - line.trimStart().length; | ||
| if (line.trim() && indentation <= CA_CERT_INDENT) break; | ||
| end += 1; | ||
| } | ||
|
|
||
| lines.splice(start, end - start, `${CA_CERT_PREFIX} ""\n`); | ||
|
|
||
| const mode = fs.statSync(controllersFile).mode & 0o7777; | ||
| const temporaryFile = controllersFile + ".tmp"; | ||
| fs.writeFileSync(temporaryFile, lines.join(""), { encoding: "utf8" }); | ||
| fs.chmodSync(temporaryFile, mode); | ||
| fs.renameSync(temporaryFile, controllersFile); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| /** | ||
| * JAAS auth — main entrypoint. | ||
| * | ||
| * Authenticates to the JAAS controller as a service account using the given | ||
| * Juju client credentials, and outputs the JUJU_DATA directory holding the | ||
| * authenticated client state. Cleanup is handled by the companion post | ||
| * entrypoint (dist/post/index.mjs), which GitHub always runs, even when this | ||
| * step or the job fails. | ||
| */ | ||
| import { execFileSync } from "node:child_process"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| import { clearCaCert } from "../clear_ca_cert.mjs"; | ||
|
|
||
| function run(command, args, options = {}) { | ||
| execFileSync(command, args, { stdio: "inherit", ...options }); | ||
| } | ||
|
|
||
| function inputEnv(name) { | ||
| return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`; | ||
| } | ||
|
|
||
| function required(name) { | ||
| const value = process.env[inputEnv(name)]; | ||
| if (!value) throw new Error(`missing required input: ${name}`); | ||
| return value; | ||
| } | ||
|
|
||
| function input(name, fallback = "") { | ||
| return process.env[inputEnv(name)] || fallback; | ||
| } | ||
|
|
||
| function appendEnv(name, value) { | ||
| fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`); | ||
| } | ||
|
|
||
| try { | ||
| const controller = required("jaas-controller"); | ||
| const controllerHost = required("jaas-controller-host"); | ||
| const clientId = required("juju-client-id"); | ||
| const clientSecret = required("juju-client-secret"); | ||
|
|
||
| run("sudo", ["snap", "install", "juju"]); | ||
| run("juju", ["version"]); | ||
|
|
||
| // Prepare the Juju client state directory. | ||
| const jujuData = path.join(process.env.RUNNER_TEMP || "/tmp", "juju-data"); | ||
| fs.mkdirSync(jujuData, { recursive: true, mode: 0o700 }); | ||
| fs.chmodSync(jujuData, 0o700); | ||
|
|
||
| // Mask and export the credentials for later steps in this job. | ||
| console.log(`::add-mask::${clientId}`); | ||
| console.log(`::add-mask::${clientSecret}`); | ||
| appendEnv("JUJU_CLIENT_ID", clientId); | ||
| appendEnv("JUJU_CLIENT_SECRET", clientSecret); | ||
|
|
||
| const env = { | ||
| ...process.env, | ||
| JUJU_DATA: jujuData, | ||
| JUJU_CLIENT_ID: clientId, | ||
| JUJU_CLIENT_SECRET: clientSecret, | ||
| }; | ||
|
|
||
| // Authenticate to JAAS. | ||
| run( | ||
| "juju", | ||
| [ | ||
| "login", | ||
| "--no-browser-login", | ||
| "-c", | ||
| controller, | ||
| controllerHost, | ||
| "--trust", | ||
| "--no-prompt", | ||
| ], | ||
| { env, stdio: ["inherit", "ignore", "inherit"] }, | ||
| ); | ||
|
|
||
| if (input("clear-ca-cert", "true") === "true") { | ||
| clearCaCert(path.join(jujuData, "controllers.yaml")); | ||
| } | ||
|
|
||
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `juju-data=${jujuData}\n`); | ||
| // Persist for the post (cleanup) entrypoint. | ||
| appendEnv("JAAS_AUTH_JUJU_DATA", jujuData); | ||
| appendEnv("JAAS_AUTH_CLEANUP", input("cleanup", "true")); | ||
|
CarlosNihelton marked this conversation as resolved.
|
||
| } catch (error) { | ||
| console.error(`::error::${error.message}`); | ||
| process.exitCode = 1; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /** | ||
| * JAAS auth — post (cleanup) entrypoint. | ||
| * | ||
| * Runs unconditionally after the job (post-if: always()). Logs out of the Juju | ||
| * controller and removes the client state, then clears the related environment | ||
| * variables. All steps are best-effort so a failure does not mask the job's | ||
| * original result. Skipped entirely when the `cleanup` input was "false". | ||
| */ | ||
| import { execFileSync } from "node:child_process"; | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| const jujuData = | ||
| process.env.JAAS_AUTH_JUJU_DATA || | ||
| path.join(process.env.RUNNER_TEMP || "/tmp", "juju-data"); | ||
|
|
||
| function appendEnv(name, value) { | ||
| fs.appendFileSync(process.env.GITHUB_ENV, `${name}=${value}\n`); | ||
| } | ||
|
|
||
| function main() { | ||
| if ((process.env.JAAS_AUTH_CLEANUP || "true") !== "true") { | ||
| console.log("Skipping JAAS cleanup (cleanup input was not 'true')."); | ||
| return; | ||
| } | ||
|
|
||
| // Best-effort: log out of the controller before deleting its state. There is | ||
| // only one controller in this JUJU_DATA, so a bare `juju logout` suffices. | ||
| try { | ||
| execFileSync("juju", ["logout"], { | ||
| env: { ...process.env, JUJU_DATA: jujuData }, | ||
| stdio: "inherit", | ||
| }); | ||
| } catch (error) { | ||
| console.error( | ||
| `::warning::juju logout failed; removing client state anyway: ${error.message}`, | ||
| ); | ||
| } | ||
|
|
||
| fs.rmSync(jujuData, { recursive: true, force: true }); | ||
|
|
||
| appendEnv("JUJU_CLIENT_ID", ""); | ||
| appendEnv("JUJU_CLIENT_SECRET", ""); | ||
| appendEnv("JUJU_DATA", ""); | ||
|
CarlosNihelton marked this conversation as resolved.
|
||
| } | ||
|
|
||
| main(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { test } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| import { clearCaCert } from "../dist/clear_ca_cert.mjs"; | ||
|
|
||
| function writeControllers(t, content) { | ||
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clear-ca-cert-")); | ||
| t.after(() => fs.rmSync(dir, { recursive: true, force: true })); | ||
| const controllersFile = path.join(dir, "controllers.yaml"); | ||
| fs.writeFileSync(controllersFile, content, { encoding: "utf8" }); | ||
| fs.chmodSync(controllersFile, 0o600); | ||
| return controllersFile; | ||
| } | ||
|
|
||
| test("clears multiline ca-cert", (t) => { | ||
| const controllersFile = writeControllers( | ||
| t, | ||
| `controllers: | ||
| jaas: | ||
| uuid: test | ||
| api-endpoints: [jaas.example.com:443/api] | ||
| ca-cert: |- | ||
| -----BEGIN CERTIFICATE----- | ||
| certificate-data | ||
| -----END CERTIFICATE----- | ||
| cloud: "" | ||
| current-controller: jaas | ||
| `, | ||
| ); | ||
|
|
||
| clearCaCert(controllersFile); | ||
|
|
||
| assert.equal( | ||
| fs.readFileSync(controllersFile, "utf8"), | ||
| `controllers: | ||
| jaas: | ||
| uuid: test | ||
| api-endpoints: [jaas.example.com:443/api] | ||
| ca-cert: "" | ||
| cloud: "" | ||
| current-controller: jaas | ||
| `, | ||
| ); | ||
| assert.equal(fs.statSync(controllersFile).mode & 0o777, 0o600); | ||
| }); | ||
|
|
||
| test("empty ca-cert is idempotent", (t) => { | ||
| const content = `controllers: | ||
| jaas: | ||
| uuid: test | ||
| ca-cert: "" | ||
| cloud: "" | ||
| current-controller: jaas | ||
| `; | ||
| const controllersFile = writeControllers(t, content); | ||
|
|
||
| clearCaCert(controllersFile); | ||
|
|
||
| assert.equal(fs.readFileSync(controllersFile, "utf8"), content); | ||
| }); | ||
|
|
||
| test("missing ca-cert raises", (t) => { | ||
| const controllersFile = writeControllers( | ||
| t, | ||
| `controllers: | ||
| jaas: | ||
| uuid: test | ||
| current-controller: jaas | ||
| `, | ||
| ); | ||
|
|
||
| assert.throws(() => clearCaCert(controllersFile), Error); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.