Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions registry/coder/modules/git-clone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
Expand All @@ -28,7 +28,7 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
Expand All @@ -43,7 +43,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
Expand All @@ -70,7 +70,7 @@ data "coder_parameter" "git_repo" {
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
Expand Down Expand Up @@ -105,7 +105,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
Expand All @@ -125,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
Expand All @@ -137,7 +137,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
Expand All @@ -159,7 +159,7 @@ For example, to clone the `feat/example` branch:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
Expand All @@ -177,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
Expand All @@ -202,7 +202,7 @@ fetches, or partial clones.
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
extra_args = [
Expand All @@ -223,7 +223,7 @@ This is useful for preparing the environment or validating prerequisites before
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
pre_clone_script = <<-EOT
Expand All @@ -246,7 +246,7 @@ This is useful for running initialization tasks like installing dependencies or
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "2.0.3"
version = "2.0.4"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT
Expand Down
120 changes: 120 additions & 0 deletions registry/coder/modules/git-clone/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,75 @@ describe("git-clone", async () => {
expect(output.exitCode).toBe(0);
});

it("does not treat an scp-style repository path as an SSH port", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "git@forgejo.example.com:2222/example/repo.git",
base_dir: "/tmp",
});
const setupFakeTools = [
installFakeGit,
"cat > /usr/local/bin/ssh-keyscan <<'SHIM'",
"#!/bin/sh",
'for arg in "$@"; do',
" [ \"$arg\" = '-p' ] && exit 1",
"done",
"echo 'forgejo.example.com ssh-rsa AAAAB3NzaC1yc2EFAKE'",
"SHIM",
"chmod +x /usr/local/bin/ssh-keyscan",
].join("\n");
const output = await executeScriptInContainer(
state,
"alpine/git",
setupFakeTools,
);
expect(output.stdout).toContain(
"Adding host key for forgejo.example.com to known_hosts...",
);
expect(output.stdout.join("\n")).not.toContain("ssh-keyscan failed");
expect(output.exitCode).toBe(0);
});

it("adds SSH host key from a non-default port to known_hosts", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "ssh://git@forgejo.example.com:2222/example/repo.git",
base_dir: "/tmp",
});
const setupFakeTools = [
installFakeGit,
"cat > /usr/local/bin/ssh-keyscan <<'SHIM'",
"#!/bin/sh",
"port=''",
"host=''",
'while [ "$#" -gt 0 ]; do',
' case "$1" in',
' -p) port="$2"; shift 2 ;;',
" -t) shift 2 ;;",
" -H) shift ;;",
' *) host="$1"; shift ;;',
" esac",
"done",
'[ "$port" = "2222" ] && [ "$host" = "forgejo.example.com" ] || exit 1',
"echo '[forgejo.example.com]:2222 ssh-rsa AAAAB3NzaC1yc2EFAKE'",
"SHIM",
"chmod +x /usr/local/bin/ssh-keyscan",
].join("\n");
const output = await executeScriptInContainer(
state,
"alpine/git",
setupFakeTools,
);
expect(output.stdout).toContain(
"Adding host key for [forgejo.example.com]:2222 to known_hosts...",
);
expect(output.stdout).toContain(
"Host key for [forgejo.example.com]:2222 added to known_hosts.",
);
expect(output.stdout.join("\n")).not.toContain("ssh-keyscan failed");
expect(output.exitCode).toBe(0);
});

it("uses StrictHostKeyChecking=accept-new when ssh-keyscan is unavailable for SSH URLs", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
Expand Down Expand Up @@ -539,6 +608,57 @@ describe("git-clone", async () => {
expect(output.exitCode).toBe(0);
});

it("skips non-default SSH port scan when host is already in known_hosts", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "ssh://git@forgejo.example.com:2222/example/repo.git",
base_dir: "/tmp",
});
const setupWithExistingKey = [
installFakeGit,
"mkdir -p /root/.ssh && chmod 700 /root/.ssh",
"echo '[forgejo.example.com]:2222 ssh-rsa AAAAB3NzaC1yc2EEXISTING' >> /root/.ssh/known_hosts",
"chmod 600 /root/.ssh/known_hosts",
].join("\n");
const output = await executeScriptInContainer(
state,
"alpine/git",
setupWithExistingKey,
);
expect(output.stdout).not.toContain(
"Adding host key for [forgejo.example.com]:2222 to known_hosts...",
);
expect(output.exitCode).toBe(0);
});

it("does not reuse a default-port key for a non-default SSH port", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "ssh://git@forgejo.example.com:2222/example/repo.git",
base_dir: "/tmp",
});
const setupWithDefaultPortKey = [
installFakeGit,
"mkdir -p /root/.ssh && chmod 700 /root/.ssh",
"echo 'forgejo.example.com ssh-rsa AAAAB3NzaC1yc2EEXISTING' >> /root/.ssh/known_hosts",
"chmod 600 /root/.ssh/known_hosts",
"cat > /usr/local/bin/ssh-keyscan <<'SHIM'",
"#!/bin/sh",
"echo '[forgejo.example.com]:2222 ssh-rsa AAAAB3NzaC1yc2EFAKE'",
"SHIM",
"chmod +x /usr/local/bin/ssh-keyscan",
].join("\n");
const output = await executeScriptInContainer(
state,
"alpine/git",
setupWithDefaultPortKey,
);
expect(output.stdout).toContain(
"Adding host key for [forgejo.example.com]:2222 to known_hosts...",
);
expect(output.exitCode).toBe(0);
});

it("fails when post-clone script fails", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
Expand Down
48 changes: 42 additions & 6 deletions registry/coder/modules/git-clone/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,55 @@ fi
# For SSH URLs, populate known_hosts before cloning to prevent "Host key verification failed"
# on new workspaces where known_hosts is empty.
if echo "$REPO_URL" | grep -qE '^git@|^ssh://'; then
SSH_HOST=$(echo "$REPO_URL" | sed -E 's|^(ssh://)?([^@/]+@)?([^:/]+).*|\3|')
SSH_PORT=""
if [[ "$REPO_URL" == ssh://* ]]; then
SSH_AUTHORITY="$${REPO_URL#ssh://}"
SSH_AUTHORITY="$${SSH_AUTHORITY%%/*}"
SSH_AUTHORITY="$${SSH_AUTHORITY##*@}"
if [[ "$SSH_AUTHORITY" == \[*\]* ]]; then
SSH_HOST="$${SSH_AUTHORITY%%]*}"
SSH_HOST="$${SSH_HOST#[}"
SSH_PORT="$${SSH_AUTHORITY#*]}"
SSH_PORT="$${SSH_PORT#:}"
else
SSH_HOST="$${SSH_AUTHORITY%%:*}"
if [[ "$SSH_AUTHORITY" == *:* ]]; then
SSH_PORT="$${SSH_AUTHORITY##*:}"
fi
fi
if [[ "$SSH_PORT" == *[!0-9]* ]]; then
SSH_PORT=""
fi
while [[ "$SSH_PORT" == 0* && "$SSH_PORT" != "0" ]]; do
SSH_PORT="$${SSH_PORT#0}"
done
else
SSH_HOST="$${REPO_URL#*@}"
SSH_HOST="$${SSH_HOST%%:*}"
SSH_HOST="$${SSH_HOST%%/*}"
fi

SSH_KNOWN_HOST="$SSH_HOST"
if [ -n "$SSH_PORT" ] && [ "$SSH_PORT" != "22" ]; then
SSH_KNOWN_HOST="[$SSH_HOST]:$SSH_PORT"
fi

mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
touch "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/known_hosts"
if ! ssh-keygen -F "$SSH_HOST" > /dev/null 2>&1; then
echo "Adding host key for $SSH_HOST to known_hosts..."
if ! ssh-keygen -F "$SSH_KNOWN_HOST" > /dev/null 2>&1; then
echo "Adding host key for $SSH_KNOWN_HOST to known_hosts..."
if command -v ssh-keyscan > /dev/null 2>&1; then
if KNOWN_HOST_ENTRY=$(ssh-keyscan -H -t rsa,ecdsa,ed25519 "$SSH_HOST" 2> /dev/null) && [ -n "$KNOWN_HOST_ENTRY" ]; then
ssh_keyscan_args=(-H -t "rsa,ecdsa,ed25519")
if [ -n "$SSH_PORT" ]; then
ssh_keyscan_args+=(-p "$SSH_PORT")
fi
if KNOWN_HOST_ENTRY=$(ssh-keyscan "$${ssh_keyscan_args[@]}" "$SSH_HOST" 2> /dev/null) && [ -n "$KNOWN_HOST_ENTRY" ]; then
printf '%s\n' "$KNOWN_HOST_ENTRY" >> "$HOME/.ssh/known_hosts"
echo "Host key for $SSH_HOST added to known_hosts."
echo "Host key for $SSH_KNOWN_HOST added to known_hosts."
else
echo "WARNING: ssh-keyscan failed for $SSH_HOST. Clone may fail if host key is not trusted."
echo "WARNING: ssh-keyscan failed for $SSH_KNOWN_HOST. Clone may fail if host key is not trusted."
fi
else
echo "ssh-keyscan not available. Using StrictHostKeyChecking=accept-new."
Expand Down