Merge pull request #241 from coder/f0ssel/github-key
feat: Add github-upload-public-key modulepull/245/head
commit
cd0c730c95
@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 960 B |
@ -0,0 +1,53 @@
|
||||
---
|
||||
display_name: Github Upload Public Key
|
||||
description: Automates uploading Coder public key to Github so users don't have to.
|
||||
icon: ../.icons/github.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, git]
|
||||
---
|
||||
|
||||
# github-upload-public-key
|
||||
|
||||
Templates that utilize Github External Auth can automatically ensure that the Coder public key is uploaded to Github so that users can clone repositories without needing to upload the public key themselves.
|
||||
|
||||
```tf
|
||||
module "github-upload-public-key" {
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
# Requirements
|
||||
|
||||
This module requires `curl` and `jq` to be installed inside your workspace.
|
||||
|
||||
Github External Auth must be enabled in the workspace for this module to work. The Github app that is configured for external auth must have both read and write permissions to "Git SSH keys" in order to upload the public key. Additionally, a Coder admin must also have the `admin:public_key` scope added to the external auth configuration of the Coder deployment. For example:
|
||||
|
||||
```
|
||||
CODER_EXTERNAL_AUTH_0_ID="USER_DEFINED_ID"
|
||||
CODER_EXTERNAL_AUTH_0_TYPE=github
|
||||
CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx
|
||||
CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx
|
||||
CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key"
|
||||
```
|
||||
|
||||
Note that the default scopes if not provided are `repo,workflow`. If the module is failing to complete after updating the external auth configuration, instruct users of the module to "Unlink" and "Link" their Github account in the External Auth user settings page to get the new scopes.
|
||||
|
||||
# Example
|
||||
|
||||
Using a coder github external auth with a non-default id: (default is `github`)
|
||||
|
||||
```tf
|
||||
data "coder_external_auth" "github" {
|
||||
id = "myauthid"
|
||||
}
|
||||
|
||||
module "github-upload-public-key" {
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.example.id
|
||||
external_auth_id = data.coder_external_auth.github.id
|
||||
}
|
||||
```
|
@ -0,0 +1,128 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
createJSONResponse,
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
writeCoder,
|
||||
} from "../test";
|
||||
import { Server, serve } from "bun";
|
||||
|
||||
describe("github-upload-public-key", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("creates new key if one does not exist", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
await writeCoder(id, "echo foo");
|
||||
let exec = await execContainer(id, [
|
||||
"env",
|
||||
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
|
||||
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key has been added to GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
// we need to increase timeout to pull the container
|
||||
}, 15000);
|
||||
|
||||
it("does nothing if one already exists", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
// use keyword to make server return a existing key
|
||||
await writeCoder(id, "echo findkey");
|
||||
let exec = await execContainer(id, [
|
||||
"env",
|
||||
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
|
||||
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key is already on GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const setupContainer = async (
|
||||
image = "lorello/alpine-bash",
|
||||
vars: Record<string, string> = {},
|
||||
) => {
|
||||
const server = await setupServer();
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
return { id, instance, server };
|
||||
};
|
||||
|
||||
const setupServer = async (): Promise<Server> => {
|
||||
let url: URL;
|
||||
const fakeSlackHost = serve({
|
||||
fetch: (req) => {
|
||||
url = new URL(req.url);
|
||||
if (url.pathname === "/api/v2/users/me/gitsshkey") {
|
||||
return createJSONResponse({
|
||||
public_key: "exists",
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === "/user/keys") {
|
||||
if (req.method === "POST") {
|
||||
return createJSONResponse(
|
||||
{
|
||||
key: "created",
|
||||
},
|
||||
201,
|
||||
);
|
||||
}
|
||||
|
||||
// case: key already exists
|
||||
if (req.headers.get("Authorization") == "Bearer findkey") {
|
||||
return createJSONResponse([
|
||||
{
|
||||
key: "foo",
|
||||
},
|
||||
{
|
||||
key: "exists",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// case: key does not exist
|
||||
return createJSONResponse([
|
||||
{
|
||||
key: "foo",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return createJSONResponse(
|
||||
{
|
||||
error: "not_found",
|
||||
},
|
||||
404,
|
||||
);
|
||||
},
|
||||
port: 0,
|
||||
});
|
||||
|
||||
return fakeSlackHost;
|
||||
};
|
@ -0,0 +1,42 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "external_auth_id" {
|
||||
type = string
|
||||
description = "The ID of the GitHub external auth."
|
||||
default = "github"
|
||||
}
|
||||
|
||||
variable "github_api_url" {
|
||||
type = string
|
||||
description = "The URL of the GitHub instance."
|
||||
default = "https://api.github.com"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
resource "coder_script" "github_upload_public_key" {
|
||||
agent_id = var.agent_id
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
CODER_OWNER_SESSION_TOKEN : data.coder_workspace.me.owner_session_token,
|
||||
CODER_ACCESS_URL : data.coder_workspace.me.access_url,
|
||||
CODER_EXTERNAL_AUTH_ID : var.external_auth_id,
|
||||
GITHUB_API_URL : var.github_api_url,
|
||||
})
|
||||
display_name = "Github Upload Public Key"
|
||||
icon = "/icon/github.svg"
|
||||
run_on_start = true
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$CODER_ACCESS_URL" ]; then
|
||||
if [ -z "${CODER_ACCESS_URL}" ]; then
|
||||
echo "CODER_ACCESS_URL is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_ACCESS_URL=${CODER_ACCESS_URL}
|
||||
fi
|
||||
|
||||
if [ -z "$CODER_OWNER_SESSION_TOKEN" ]; then
|
||||
if [ -z "${CODER_OWNER_SESSION_TOKEN}" ]; then
|
||||
echo "CODER_OWNER_SESSION_TOKEN is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_OWNER_SESSION_TOKEN=${CODER_OWNER_SESSION_TOKEN}
|
||||
fi
|
||||
|
||||
if [ -z "$CODER_EXTERNAL_AUTH_ID" ]; then
|
||||
if [ -z "${CODER_EXTERNAL_AUTH_ID}" ]; then
|
||||
echo "CODER_EXTERNAL_AUTH_ID is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_EXTERNAL_AUTH_ID=${CODER_EXTERNAL_AUTH_ID}
|
||||
fi
|
||||
|
||||
if [ -z "$GITHUB_API_URL" ]; then
|
||||
if [ -z "${GITHUB_API_URL}" ]; then
|
||||
echo "GITHUB_API_URL is empty!"
|
||||
exit 1
|
||||
fi
|
||||
GITHUB_API_URL=${GITHUB_API_URL}
|
||||
fi
|
||||
|
||||
echo "Fetching GitHub token..."
|
||||
GITHUB_TOKEN=$(coder external-auth access-token $CODER_EXTERNAL_AUTH_ID)
|
||||
if [ $? -ne 0 ]; then
|
||||
printf "Authenticate with Github to automatically upload Coder public key:\n$GITHUB_TOKEN\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching public key from Coder..."
|
||||
PUBLIC_KEY_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-w "\n%%{http_code}" \
|
||||
-H 'accept: application/json' \
|
||||
-H "cookie: coder_session_token=$CODER_OWNER_SESSION_TOKEN" \
|
||||
"$CODER_ACCESS_URL/api/v2/users/me/gitsshkey"
|
||||
)
|
||||
PUBLIC_KEY_RESPONSE_STATUS=$(tail -n1 <<< "$PUBLIC_KEY_RESPONSE")
|
||||
PUBLIC_KEY_BODY=$(sed \$d <<< "$PUBLIC_KEY_RESPONSE")
|
||||
|
||||
if [ "$PUBLIC_KEY_RESPONSE_STATUS" -ne 200 ]; then
|
||||
echo "Failed to fetch Coder public SSH key with status code $PUBLIC_KEY_RESPONSE_STATUS!"
|
||||
echo "$PUBLIC_KEY_BODY"
|
||||
exit 1
|
||||
fi
|
||||
PUBLIC_KEY=$(jq -r '.public_key' <<< "$PUBLIC_KEY_BODY")
|
||||
if [ -z "$PUBLIC_KEY" ]; then
|
||||
echo "No Coder public SSH key found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching public keys from GitHub..."
|
||||
GITHUB_KEYS_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-w "\n%%{http_code}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
$GITHUB_API_URL/user/keys
|
||||
)
|
||||
GITHUB_KEYS_RESPONSE_STATUS=$(tail -n1 <<< "$GITHUB_KEYS_RESPONSE")
|
||||
GITHUB_KEYS_RESPONSE_BODY=$(sed \$d <<< "$GITHUB_KEYS_RESPONSE")
|
||||
|
||||
if [ "$GITHUB_KEYS_RESPONSE_STATUS" -ne 200 ]; then
|
||||
echo "Failed to fetch Coder public SSH key with status code $GITHUB_KEYS_RESPONSE_STATUS!"
|
||||
echo "$GITHUB_KEYS_RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GITHUB_MATCH=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$GITHUB_KEYS_RESPONSE_BODY")
|
||||
|
||||
if [ "$PUBLIC_KEY" = "$GITHUB_MATCH" ]; then
|
||||
echo "Your Coder public key is already on GitHub!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Your Coder public key is not in GitHub. Adding it now..."
|
||||
CODER_PUBLIC_KEY_NAME="$CODER_ACCESS_URL Workspaces"
|
||||
UPLOAD_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-X POST \
|
||||
-w "\n%%{http_code}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
$GITHUB_API_URL/user/keys \
|
||||
-d "{\"title\":\"$CODER_PUBLIC_KEY_NAME\",\"key\":\"$PUBLIC_KEY\"}"
|
||||
)
|
||||
UPLOAD_RESPONSE_STATUS=$(tail -n1 <<< "$UPLOAD_RESPONSE")
|
||||
UPLOAD_RESPONSE_BODY=$(sed \$d <<< "$UPLOAD_RESPONSE")
|
||||
|
||||
if [ "$UPLOAD_RESPONSE_STATUS" -ne 201 ]; then
|
||||
echo "Failed to upload Coder public SSH key with status code $UPLOAD_RESPONSE_STATUS!"
|
||||
echo "$UPLOAD_RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Your Coder public key has been added to GitHub!"
|
Loading…
Reference in New Issue