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
44 changes: 27 additions & 17 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 = "1.3.1"
version = "1.4.0"
Comment thread
35C4n0r marked this conversation as resolved.
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 = "1.3.1"
version = "1.4.0"
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 = "1.3.1"
version = "1.4.0"
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 = "1.3.1"
version = "1.4.0"
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 = "1.3.1"
version = "1.4.0"
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 = "1.3.1"
version = "1.4.0"
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 = "1.3.1"
version = "1.4.0"
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 = "1.3.1"
version = "1.4.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
Expand All @@ -177,29 +177,39 @@ 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 = "1.3.1"
version = "1.4.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
base_dir = "~/projects/coder"
}
```

## Git shallow clone
## Extra `git clone` arguments
Comment thread
35C4n0r marked this conversation as resolved.

Limit the clone history to speed-up workspace startup by setting `depth`.
Pass any additional flags through `extra_args` (one element per argument).
This lets you enable anything `git clone` supports without the module having
to expose it explicitly, for example a shallow clone, submodules, parallel
fetches, or partial clones.

When `depth` is greater than `0` the module runs `git clone --depth <depth>`.
If not defined, the default, `0`, performs a full clone.
> Do not put secrets in `extra_args`. The resolved `git clone` command
> (including every element of `extra_args`) is echoed to the workspace
> startup log, so values like `--config=http.extraHeader=Authorization: Bearer <token>`
> would appear there in plaintext.

```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.3.1"
version = "1.4.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
extra_args = [
"--depth=1",
"--recurse-submodules",
"--jobs=8",
"--filter=blob:none",
]
}
```

Expand All @@ -212,7 +222,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 = "1.3.1"
version = "1.4.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
pre_clone_script = <<-EOT
Expand All @@ -235,7 +245,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 = "1.3.1"
version = "1.4.0"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT
Expand Down
137 changes: 124 additions & 13 deletions registry/coder/modules/git-clone/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,48 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
execContainer,
findResourceInstance,
runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
type scriptOutput,
type TerraformState,
} from "~test";

const executeScriptInContainer = async (
state: TerraformState,
image: string,
before?: string,
): Promise<scriptOutput> => {
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image);
await execContainer(id, ["sh", "-c", "apk add --no-cache bash >/dev/null"]);
if (before) {
await execContainer(id, ["sh", "-c", before]);
}
const resp = await execContainer(id, ["bash", "-c", instance.script]);
return {
exitCode: resp.exitCode,
stdout: resp.stdout.trim().split("\n"),
stderr: resp.stderr.trim().split("\n"),
};
};

// Drops a fake `git` onto PATH that prints each argv entry on its own line.
// Lets tests prove that arguments (including ones with embedded spaces) reach
// `git clone` as single argv tokens, which the echo line cannot show because
// it joins with spaces.
const installFakeGit = [
"cat > /usr/local/bin/git <<'SHIM'",
"#!/bin/sh",
'for arg in "$@"; do',
' printf "argv:%s\\n" "$arg"',
"done",
"SHIM",
"chmod +x /usr/local/bin/git",
].join("\n");

describe("git-clone", async () => {
await runTerraformInit(import.meta.dir);

Expand All @@ -31,8 +68,9 @@ describe("git-clone", async () => {
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.stdout).toEqual([
"Creating directory ~/fake-url...",
"Cloning fake-url to ~/fake-url...",
"Creating directory /root/fake-url...",
"Cloning fake-url to /root/fake-url...",
"Running: git clone fake-url /root/fake-url",
]);
expect(output.stderr.join(" ")).toContain("fatal");
expect(output.stderr.join(" ")).toContain("fake-url");
Expand Down Expand Up @@ -207,8 +245,9 @@ describe("git-clone", async () => {
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"Creating directory ~/repo-tests.log...",
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
"Creating directory /root/repo-tests.log...",
"Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...",
"Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log",
]);
});

Expand All @@ -220,8 +259,9 @@ describe("git-clone", async () => {
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"Creating directory ~/repo-tests.log...",
"Cloning https://gitlab.com/mike.brew/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
"Creating directory /root/repo-tests.log...",
"Cloning https://gitlab.com/mike.brew/repo-tests.log to /root/repo-tests.log on branch feat/branch...",
"Running: git clone -b feat/branch https://gitlab.com/mike.brew/repo-tests.log /root/repo-tests.log",
]);
});

Expand All @@ -241,8 +281,9 @@ describe("git-clone", async () => {
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"Creating directory ~/repo-tests.log...",
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
"Creating directory /root/repo-tests.log...",
"Cloning https://github.com/michaelbrewer/repo-tests.log to /root/repo-tests.log on branch feat/branch...",
"Running: git clone -b feat/branch https://github.com/michaelbrewer/repo-tests.log /root/repo-tests.log",
]);
});

Expand All @@ -256,7 +297,6 @@ describe("git-clone", async () => {
const output = await executeScriptInContainer(
state,
"alpine/git",
"sh",
"mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt",
);
expect(output.stdout).toContain("Running post-clone script...");
Expand All @@ -272,7 +312,7 @@ describe("git-clone", async () => {
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.stdout).toContain("Running pre-clone script...");
expect(output.stdout).toContain("Pre-clone script executed");
expect(output.stdout).toContain("Cloning fake-url to ~/fake-url...");
expect(output.stdout).toContain("Cloning fake-url to /root/fake-url...");
});

it("fails when pre-clone script fails", async () => {
Expand All @@ -285,7 +325,79 @@ describe("git-clone", async () => {
expect(output.exitCode).toBe(42);
expect(output.stdout).toContain("Running pre-clone script...");
expect(output.stdout).toContain("Pre-clone script failed");
expect(output.stdout).not.toContain("Cloning fake-url to ~/fake-url...");
expect(output.stdout).not.toContain(
"Cloning fake-url to /root/fake-url...",
);
});

it("defaults extra_args to empty", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
});
const script = findResourceInstance(state, "coder_script").script;
expect(script).toContain('EXTRA_ARGS=""');
});

it("passes extra_args to git clone", async () => {
Comment thread
35C4n0r marked this conversation as resolved.
Comment thread
35C4n0r marked this conversation as resolved.
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
extra_args: JSON.stringify([
"--recurse-submodules",
"--jobs=8",
"--config=user.name=Coder User",
"-c",
"core.sshCommand=ssh -i /tmp/key",
]),
});
const output = await executeScriptInContainer(
state,
"alpine/git",
installFakeGit,
);
expect(output.exitCode).toBe(0);
expect(output.stdout.join("\n")).toContain(
[
"argv:clone",
"argv:--recurse-submodules",
"argv:--jobs=8",
"argv:--config=user.name=Coder User",
"argv:-c",
"argv:core.sshCommand=ssh -i /tmp/key",
"argv:fake-url",
"argv:/root/fake-url",
].join("\n"),
);
});

it("passes extra_args alongside branch_name in the correct order", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "fake-url",
branch_name: "feat/branch",
extra_args: JSON.stringify([
"--recurse-submodules",
"--config=user.name=Coder User",
]),
});
const output = await executeScriptInContainer(
state,
"alpine/git",
installFakeGit,
);
expect(output.exitCode).toBe(0);
expect(output.stdout.join("\n")).toContain(
[
"argv:clone",
"argv:--recurse-submodules",
"argv:--config=user.name=Coder User",
"argv:-b",
"argv:feat/branch",
"argv:fake-url",
"argv:/root/fake-url",
].join("\n"),
);
});

it("fails when post-clone script fails", async () => {
Expand All @@ -298,7 +410,6 @@ describe("git-clone", async () => {
const output = await executeScriptInContainer(
state,
"alpine/git",
"sh",
"mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt",
);
expect(output.exitCode).toBe(43);
Expand Down
11 changes: 6 additions & 5 deletions registry/coder/modules/git-clone/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ variable "folder_name" {
default = ""
}

variable "depth" {
description = "If > 0, perform a shallow clone using this depth."
type = number
default = 0
variable "extra_args" {
Comment thread
35C4n0r marked this conversation as resolved.
description = "Extra arguments to pass to `git clone`, one element per argument (e.g. `[\"--recurse-submodules\", \"--jobs=8\", \"--filter=blob:none\"]`)."
type = list(string)
default = []
}

variable "post_clone_script" {
Expand Down Expand Up @@ -97,6 +97,7 @@ locals {
encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
# Encode the pre_clone_script for passing to the shell script
encoded_pre_clone_script = var.pre_clone_script != null ? base64encode(var.pre_clone_script) : ""
encoded_extra_args = base64encode(join("\n", var.extra_args))
}

output "repo_dir" {
Expand Down Expand Up @@ -135,7 +136,7 @@ resource "coder_script" "git_clone" {
CLONE_PATH = local.clone_path,
REPO_URL : local.clone_url,
BRANCH_NAME : local.branch_name,
DEPTH = var.depth,
EXTRA_ARGS = local.encoded_extra_args,
POST_CLONE_SCRIPT : local.encoded_post_clone_script,
PRE_CLONE_SCRIPT : local.encoded_pre_clone_script,
})
Expand Down
Loading
Loading