This repository was archived by the owner on May 15, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 59
feat: Add github-upload-public-key module #241
Merged
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
b2e87ef
feat: Add github-upload-public-key module
f0ssel c9e418a
improve status code handling and add readme
f0ssel edc163b
fix testing
f0ssel 1273378
Update README.md
f0ssel e8ce194
use code cli for token and update readme
f0ssel 2e43788
heading
f0ssel 428f386
add troubleshooting
f0ssel 5a33af2
fmt
f0ssel cff60c4
add auth id var
f0ssel 5030fcb
add coder workspace me
f0ssel a45706a
fix Invalid template control keyword
f0ssel 180e10c
require curl and jq
f0ssel 46bf422
maintainer
f0ssel 36fa871
add tests
f0ssel aced754
fmt
f0ssel 67fef29
increase test timeout
f0ssel a239212
fmt and increase timeout again
f0ssel daed803
pr review
f0ssel 4bdb428
fix test
f0ssel 85e73c2
fmt
f0ssel c068082
pr comments
f0ssel 282e1f8
take env and then interpolate
f0ssel 873207f
remove set -e
f0ssel 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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,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 | ||
} | ||
``` |
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,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; | ||
}; |
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,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 | ||
} |
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,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!" |
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
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
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.