Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ jobs:
workspace_image: "ghcr.io/ppat/coder-workspace:${{ needs.publish-image.outputs.image_tag }}"
home_pvc_storage_class: sc-longhorn-replicated
tmp_pvc_storage_class: sc-longhorn-local-non-replicated-ephemeral
test_mode: "false"
kubernetes_config_path: ""
skip_dotfiles_scripts: "false"
message: "[Release Notes](https://github.com/${{ github.repository }}/releases/tag/${{ env.source_sha }})"

- name: Tailscale Disconnect
Expand Down
18 changes: 0 additions & 18 deletions .github/workflows/test-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,3 @@ jobs:
tailscale_oauth_secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
build_secrets: |
FETCH_GH_TOKEN=${{ secrets.GITHUB_TOKEN }}

integration:
needs: build
uses: ./.github/workflows/test-template.yaml
with:
image_repository: ${{ vars.CONTAINER_REGISTRY_PATH }}/coder-workspace
image_tag: ${{ needs.build.outputs.image_tag }}
service_commands: >-
["test -f /home/coder/.local/state/dotfiles/applied || { touch /tmp/service-started-too-early; exit 1; };
printf x >> /tmp/service-restarts; sleep 2; exit 1",
"test -f /home/coder/.local/state/dotfiles/applied || { touch /tmp/service-started-too-early; exit 1; };
touch /tmp/service-steady; exec sleep 300"]
secrets:
private_registry: ${{ secrets.CONTAINER_REGISTRY }}
private_registry_username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
private_registry_token: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
tailscale_oauth_client_id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
tailscale_oauth_secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
186 changes: 120 additions & 66 deletions .github/workflows/test-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,13 @@ name: test template
on:
pull_request:
paths:
- actions/coder-template-push/**
- .github/compose/**
- .github/workflows/test-template.yaml
- templates/kubernetes/homelab-workspace/**
workflow_call:
inputs:
image_repository:
type: string
default: ""
image_tag:
type: string
default: ""
service_commands:
type: string
default: "[]"
secrets:
private_registry:
required: false
private_registry_username:
required: false
private_registry_token:
required: false
tailscale_oauth_client_id:
required: false
tailscale_oauth_secret:
required: false

concurrency:
# A called workflow inherits the caller's github.workflow and run ID. Include
# its image input so it cannot cancel the test-image run that invoked it.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-${{ inputs.image_tag || 'released' }}
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
Expand Down Expand Up @@ -74,36 +51,6 @@ jobs:
# created explicitly before the template can be applied.
run: kubectl create namespace coder

- name: Connect to private registry network
if: inputs.image_tag != ''
uses: tailscale/github-action@780049a30b6ff5c378a9e7b389d15ece7a204888 # v4.1.3
with:
oauth-client-id: ${{ secrets.tailscale_oauth_client_id }}
oauth-secret: ${{ secrets.tailscale_oauth_secret }}
tags: tag:github-action-ci-runner

- name: Log in to private registry
if: inputs.image_tag != ''
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ secrets.private_registry }}
username: ${{ secrets.private_registry_username }}
password: ${{ secrets.private_registry_token }}

- name: Load pull request image into test cluster
if: inputs.image_tag != ''
id: pull_request_image
env:
IMAGE_REGISTRY: ${{ secrets.private_registry }}
IMAGE_REPOSITORY: ${{ inputs.image_repository }}
IMAGE_TAG: ${{ inputs.image_tag }}
shell: bash
run: |
workspace_image="${IMAGE_REGISTRY}/${IMAGE_REPOSITORY}:${IMAGE_TAG}"
docker pull "${workspace_image}"
kind load docker-image --name "${KIND_CLUSTER}" "${workspace_image}"
echo "workspace_image=${workspace_image}" >> "${GITHUB_OUTPUT}"

- name: Start local Coder
env:
CODER_KUBECONFIG: ${{ runner.temp }}/kubeconfig-coder
Expand Down Expand Up @@ -153,7 +100,6 @@ jobs:
coder_password: ci-password

- name: Determine latest released workspace image
if: inputs.image_tag == ''
id: released_image
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -168,21 +114,49 @@ jobs:
template_dir: current/templates/kubernetes/homelab-workspace
template_name: ${{ env.TEMPLATE_NAME }}
template_version: ${{ github.sha }}
workspace_image: ${{ steps.pull_request_image.outputs.workspace_image || steps.released_image.outputs.workspace_image }}
workspace_image: ${{ steps.released_image.outputs.workspace_image }}
home_pvc_storage_class: standard
tmp_pvc_storage_class: standard
test_mode: "true"
kubernetes_config_path: /home/coder/.kube/config
skip_dotfiles_scripts: "true"

- name: Confirm storage parameter contracts
shell: bash
run: |
version_id="$(coder templates versions list "${TEMPLATE_NAME}" --output json \
| jq --raw-output '.[0].TemplateVersion.id')"
[[ "${version_id}" =~ ^[0-9a-f-]{36}$ ]]
session_token="$(<"${HOME}/.config/coderv2/session")"
owner_id="$(curl --fail --silent --show-error \
--header "Coder-Session-Token: ${session_token}" "${CODER_URL}/api/v2/users/me" | jq --raw-output .id)"
parameters="$(curl --fail --silent --show-error --request POST \
--header "Coder-Session-Token: ${session_token}" --header 'Content-Type: application/json' \
--data "$(jq --null-input --arg owner_id "${owner_id}" \
'{id: 0, owner_id: $owner_id, inputs: {use_existing_home_pvc: "false"}}')" \
"${CODER_URL}/api/v2/templateversions/${version_id}/dynamic-parameters/evaluate")"
jq --exit-status \
'any(.parameters[]; .name == "home_pvc_size" and .mutable == true
and any(.validations[]; .validation_max == 256))' \
<<<"${parameters}"
jq --exit-status \
'any(.parameters[]; .name == "tmp_pvc_size" and .mutable == true and .default_value.value == "1"
and any(.validations[]; .validation_min == 1 and .validation_max == 50))' \
<<<"${parameters}"

- name: Start workspace
env:
SERVICE_COMMANDS: ${{ inputs.service_commands || '[]' }}
SERVICE_COMMANDS: >-
["test -f /home/coder/.local/state/dotfiles/applied || { touch /tmp/service-started-too-early; exit 1; };
printf x >> /tmp/service-restarts; sleep 2; exit 1",
"test -f /home/coder/.local/state/dotfiles/applied || { touch /tmp/service-started-too-early; exit 1; };
touch /tmp/service-steady; exec sleep 300"]
shell: bash
run: |
service_parameter="$(jq --null-input --raw-output \
--arg value "service_commands=${SERVICE_COMMANDS}" '[$value] | @csv')"
coder create "${WORKSPACE_NAME}" --template "${TEMPLATE_NAME}" --no-wait --yes \
--parameter dotfiles_url=https://github.com/ppat/dotfiles.git \
--parameter use_existing_home_pvc=false --parameter home_pvc_size=2 \
--parameter use_existing_home_pvc=false --parameter home_pvc_size=2 --parameter tmp_pvc_size=1 \
--parameter memory=4 --parameter preferred_nodes='[]' --parameter memory_watchdog_mode=enforce \
--parameter "${service_parameter}"

Expand All @@ -195,6 +169,27 @@ jobs:
run: |
coder ssh "${WORKSPACE_NAME}" -- env

- name: Confirm script bundle and temporary storage
shell: bash
run: |
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-agent-startup.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-container-entrypoint.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-dotfiles.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-memory-watchdog.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-memory-watchdog-start.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-prepare-workspace.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-service-command.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-start-services.sh
coder ssh "${WORKSPACE_NAME}" -- test -f /scripts/supervisord.conf
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/script-vscode-server-gc.sh
coder ssh "${WORKSPACE_NAME}" -- test -x /scripts/workspace-init.sh
deployment_name="$(kubectl --namespace coder get deployment \
--selector "com.coder.workspace.name=${WORKSPACE_NAME}" \
--output jsonpath='{.items[0].metadata.name}')"
[[ "$(kubectl --namespace coder get "deployment/${deployment_name}" \
--output jsonpath='{.spec.template.spec.volumes[?(@.name=="tmp")].ephemeral.volumeClaimTemplate.spec.resources.requests.storage}')" \
== "1Gi" ]]

- name: Confirm dotfiles were applied
shell: bash
run: |
Expand All @@ -214,7 +209,6 @@ jobs:
[[ "${origin}" == "https://github.com/ppat/dotfiles.git" ]]

- name: Confirm supervised service starts after dotfiles and restarts
if: inputs.service_commands != '' && inputs.service_commands != '[]'
shell: bash
run: |
# Expand the positional parameter inside the runner-owned polling shell.
Expand All @@ -224,7 +218,7 @@ jobs:
[ "$count" -ge 2 ]; do sleep 2; done' \
_ "${WORKSPACE_NAME}"; then
coder ssh "${WORKSPACE_NAME}" -- \
supervisorctl --configuration /supervisord.conf status || true
supervisorctl --configuration /scripts/supervisord.conf status || true
coder ssh "${WORKSPACE_NAME}" -- ls -la /home/coder/.local/state/supervisor /tmp/service-* || true
coder ssh "${WORKSPACE_NAME}" -- tail -n 100 /home/coder/.local/state/supervisor/service-0.log || true
coder ssh "${WORKSPACE_NAME}" -- tail -n 100 /home/coder/.local/state/supervisor/service-1.log || true
Expand All @@ -233,13 +227,13 @@ jobs:
coder ssh "${WORKSPACE_NAME}" -- test ! -e /tmp/service-started-too-early
coder ssh "${WORKSPACE_NAME}" -- test -e /tmp/service-steady
coder ssh "${WORKSPACE_NAME}" -- \
supervisorctl --configuration /supervisord.conf status service:service-0
supervisorctl --configuration /scripts/supervisord.conf status service:service-0
coder ssh "${WORKSPACE_NAME}" -- \
supervisorctl --configuration /supervisord.conf status service:service-1
supervisorctl --configuration /scripts/supervisord.conf status service:service-1

- name: Show dotfiles log
if: always()
run: coder ssh "${WORKSPACE_NAME}" -- cat /home/coder/.local/state/dotfiles/run.log
run: coder ssh "${WORKSPACE_NAME}" -- cat /home/coder/.local/state/dotfiles/run.log || true

- name: Confirm workspace-owned home PVC lifecycle
shell: bash
Expand All @@ -248,15 +242,75 @@ jobs:
--selector "com.coder.workspace.name=${WORKSPACE_NAME}" \
--output jsonpath='{.items[0].metadata.name}')"
[[ -n "${pvc_name}" ]]
pod_uid="$(kubectl --namespace coder get pod \
--selector "com.coder.workspace.name=${WORKSPACE_NAME}" \
--output jsonpath='{.items[0].metadata.uid}')"
tmp_pvc_name="$(kubectl --namespace coder get pvc --output json | jq --raw-output --arg uid "${pod_uid}" \
'.items[] | select(any(.metadata.ownerReferences[]?; .uid == $uid)) | .metadata.name')"
[[ -n "${tmp_pvc_name}" ]]
coder ssh "${WORKSPACE_NAME}" -- touch /home/coder/.home-pvc-lifecycle
coder stop "${WORKSPACE_NAME}" --yes
kubectl --namespace coder get "persistentvolumeclaim/${pvc_name}"
kubectl --namespace coder wait --for=delete \
"persistentvolumeclaim/${tmp_pvc_name}" --timeout=5m
coder start "${WORKSPACE_NAME}" --no-wait --yes
coder ping --num 3 --timeout 30s "${WORKSPACE_NAME}"
coder ssh "${WORKSPACE_NAME}" -- test -f /home/coder/.home-pvc-lifecycle
coder ssh "${WORKSPACE_NAME}" -- test -f /home/coder/.home-pvc-lifecycle || {
echo "::error::Home PVC did not preserve the lifecycle marker across stop/start."
exit 1
}
home_size="$(kubectl --namespace coder get "persistentvolumeclaim/${pvc_name}" \
--output jsonpath='{.spec.resources.requests.storage}')"
[[ "${home_size}" == "2Gi" ]] || {
echo "::error::Expected home PVC ${pvc_name} to remain 2Gi, observed ${home_size}."
exit 1
}
new_pod_uid="$(kubectl --namespace coder get pod \
--selector "com.coder.workspace.name=${WORKSPACE_NAME}" \
--output jsonpath='{.items[0].metadata.uid}')"
new_tmp_pvc_name="$(kubectl --namespace coder get pvc --output json | jq --raw-output --arg uid "${new_pod_uid}" \
'.items[] | select(any(.metadata.ownerReferences[]?; .uid == $uid)) | .metadata.name')"
[[ -n "${new_tmp_pvc_name}" && "${new_tmp_pvc_name}" != "${tmp_pvc_name}" ]] || {
echo "::error::Expected a replacement tmp PVC; old=${tmp_pvc_name}, new=${new_tmp_pvc_name:-missing}."
exit 1
}
tmp_request="$(kubectl --namespace coder get "persistentvolumeclaim/${new_tmp_pvc_name}" \
--output jsonpath='{.spec.resources.requests.storage}')"
tmp_capacity="$(kubectl --namespace coder get "persistentvolumeclaim/${new_tmp_pvc_name}" \
--output jsonpath='{.status.capacity.storage}')"
[[ "${tmp_request}" == "1Gi" && "${tmp_capacity}" == "1Gi" ]] || {
echo "::error::Expected tmp PVC ${new_tmp_pvc_name} to request and bind 1Gi; request=${tmp_request}, capacity=${tmp_capacity}."
exit 1
}
coder delete "${WORKSPACE_NAME}" --yes
kubectl --namespace coder wait --for=delete "persistentvolumeclaim/${pvc_name}" --timeout=5m

- name: Confirm custom temporary storage size
env:
SIZED_WORKSPACE_NAME: template-test-tmp-size
shell: bash
run: |
coder create "${SIZED_WORKSPACE_NAME}" --template "${TEMPLATE_NAME}" --no-wait --yes \
--parameter dotfiles_url='' --parameter use_existing_home_pvc=false \
--parameter home_pvc_size=1 --parameter tmp_pvc_size=2 --parameter memory=4 \
--parameter preferred_nodes='[]' --parameter service_commands='[]' \
--parameter memory_watchdog_mode=enforce
coder ping --num 3 --timeout 30s "${SIZED_WORKSPACE_NAME}"
pod_uid="$(kubectl --namespace coder get pod \
--selector "com.coder.workspace.name=${SIZED_WORKSPACE_NAME}" \
--output jsonpath='{.items[0].metadata.uid}')"
tmp_pvc_name="$(kubectl --namespace coder get pvc --output json | jq --raw-output --arg uid "${pod_uid}" \
'.items[] | select(any(.metadata.ownerReferences[]?; .uid == $uid)) | .metadata.name')"
tmp_request="$(kubectl --namespace coder get "persistentvolumeclaim/${tmp_pvc_name}" \
--output jsonpath='{.spec.resources.requests.storage}')"
tmp_capacity="$(kubectl --namespace coder get "persistentvolumeclaim/${tmp_pvc_name}" \
--output jsonpath='{.status.capacity.storage}')"
[[ "${tmp_request}" == "2Gi" && "${tmp_capacity}" == "2Gi" ]] || {
echo "::error::Expected tmp PVC ${tmp_pvc_name} to request and bind 2Gi; request=${tmp_request}, capacity=${tmp_capacity}."
exit 1
}
coder delete "${SIZED_WORKSPACE_NAME}" --yes

- name: Show Coder logs on failure
if: failure()
run: docker compose -f "${COMPOSE_FILE}" logs
Loading
Loading