Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb70300b37 | |||
| 5c6d7f4434 | |||
| d1cd784866 | |||
| db94b30b1c | |||
| a0411a39f9 |
@@ -1,18 +1,13 @@
|
||||
{
|
||||
"name": "Development environments on your infrastructure",
|
||||
"image": "codercom/oss-dogfood:latest",
|
||||
"name": "Development environments on your infrastructure",
|
||||
"image": "codercom/oss-dogfood:latest",
|
||||
|
||||
"features": {
|
||||
// See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": "false"
|
||||
}
|
||||
},
|
||||
// SYS_PTRACE to enable go debugging
|
||||
"runArgs": ["--cap-add=SYS_PTRACE"],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["biomejs.biome"]
|
||||
}
|
||||
}
|
||||
"features": {
|
||||
// See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": "false"
|
||||
}
|
||||
},
|
||||
// SYS_PTRACE to enable go debugging
|
||||
"runArgs": ["--cap-add=SYS_PTRACE"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Ignore all files and folders
|
||||
**
|
||||
|
||||
# Include flake.nix and flake.lock
|
||||
!flake.nix
|
||||
!flake.lock
|
||||
+1
-1
@@ -7,7 +7,7 @@ trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
|
||||
[*.{yaml,yml,tf,tfvars,nix}]
|
||||
[*.{md,json,yaml,yml,tf,tfvars,nix}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
||||
@@ -3,5 +3,3 @@
|
||||
|
||||
# chore: format code with semicolons when using prettier (#9555)
|
||||
988c9af0153561397686c119da9d1336d2433fdd
|
||||
# chore: use tabs for prettier and biome (#14283)
|
||||
95a7c0c4f087744a22c2e88dd3c5d30024d5fb02
|
||||
|
||||
+2
-4
@@ -1,9 +1,7 @@
|
||||
# Generated files
|
||||
agent/agentcontainers/acmock/acmock.go linguist-generated=true
|
||||
agent/agentcontainers/dcspec/dcspec_gen.go linguist-generated=true
|
||||
coderd/apidoc/docs.go linguist-generated=true
|
||||
docs/reference/api/*.md linguist-generated=true
|
||||
docs/reference/cli/*.md linguist-generated=true
|
||||
docs/api/*.md linguist-generated=true
|
||||
docs/cli/*.md linguist-generated=true
|
||||
coderd/apidoc/swagger.json linguist-generated=true
|
||||
coderd/database/dump.sql linguist-generated=true
|
||||
peerbroker/proto/*.go linguist-generated=true
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
dirs:
|
||||
- docs
|
||||
excludedDirs:
|
||||
# Downstream bug in linkspector means large markdown files fail to parse
|
||||
# but these are autogenerated and shouldn't need checking
|
||||
- docs/reference
|
||||
# Older changelogs may contain broken links
|
||||
- docs/changelogs
|
||||
ignorePatterns:
|
||||
- pattern: "localhost"
|
||||
- pattern: "example.com"
|
||||
- pattern: "mailto:"
|
||||
- pattern: "127.0.0.1"
|
||||
- pattern: "0.0.0.0"
|
||||
- pattern: "JFROG_URL"
|
||||
- pattern: "coder.company.org"
|
||||
# These real sites were blocking the linkspector action / GitHub runner IPs(?)
|
||||
- pattern: "i.imgur.com"
|
||||
- pattern: "code.visualstudio.com"
|
||||
- pattern: "www.emacswiki.org"
|
||||
- pattern: "linux.die.net/man"
|
||||
- pattern: "www.gnu.org"
|
||||
- pattern: "wiki.ubuntu.com"
|
||||
- pattern: "mutagen.io"
|
||||
aliveStatusCodes:
|
||||
- 200
|
||||
@@ -1,78 +0,0 @@
|
||||
name: "🐞 Bug"
|
||||
description: "File a bug report."
|
||||
title: "bug: "
|
||||
labels: ["needs-triage"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: existing_issues
|
||||
attributes:
|
||||
label: "Is there an existing issue for this?"
|
||||
description: "Please search to see if an issue already exists for the bug you encountered."
|
||||
options:
|
||||
- label: "I have searched the existing issues"
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: issue
|
||||
attributes:
|
||||
label: "Current Behavior"
|
||||
description: "A concise description of what you're experiencing."
|
||||
placeholder: "Tell us what you see!"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: "Relevant Log Output"
|
||||
description: "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks."
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: "Expected Behavior"
|
||||
description: "A concise description of what you expected to happen."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: "Steps to Reproduce"
|
||||
description: "Provide step-by-step instructions to reproduce the issue."
|
||||
placeholder: |
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Another step
|
||||
4. Issue occurs
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: "Environment"
|
||||
description: |
|
||||
Provide details about your environment:
|
||||
- **Host OS**: (e.g., Ubuntu 24.04, Debian 12)
|
||||
- **Coder Version**: (e.g., v2.18.4)
|
||||
placeholder: |
|
||||
Run `coder version` to get Coder version
|
||||
value: |
|
||||
- Host OS:
|
||||
- Coder version:
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: additional_info
|
||||
attributes:
|
||||
label: "Additional Context"
|
||||
description: "Select any applicable options:"
|
||||
multiple: true
|
||||
options:
|
||||
- "The issue occurs consistently"
|
||||
- "The issue is new (previously worked fine)"
|
||||
- "The issue happens on multiple deployments"
|
||||
- "I have tested this on the latest version"
|
||||
@@ -1,10 +0,0 @@
|
||||
contact_links:
|
||||
- name: Questions, suggestion or feature requests?
|
||||
url: https://github.com/coder/coder/discussions/new/choose
|
||||
about: Our preferred starting point if you have any questions or suggestions about configuration, features or unexpected behavior.
|
||||
- name: Coder Docs
|
||||
url: https://coder.com/docs
|
||||
about: Check our docs.
|
||||
- name: Coder Discord Community
|
||||
url: https://discord.gg/coder
|
||||
about: Get in touch with the Coder developers and community for support.
|
||||
@@ -1,10 +0,0 @@
|
||||
name: "Install cosign"
|
||||
description: |
|
||||
Cosign Github Action.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
||||
with:
|
||||
cosign-release: "v2.4.3"
|
||||
@@ -1,10 +0,0 @@
|
||||
name: "Install syft"
|
||||
description: |
|
||||
Downloads Syft to the Action tool cache and provides a reference.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install syft
|
||||
uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
|
||||
with:
|
||||
syft-version: "v1.20.0"
|
||||
@@ -4,12 +4,12 @@ description: |
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.24.1"
|
||||
default: "1.22.5"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ inputs.version }}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
name: "Setup ImDisk"
|
||||
if: runner.os == 'Windows'
|
||||
description: |
|
||||
Sets up the ImDisk toolkit for Windows and creates a RAM disk on drive R:.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download ImDisk
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir imdisk
|
||||
cd imdisk
|
||||
curl -L -o files.cab https://github.com/coder/imdisk-artifacts/raw/92a17839ebc0ee3e69be019f66b3e9b5d2de4482/files.cab
|
||||
curl -L -o install.bat https://github.com/coder/imdisk-artifacts/raw/92a17839ebc0ee3e69be019f66b3e9b5d2de4482/install.bat
|
||||
cd ..
|
||||
|
||||
- name: Install ImDisk
|
||||
shell: cmd
|
||||
run: |
|
||||
cd imdisk
|
||||
install.bat /silent
|
||||
|
||||
- name: Create RAM Disk
|
||||
shell: cmd
|
||||
run: |
|
||||
imdisk -a -s 4096M -m R: -p "/fs:ntfs /q /y"
|
||||
@@ -11,16 +11,16 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.6
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
uses: actions/setup-node@v4.0.3
|
||||
with:
|
||||
node-version: 20.16.0
|
||||
# See https://github.com/actions/setup-node#caching-global-packages-data
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: ${{ inputs.directory }}/pnpm-lock.yaml
|
||||
|
||||
- name: Install root node_modules
|
||||
shell: bash
|
||||
run: ./scripts/pnpm_install.sh
|
||||
|
||||
@@ -5,6 +5,6 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup sqlc
|
||||
uses: sqlc-dev/setup-sqlc@c0209b9199cd1cce6a14fc27cabcec491b651761 # v4.0.0
|
||||
uses: sqlc-dev/setup-sqlc@v4
|
||||
with:
|
||||
sqlc-version: "1.27.0"
|
||||
sqlc-version: "1.25.0"
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: 1.11.2
|
||||
terraform_version: 1.9.2
|
||||
terraform_wrapper: false
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
name: Upload tests to datadog
|
||||
description: |
|
||||
Uploads the test results to datadog.
|
||||
if: always()
|
||||
inputs:
|
||||
api-key:
|
||||
description: "Datadog API key"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
enabled: true
|
||||
preservePullRequestTitle: true
|
||||
+35
-19
@@ -9,6 +9,21 @@ updates:
|
||||
labels: []
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
ignore:
|
||||
# These actions deliver the latest versions by updating the major
|
||||
# release tag, so ignore minor and patch versions
|
||||
- dependency-name: "actions/*"
|
||||
update-types:
|
||||
- version-update:semver-minor
|
||||
- version-update:semver-patch
|
||||
- dependency-name: "Apple-Actions/import-codesign-certs"
|
||||
update-types:
|
||||
- version-update:semver-minor
|
||||
- version-update:semver-patch
|
||||
- dependency-name: "marocchino/sticky-pull-request-comment"
|
||||
update-types:
|
||||
- version-update:semver-minor
|
||||
- version-update:semver-patch
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
@@ -36,14 +51,7 @@ updates:
|
||||
|
||||
# Update our Dockerfile.
|
||||
- package-ecosystem: "docker"
|
||||
directories:
|
||||
- "/dogfood/coder"
|
||||
- "/dogfood/coder-envbuilder"
|
||||
- "/scripts"
|
||||
- "/examples/templates/docker/build"
|
||||
- "/examples/parameters/build"
|
||||
- "/scaletest/templates/scaletest-runner"
|
||||
- "/scripts/ironbank"
|
||||
directory: "/scripts/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
time: "06:00"
|
||||
@@ -60,9 +68,6 @@ updates:
|
||||
directories:
|
||||
- "/site"
|
||||
- "/offlinedocs"
|
||||
- "/scripts"
|
||||
- "/scripts/apidocgen"
|
||||
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
time: "06:00"
|
||||
@@ -81,26 +86,37 @@ updates:
|
||||
- "@mui*"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
- "react*"
|
||||
- "@types/react*"
|
||||
emotion:
|
||||
patterns:
|
||||
- "@emotion*"
|
||||
exclude-patterns:
|
||||
- "jest-runner-eslint"
|
||||
eslint:
|
||||
patterns:
|
||||
- "eslint*"
|
||||
- "@typescript-eslint*"
|
||||
jest:
|
||||
patterns:
|
||||
- "jest"
|
||||
- "jest*"
|
||||
- "@types/jest"
|
||||
vite:
|
||||
patterns:
|
||||
- "vite*"
|
||||
- "@vitejs/plugin-react"
|
||||
ignore:
|
||||
# Ignore major version updates to avoid breaking changes
|
||||
# Ignore patch updates for all dependencies
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- version-update:semver-patch
|
||||
# Ignore major updates to Node.js types, because they need to
|
||||
# correspond to the Node.js engine version
|
||||
- dependency-name: "@types/node"
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
# Ignore @storybook updates, run `pnpm dlx storybook@latest upgrade` to upgrade manually
|
||||
- dependency-name: "*storybook*" # matches @storybook/* and storybook*
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- version-update:semver-minor
|
||||
- version-update:semver-patch
|
||||
open-pull-requests-limit: 15
|
||||
|
||||
+115
-663
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ name: contrib
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
@@ -10,30 +10,35 @@ on:
|
||||
- synchronize
|
||||
- labeled
|
||||
- unlabeled
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
# For jobs that don't run on draft PRs.
|
||||
- ready_for_review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Only run one instance per PR to ensure in-order execution.
|
||||
concurrency: pr-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
cla:
|
||||
# Dependabot is annoying, but this makes it a bit less so.
|
||||
auto-approve-dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request_target'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: auto-approve dependabot
|
||||
uses: hmarr/auto-approve-action@v4
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
|
||||
cla:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cla
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
|
||||
uses: contributor-assistant/github-action@v2.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CDRCI2_GITHUB_TOKEN }}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.CDRCOMMUNITY_GITHUB_TOKEN }}
|
||||
with:
|
||||
remote-organization-name: "coder"
|
||||
remote-repository-name: "cla"
|
||||
@@ -46,13 +51,11 @@ jobs:
|
||||
|
||||
release-labels:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
# Skip tagging for draft PRs.
|
||||
if: ${{ github.event_name == 'pull_request_target' && !github.event.pull_request.draft }}
|
||||
if: ${{ github.event_name == 'pull_request_target' && success() && !github.event.pull_request.draft }}
|
||||
steps:
|
||||
- name: release-labels
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
# This script ensures PR title and labels are in sync:
|
||||
#
|
||||
@@ -84,7 +87,7 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
}
|
||||
|
||||
if (action === "opened" || action === "reopened" || action === "ready_for_review") {
|
||||
if (action === "opened" || action === "reopened") {
|
||||
if (isBreakingTitle && !labels.includes(releaseLabels.breaking)) {
|
||||
console.log('Add "%s" label', releaseLabels.breaking)
|
||||
await github.rest.issues.addLabels({
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
name: dependabot
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependabot-automerge:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'opened' &&
|
||||
github.event.pull_request.user.login == 'dependabot[bot]' &&
|
||||
github.actor_id == 49699333 &&
|
||||
github.repository == 'coder/coder'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Approve the PR
|
||||
run: |
|
||||
echo "Approving $PR_URL"
|
||||
gh pr review --approve "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
- name: Enable auto-merge
|
||||
run: |
|
||||
echo "Enabling auto-merge for $PR_URL"
|
||||
gh pr merge --auto --squash "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
- name: Send Slack notification
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
PR_TITLE: ${{github.event.pull_request.title}}
|
||||
PR_NUMBER: ${{github.event.pull_request.number}}
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data '{
|
||||
"username": "dependabot",
|
||||
"icon_url": "https://avatars.githubusercontent.com/u/27347476",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":pr-merged: Auto merge enabled for Dependabot PR #${{ env.PR_NUMBER }}",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "${{ env.PR_TITLE }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "actions",
|
||||
"elements": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "View PR"
|
||||
},
|
||||
"url": "${{ env.PR_URL }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}' ${{ secrets.DEPENDABOT_PRS_SLACK_WEBHOOK }}
|
||||
@@ -22,6 +22,10 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Necessary to push docker images to ghcr.io.
|
||||
packages: write
|
||||
# Necessary for depot.dev authentication.
|
||||
id-token: write
|
||||
|
||||
# Avoid running multiple jobs for the same commit.
|
||||
concurrency:
|
||||
@@ -29,24 +33,14 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
# Necessary for depot.dev authentication.
|
||||
id-token: write
|
||||
# Necessary to push docker images to ghcr.io.
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'coder'
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker login
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -56,17 +50,16 @@ jobs:
|
||||
run: mkdir base-build-context
|
||||
|
||||
- name: Install depot.dev CLI
|
||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
# This uses OIDC authentication, so no auth variables are required.
|
||||
- name: Build base Docker image via depot.dev
|
||||
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: wl5hnrrkns
|
||||
context: base-build-context
|
||||
file: scripts/Dockerfile.base
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
provenance: true
|
||||
pull: true
|
||||
no-cache: true
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
name: Docs CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
- ".github/workflows/docs-ci.yaml"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "**.md"
|
||||
- ".github/workflows/docs-ci.yaml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- uses: tj-actions/changed-files@27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99 # v45.0.7
|
||||
id: changed-files
|
||||
with:
|
||||
files: |
|
||||
docs/**
|
||||
**.md
|
||||
separator: ","
|
||||
|
||||
- name: lint
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
pnpm exec markdownlint-cli2 ${{ steps.changed-files.outputs.all_changed_files }}
|
||||
|
||||
- name: fmt
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
# markdown-table-formatter requires a space separated list of files
|
||||
echo ${{ steps.changed-files.outputs.all_changed_files }} | tr ',' '\n' | pnpm exec markdown-table-formatter --check
|
||||
@@ -17,48 +17,17 @@ on:
|
||||
- "flake.nix"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build_image:
|
||||
if: github.actor != 'dependabot[bot]' # Skip Dependabot PRs
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Nix
|
||||
uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
|
||||
|
||||
- uses: nix-community/cache-nix-action@c448f065ba14308da81de769632ca67a3ce67cf5 # v6.1.2
|
||||
with:
|
||||
# restore and save a cache using this key
|
||||
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock') }}
|
||||
# if there's no cache hit, restore a cache by this prefix
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
||||
# collect garbage until Nix store size (in bytes) is at most this number
|
||||
# before trying to save a new cache
|
||||
# 1G = 1073741824
|
||||
gc-max-store-size-linux: 5G
|
||||
# do purge caches
|
||||
purge: true
|
||||
# purge all versions of the cache
|
||||
purge-prefixes: nix-${{ runner.os }}-
|
||||
# created more than this number of seconds ago relative to the start of the `Post Restore` phase
|
||||
purge-created: 0
|
||||
# except the version with the `primary-key`, if it exists
|
||||
purge-primary-key: never
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get branch name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@f44339b51f74753b57583fbbd124e18a81170ab1 # v8.1.0
|
||||
uses: tj-actions/branch-names@v8
|
||||
|
||||
- name: "Branch name to Docker tag name"
|
||||
id: docker-tag-name
|
||||
@@ -69,81 +38,58 @@ jobs:
|
||||
echo "tag=${tag}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Build and push Non-Nix image
|
||||
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: b4q6ltmpzh
|
||||
token: ${{ secrets.DEPOT_TOKEN }}
|
||||
buildx-fallback: true
|
||||
context: "{{defaultContext}}:dogfood/coder"
|
||||
context: "{{defaultContext}}:dogfood"
|
||||
pull: true
|
||||
save: true
|
||||
push: ${{ github.ref == 'refs/heads/main' }}
|
||||
tags: "codercom/oss-dogfood:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood:latest"
|
||||
|
||||
- name: Build Nix image
|
||||
run: nix build .#dev_image
|
||||
|
||||
- name: Push Nix image
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
docker load -i result
|
||||
|
||||
CURRENT_SYSTEM=$(nix eval --impure --raw --expr 'builtins.currentSystem')
|
||||
|
||||
docker image tag codercom/oss-dogfood-nix:latest-$CURRENT_SYSTEM codercom/oss-dogfood-nix:${{ steps.docker-tag-name.outputs.tag }}
|
||||
docker image push codercom/oss-dogfood-nix:${{ steps.docker-tag-name.outputs.tag }}
|
||||
|
||||
docker image tag codercom/oss-dogfood-nix:latest-$CURRENT_SYSTEM codercom/oss-dogfood-nix:latest
|
||||
docker image push codercom/oss-dogfood-nix:latest
|
||||
- name: Build and push Nix image
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: b4q6ltmpzh
|
||||
token: ${{ secrets.DEPOT_TOKEN }}
|
||||
buildx-fallback: true
|
||||
context: "."
|
||||
file: "dogfood/Dockerfile.nix"
|
||||
pull: true
|
||||
save: true
|
||||
push: ${{ github.ref == 'refs/heads/main' }}
|
||||
tags: "codercom/oss-dogfood-nix:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood-nix:latest"
|
||||
|
||||
deploy_template:
|
||||
needs: build_image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
|
||||
with:
|
||||
workload_identity_provider: projects/573722524737/locations/global/workloadIdentityPools/github/providers/github
|
||||
service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com
|
||||
|
||||
- name: Terraform init and validate
|
||||
run: |
|
||||
pushd dogfood/
|
||||
terraform init
|
||||
cd dogfood
|
||||
terraform init -upgrade
|
||||
terraform validate
|
||||
popd
|
||||
pushd dogfood/coder
|
||||
terraform init
|
||||
terraform validate
|
||||
popd
|
||||
pushd dogfood/coder-envbuilder
|
||||
terraform init
|
||||
terraform validate
|
||||
popd
|
||||
|
||||
- name: Get short commit SHA
|
||||
if: github.ref == 'refs/heads/main'
|
||||
@@ -155,18 +101,22 @@ jobs:
|
||||
id: message
|
||||
run: echo "pr_title=$(git log --format=%s -n 1 ${{ github.sha }})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Get latest Coder binary from the server"
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
curl -fsSL "https://dev.coder.com/bin/coder-linux-amd64" -o "./coder"
|
||||
chmod +x "./coder"
|
||||
|
||||
- name: "Push template"
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
cd dogfood
|
||||
terraform apply -auto-approve
|
||||
./coder templates push $CODER_TEMPLATE_NAME --directory $CODER_TEMPLATE_DIR --yes --name=$CODER_TEMPLATE_VERSION --message="$CODER_TEMPLATE_MESSAGE"
|
||||
env:
|
||||
# Consumed by coderd provider
|
||||
# Consumed by Coder CLI
|
||||
CODER_URL: https://dev.coder.com
|
||||
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
|
||||
# Template source & details
|
||||
TF_VAR_CODER_TEMPLATE_NAME: ${{ secrets.CODER_TEMPLATE_NAME }}
|
||||
TF_VAR_CODER_TEMPLATE_VERSION: ${{ steps.vars.outputs.sha_short }}
|
||||
TF_VAR_CODER_TEMPLATE_DIR: ./coder
|
||||
TF_VAR_CODER_TEMPLATE_MESSAGE: ${{ steps.message.outputs.pr_title }}
|
||||
TF_LOG: info
|
||||
CODER_TEMPLATE_NAME: ${{ secrets.CODER_TEMPLATE_NAME }}
|
||||
CODER_TEMPLATE_VERSION: ${{ steps.vars.outputs.sha_short }}
|
||||
CODER_TEMPLATE_DIR: ./dogfood
|
||||
CODER_TEMPLATE_MESSAGE: ${{ steps.message.outputs.pr_title }}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Workflow for serving the webapp locally & running Meticulous tests against it.
|
||||
|
||||
name: Meticulous
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "site/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "site/**"
|
||||
# Meticulous needs the workflow to be triggered on workflow_dispatch events,
|
||||
# so that Meticulous can run the workflow on the base commit to compare
|
||||
# against if an existing workflow hasn't run.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: read
|
||||
|
||||
jobs:
|
||||
meticulous:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build
|
||||
working-directory: ./site
|
||||
run: pnpm build
|
||||
- name: Serve
|
||||
working-directory: ./site
|
||||
run: |
|
||||
pnpm vite preview &
|
||||
sleep 5
|
||||
- name: Run Meticulous tests
|
||||
uses: alwaysmeticulous/report-diffs-action/cloud-compute@v1
|
||||
with:
|
||||
api-token: ${{ secrets.METICULOUS_API_TOKEN }}
|
||||
app-url: "http://127.0.0.1:4173/"
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"ignorePatterns": [
|
||||
{
|
||||
"pattern": "://localhost"
|
||||
},
|
||||
{
|
||||
"pattern": "://.*.?example\\.com"
|
||||
},
|
||||
{
|
||||
"pattern": "developer.github.com"
|
||||
},
|
||||
{
|
||||
"pattern": "docs.github.com"
|
||||
},
|
||||
{
|
||||
"pattern": "support.google.com"
|
||||
},
|
||||
{
|
||||
"pattern": "tailscale.com"
|
||||
},
|
||||
{
|
||||
"pattern": "wireguard.com"
|
||||
}
|
||||
],
|
||||
"aliveStatusCodes": [200, 0]
|
||||
}
|
||||
@@ -3,38 +3,21 @@
|
||||
name: nightly-gauntlet
|
||||
on:
|
||||
schedule:
|
||||
# Every day at 4AM
|
||||
- cron: "0 4 * * 1-5"
|
||||
# Every day at midnight
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-go-pg:
|
||||
runs-on: ${{ matrix.os == 'macos-latest' && github.repository_owner == 'coder' && 'depot-macos-latest' || matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'windows-latest-16-cores' || matrix.os }}
|
||||
if: github.ref == 'refs/heads/main'
|
||||
# This timeout must be greater than the timeout set by `go test` in
|
||||
# `make test-postgres` to ensure we receive a trace of running
|
||||
# goroutines. Setting this to the timeout +5m should work quite well
|
||||
# even if some of the preceding steps are slow.
|
||||
timeout-minutes: 25
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-latest
|
||||
- windows-2022
|
||||
go-race:
|
||||
# While GitHub's toaster runners are likelier to flake, we want consistency
|
||||
# between this environment and the regular test environment for DataDog
|
||||
# statistics and to only show real workflow threats.
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
# This runner costs 0.016 USD per minute,
|
||||
# so 0.016 * 240 = 3.84 USD per run.
|
||||
timeout-minutes: 240
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
@@ -42,101 +25,36 @@ jobs:
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
|
||||
# Sets up the ImDisk toolkit for Windows and creates a RAM disk on drive R:.
|
||||
- name: Setup ImDisk
|
||||
if: runner.os == 'Windows'
|
||||
uses: ./.github/actions/setup-imdisk
|
||||
|
||||
- name: Test with PostgreSQL Database
|
||||
env:
|
||||
POSTGRES_VERSION: "13"
|
||||
TS_DEBUG_DISCO: "true"
|
||||
LC_CTYPE: "en_US.UTF-8"
|
||||
LC_ALL: "en_US.UTF-8"
|
||||
shell: bash
|
||||
- name: Run Tests
|
||||
run: |
|
||||
# if macOS, install google-chrome for scaletests
|
||||
# As another concern, should we really have this kind of external dependency
|
||||
# requirement on standard CI?
|
||||
if [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
brew install google-chrome
|
||||
fi
|
||||
# -race is likeliest to catch flaky tests
|
||||
# due to correctness detection and its performance
|
||||
# impact.
|
||||
gotestsum --junitfile="gotests.xml" -- -timeout=240m -count=10 -race ./...
|
||||
|
||||
# By default Go will use the number of logical CPUs, which
|
||||
# is a fine default.
|
||||
PARALLEL_FLAG=""
|
||||
|
||||
# macOS will output "The default interactive shell is now zsh"
|
||||
# intermittently in CI...
|
||||
if [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
touch ~/.bash_profile && echo "export BASH_SILENCE_DEPRECATION_WARNING=1" >> ~/.bash_profile
|
||||
fi
|
||||
|
||||
if [ "${{ runner.os }}" == "Windows" ]; then
|
||||
# Create a temp dir on the R: ramdisk drive for Windows. The default
|
||||
# C: drive is extremely slow: https://github.com/actions/runner-images/issues/8755
|
||||
mkdir -p "R:/temp/embedded-pg"
|
||||
go run scripts/embedded-pg/main.go -path "R:/temp/embedded-pg"
|
||||
else
|
||||
go run scripts/embedded-pg/main.go
|
||||
fi
|
||||
|
||||
# Reduce test parallelism, mirroring what we do for race tests.
|
||||
# We'd been encountering issues with timing related flakes, and
|
||||
# this seems to help.
|
||||
DB=ci gotestsum --format standard-quiet -- -v -short -count=1 -parallel 4 -p 4 ./...
|
||||
|
||||
- name: Upload test stats to Datadog
|
||||
timeout-minutes: 1
|
||||
continue-on-error: true
|
||||
- name: Upload test results to DataDog
|
||||
uses: ./.github/actions/upload-datadog
|
||||
if: success() || failure()
|
||||
if: always()
|
||||
with:
|
||||
api-key: ${{ secrets.DATADOG_API_KEY }}
|
||||
|
||||
notify-slack-on-failure:
|
||||
needs:
|
||||
- test-go-pg
|
||||
runs-on: ubuntu-latest
|
||||
if: failure() && github.ref == 'refs/heads/main'
|
||||
|
||||
go-timing:
|
||||
# We run these tests with p=1 so we don't need a lot of compute.
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Send Slack notification
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data '{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": "❌ Nightly gauntlet failed",
|
||||
"emoji": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"fields": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Workflow:*\n${{ github.workflow }}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Committer:*\n${{ github.actor }}"
|
||||
},
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": "*Commit:*\n${{ github.sha }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*View failure:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Click here>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}' ${{ secrets.CI_FAILURE_SLACK_WEBHOOK }}
|
||||
gotestsum --junitfile="gotests.xml" -- --tags="timing" -p=1 -run='_Timing/' ./...
|
||||
|
||||
- name: Upload test results to DataDog
|
||||
uses: ./.github/actions/upload-datadog
|
||||
if: always()
|
||||
with:
|
||||
api-key: ${{ secrets.DATADOG_API_KEY }}
|
||||
|
||||
@@ -13,10 +13,5 @@ jobs:
|
||||
assign-author:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Assign author
|
||||
uses: toshimaru/auto-author-assign@16f0022cf3d7970c106d8d1105f75a1165edb516 # v2.1.1
|
||||
uses: toshimaru/auto-author-assign@v2.1.1
|
||||
|
||||
@@ -9,20 +9,12 @@ on:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
# Necessary to delete docker images from ghcr.io.
|
||||
packages: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Get PR number
|
||||
id: pr_number
|
||||
run: |
|
||||
@@ -34,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Delete image
|
||||
continue-on-error: true
|
||||
uses: bots-house/ghcr-delete-image-action@3827559c68cb4dcdf54d813ea9853be6d468d3a4 # v1.1.0
|
||||
uses: bots-house/ghcr-delete-image-action@v1.1.0
|
||||
with:
|
||||
owner: coder
|
||||
name: coder-preview
|
||||
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- main
|
||||
- "temp-cherry-pick-*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
experiments:
|
||||
@@ -31,6 +30,8 @@ env:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
pull-requests: write # needed for commenting on PRs
|
||||
|
||||
jobs:
|
||||
check_pr:
|
||||
@@ -38,13 +39,8 @@ jobs:
|
||||
outputs:
|
||||
PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check if PR is open
|
||||
id: check_pr
|
||||
@@ -73,13 +69,8 @@ jobs:
|
||||
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -111,7 +102,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG_BASE64 }}" | base64 --decode > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
chmod 644 ~/.kube/config
|
||||
export KUBECONFIG=~/.kube/config
|
||||
|
||||
- name: Check if the helm deployment already exists
|
||||
@@ -128,7 +119,7 @@ jobs:
|
||||
echo "NEW=$NEW" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check changed files
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ github.ref }}
|
||||
@@ -163,23 +154,16 @@ jobs:
|
||||
set -euo pipefail
|
||||
# build if the workflow is manually triggered and the deployment doesn't exist (first build or force rebuild)
|
||||
echo "first_or_force_build=${{ (github.event_name == 'workflow_dispatch' && steps.check_deployment.outputs.NEW == 'true') || github.event.inputs.build == 'true' }}" >> $GITHUB_OUTPUT
|
||||
# build if the deployment already exist and there are changes in the files that we care about (automatic updates)
|
||||
# build if the deployment alreday exist and there are changes in the files that we care about (automatic updates)
|
||||
echo "automatic_rebuild=${{ steps.check_deployment.outputs.NEW == 'false' && steps.filter.outputs.all_count > steps.filter.outputs.ignored_count }}" >> $GITHUB_OUTPUT
|
||||
|
||||
comment-pr:
|
||||
needs: get_info
|
||||
if: needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true'
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
pull-requests: write # needed for commenting on PRs
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ needs.get_info.outputs.PR_NUMBER }}
|
||||
@@ -189,7 +173,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR
|
||||
id: comment_id
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ needs.get_info.outputs.PR_NUMBER }}
|
||||
@@ -206,10 +190,7 @@ jobs:
|
||||
# Run build job only if there are changes in the files that we care about or if the workflow is manually triggered with --build flag
|
||||
if: needs.get_info.outputs.BUILD == 'true'
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# Necessary to push docker images to ghcr.io.
|
||||
packages: write
|
||||
# This concurrency only cancels build jobs if a new build is triggred. It will avoid cancelling the current deployemtn in case of docs changes.
|
||||
# This concurrency only cancels build jobs if a new build is triggred. It will avoid cancelling the current deployemtn in case of docs chnages.
|
||||
concurrency:
|
||||
group: build-${{ github.workflow }}-${{ github.ref }}-${{ needs.get_info.outputs.BUILD }}
|
||||
cancel-in-progress: true
|
||||
@@ -217,13 +198,8 @@ jobs:
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -237,7 +213,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-sqlc
|
||||
|
||||
- name: GHCR Login
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -266,8 +242,6 @@ jobs:
|
||||
always() && (needs.build.result == 'success' || needs.build.result == 'skipped') &&
|
||||
(needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true')
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
pull-requests: write # needed for commenting on PRs
|
||||
env:
|
||||
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
|
||||
PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }}
|
||||
@@ -275,17 +249,12 @@ jobs:
|
||||
PR_URL: ${{ needs.get_info.outputs.PR_URL }}
|
||||
PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Set up kubeconfig
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG_BASE64 }}" | base64 --decode > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
chmod 644 ~/.kube/config
|
||||
export KUBECONFIG=~/.kube/config
|
||||
|
||||
- name: Check if image exists
|
||||
@@ -325,7 +294,7 @@ jobs:
|
||||
kubectl create namespace "pr${{ env.PR_NUMBER }}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check and Create Certificate
|
||||
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
|
||||
@@ -422,14 +391,14 @@ jobs:
|
||||
"${DEST}" version
|
||||
mv "${DEST}" /usr/local/bin/coder
|
||||
|
||||
- name: Create first user
|
||||
- name: Create first user, template and workspace
|
||||
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
|
||||
id: setup_deployment
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Create first user
|
||||
|
||||
# create a masked random password 12 characters long
|
||||
password=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-12)
|
||||
|
||||
@@ -438,22 +407,20 @@ jobs:
|
||||
echo "password=$password" >> $GITHUB_OUTPUT
|
||||
|
||||
coder login \
|
||||
--first-user-username pr${{ env.PR_NUMBER }}-admin \
|
||||
--first-user-username coder \
|
||||
--first-user-email pr${{ env.PR_NUMBER }}@coder.com \
|
||||
--first-user-password $password \
|
||||
--first-user-trial=false \
|
||||
--first-user-trial \
|
||||
--use-token-as-session \
|
||||
https://${{ env.PR_HOSTNAME }}
|
||||
|
||||
# Create a user for the github.actor
|
||||
# TODO: update once https://github.com/coder/coder/issues/15466 is resolved
|
||||
# coder users create \
|
||||
# --username ${{ github.actor }} \
|
||||
# --login-type github
|
||||
# Create template
|
||||
cd ./.github/pr-deployments/template
|
||||
coder templates push -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes
|
||||
|
||||
# promote the user to admin role
|
||||
# coder org members edit-role ${{ github.actor }} organization-admin
|
||||
# TODO: update once https://github.com/coder/internal/issues/207 is resolved
|
||||
# Create workspace
|
||||
coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y
|
||||
coder stop kube -y
|
||||
|
||||
- name: Send Slack notification
|
||||
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
|
||||
@@ -465,7 +432,7 @@ jobs:
|
||||
"pr_url": "'"${{ env.PR_URL }}"'",
|
||||
"pr_title": "'"${{ env.PR_TITLE }}"'",
|
||||
"pr_access_url": "'"https://${{ env.PR_HOSTNAME }}"'",
|
||||
"pr_username": "'"pr${{ env.PR_NUMBER }}-admin"'",
|
||||
"pr_username": "'"test"'",
|
||||
"pr_email": "'"pr${{ env.PR_NUMBER }}@coder.com"'",
|
||||
"pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'",
|
||||
"pr_actor": "'"${{ github.actor }}"'"
|
||||
@@ -474,7 +441,7 @@ jobs:
|
||||
echo "Slack notification sent"
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
@@ -483,7 +450,7 @@ jobs:
|
||||
direction: last
|
||||
|
||||
- name: Comment on PR
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
env:
|
||||
STATUS: ${{ needs.get_info.outputs.NEW == 'true' && 'Created' || 'Updated' }}
|
||||
with:
|
||||
@@ -498,14 +465,3 @@ jobs:
|
||||
cc: @${{ github.actor }}
|
||||
reactions: rocket
|
||||
reactions-edit-mode: replace
|
||||
|
||||
- name: Create template and workspace
|
||||
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd .github/pr-deployments/template
|
||||
coder templates push -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes
|
||||
|
||||
# Create workspace
|
||||
coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y
|
||||
coder stop kube -y
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
name: release-validation
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
network-performance:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Run Schmoder CI
|
||||
uses: benc-uk/workflow-dispatch@e2e5e9a103e331dad343f381a29e654aea3cf8fc # v1.2.4
|
||||
with:
|
||||
workflow: ci.yaml
|
||||
repo: coder/schmoder
|
||||
inputs: '{ "num_releases": "3", "commit": "${{ github.sha }}" }'
|
||||
token: ${{ secrets.CDRCI_SCHMODER_ACTIONS_TOKEN }}
|
||||
ref: main
|
||||
+23
-307
@@ -18,7 +18,12 @@ on:
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Required to publish a release
|
||||
contents: write
|
||||
# Necessary to push docker images to ghcr.io.
|
||||
packages: write
|
||||
# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
|
||||
id-token: write
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -32,114 +37,17 @@ env:
|
||||
CODER_RELEASE_NOTES: ${{ inputs.release_notes }}
|
||||
|
||||
jobs:
|
||||
# build-dylib is a separate job to build the dylib on macOS.
|
||||
build-dylib:
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }}
|
||||
steps:
|
||||
# Harden Runner doesn't work on macOS.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# If the event that triggered the build was an annotated tag (which our
|
||||
# tags are supposed to be), actions/checkout has a bug where the tag in
|
||||
# question is only a lightweight tag and not a full annotated tag. This
|
||||
# command seems to fix it.
|
||||
# https://github.com/actions/checkout/issues/290
|
||||
- name: Fetch git tags
|
||||
run: git fetch --tags --force
|
||||
|
||||
- name: Setup build tools
|
||||
run: |
|
||||
brew install bash gnu-getopt make
|
||||
echo "$(brew --prefix bash)/bin" >> $GITHUB_PATH
|
||||
echo "$(brew --prefix gnu-getopt)/bin" >> $GITHUB_PATH
|
||||
echo "$(brew --prefix make)/libexec/gnubin" >> $GITHUB_PATH
|
||||
|
||||
- name: Switch XCode Version
|
||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
||||
with:
|
||||
xcode-version: "16.0.0"
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
|
||||
sudo tar -xzf /tmp/rcodesign.tar.gz \
|
||||
-C /usr/local/bin \
|
||||
--strip-components=1 \
|
||||
apple-codesign-0.22.0-macos-universal/rcodesign
|
||||
rm /tmp/rcodesign.tar.gz
|
||||
|
||||
- name: Setup Apple Developer certificate and API key
|
||||
run: |
|
||||
set -euo pipefail
|
||||
touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
|
||||
chmod 600 /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
|
||||
echo "$AC_CERTIFICATE_P12_BASE64" | base64 -d > /tmp/apple_cert.p12
|
||||
echo "$AC_CERTIFICATE_PASSWORD" > /tmp/apple_cert_password.txt
|
||||
echo "$AC_APIKEY_P8_BASE64" | base64 -d > /tmp/apple_apikey.p8
|
||||
env:
|
||||
AC_CERTIFICATE_P12_BASE64: ${{ secrets.AC_CERTIFICATE_P12_BASE64 }}
|
||||
AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
|
||||
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
|
||||
|
||||
- name: Build dylibs
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
go mod download
|
||||
|
||||
make gen/mark-fresh
|
||||
make build/coder-dylib
|
||||
env:
|
||||
CODER_SIGN_DARWIN: 1
|
||||
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
|
||||
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: dylibs
|
||||
path: |
|
||||
./build/*.h
|
||||
./build/*.dylib
|
||||
retention-days: 7
|
||||
|
||||
- name: Delete Apple Developer certificate and API key
|
||||
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
|
||||
|
||||
release:
|
||||
name: Build and publish
|
||||
needs: build-dylib
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# Required to publish a release
|
||||
contents: write
|
||||
# Necessary to push docker images to ghcr.io.
|
||||
packages: write
|
||||
# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
|
||||
# Also necessary for keyless cosign (https://docs.sigstore.dev/cosign/signing/overview/)
|
||||
# And for GitHub Actions attestation
|
||||
id-token: write
|
||||
# Required for GitHub Actions attestation
|
||||
attestations: write
|
||||
env:
|
||||
# Necessary for Docker manifest
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -208,7 +116,7 @@ jobs:
|
||||
cat "$CODER_RELEASE_NOTES_FILE"
|
||||
|
||||
- name: Docker Login
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -222,14 +130,11 @@ jobs:
|
||||
|
||||
# Necessary for signing Windows binaries.
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "11.0"
|
||||
|
||||
- name: Install go-winres
|
||||
run: go install github.com/tc-hib/go-winres@d743268d7ea168077ddd443c4240562d4f5e8c3e # v0.3.3
|
||||
|
||||
- name: Install nsis and zstd
|
||||
run: sudo apt-get install -y nsis zstd
|
||||
|
||||
@@ -250,12 +155,6 @@ jobs:
|
||||
apple-codesign-0.22.0-x86_64-unknown-linux-musl/rcodesign
|
||||
rm /tmp/rcodesign.tar.gz
|
||||
|
||||
- name: Install cosign
|
||||
uses: ./.github/actions/install-cosign
|
||||
|
||||
- name: Install syft
|
||||
uses: ./.github/actions/install-syft
|
||||
|
||||
- name: Setup Apple Developer certificate and API key
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -286,26 +185,14 @@ jobs:
|
||||
# Setup GCloud for signing Windows binaries.
|
||||
- name: Authenticate to Google Cloud
|
||||
id: gcloud_auth
|
||||
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }}
|
||||
token_format: "access_token"
|
||||
|
||||
- name: Setup GCloud SDK
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
|
||||
|
||||
- name: Download dylibs
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
with:
|
||||
name: dylibs
|
||||
path: ./build
|
||||
|
||||
- name: Insert dylibs
|
||||
run: |
|
||||
mv ./build/*amd64.dylib ./site/out/bin/coder-vpn-darwin-amd64.dylib
|
||||
mv ./build/*arm64.dylib ./site/out/bin/coder-vpn-darwin-arm64.dylib
|
||||
mv ./build/*arm64.h ./site/out/bin/coder-vpn-darwin-dylib.h
|
||||
uses: "google-github-actions/setup-gcloud@v2"
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
@@ -323,7 +210,6 @@ jobs:
|
||||
env:
|
||||
CODER_SIGN_WINDOWS: "1"
|
||||
CODER_SIGN_DARWIN: "1"
|
||||
CODER_WINDOWS_RESOURCES: "1"
|
||||
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
|
||||
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
|
||||
AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }}
|
||||
@@ -359,19 +245,17 @@ jobs:
|
||||
|
||||
- name: Install depot.dev CLI
|
||||
if: steps.image-base-tag.outputs.tag != ''
|
||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
# This uses OIDC authentication, so no auth variables are required.
|
||||
- name: Build base Docker image via depot.dev
|
||||
if: steps.image-base-tag.outputs.tag != ''
|
||||
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: wl5hnrrkns
|
||||
context: base-build-context
|
||||
file: scripts/Dockerfile.base
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
provenance: true
|
||||
sbom: true
|
||||
pull: true
|
||||
no-cache: true
|
||||
push: true
|
||||
@@ -379,7 +263,6 @@ jobs:
|
||||
${{ steps.image-base-tag.outputs.tag }}
|
||||
|
||||
- name: Verify that images are pushed properly
|
||||
if: steps.image-base-tag.outputs.tag != ''
|
||||
run: |
|
||||
# retry 10 times with a 5 second delay as the images may not be
|
||||
# available immediately
|
||||
@@ -408,55 +291,14 @@ jobs:
|
||||
echo "$manifests" | grep -q linux/arm64
|
||||
echo "$manifests" | grep -q linux/arm/v7
|
||||
|
||||
# GitHub attestation provides SLSA provenance for Docker images, establishing a verifiable
|
||||
# record that these images were built in GitHub Actions with specific inputs and environment.
|
||||
# This complements our existing cosign attestations (which focus on SBOMs) by adding
|
||||
# GitHub-specific build provenance to enhance our supply chain security.
|
||||
#
|
||||
# TODO: Consider refactoring these attestation steps to use a matrix strategy or composite action
|
||||
# to reduce duplication while maintaining the required functionality for each distinct image tag.
|
||||
- name: GitHub Attestation for Base Docker image
|
||||
id: attest_base
|
||||
if: ${{ !inputs.dry_run && steps.image-base-tag.outputs.tag != '' }}
|
||||
continue-on-error: true
|
||||
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
|
||||
with:
|
||||
subject-name: ${{ steps.image-base-tag.outputs.tag }}
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/release.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Build Linux Docker images
|
||||
id: build_docker
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
# build Docker images for each architecture
|
||||
version="$(./scripts/version.sh)"
|
||||
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
|
||||
|
||||
# we can't build multi-arch if the images aren't pushed, so quit now
|
||||
# if dry-running
|
||||
if [[ "$CODER_RELEASE" != *t* ]]; then
|
||||
@@ -464,133 +306,22 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# build Docker images for each architecture
|
||||
version="$(./scripts/version.sh)"
|
||||
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
|
||||
|
||||
# build and push multi-arch manifest, this depends on the other images
|
||||
# being pushed so will automatically push them.
|
||||
make push/build/coder_"$version"_linux.tag
|
||||
|
||||
# Save multiarch image tag for attestation
|
||||
multiarch_image="$(./scripts/image_tag.sh)"
|
||||
echo "multiarch_image=${multiarch_image}" >> $GITHUB_OUTPUT
|
||||
|
||||
# For debugging, print all docker image tags
|
||||
docker images
|
||||
|
||||
# if the current version is equal to the highest (according to semver)
|
||||
# version in the repo, also create a multi-arch image as ":latest" and
|
||||
# push it
|
||||
created_latest_tag=false
|
||||
if [[ "$(git tag | grep '^v' | grep -vE '(rc|dev|-|\+|\/)' | sort -r --version-sort | head -n1)" == "v$(./scripts/version.sh)" ]]; then
|
||||
./scripts/build_docker_multiarch.sh \
|
||||
--push \
|
||||
--target "$(./scripts/image_tag.sh --version latest)" \
|
||||
$(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
|
||||
created_latest_tag=true
|
||||
echo "created_latest_tag=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "created_latest_tag=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
|
||||
|
||||
- name: GitHub Attestation for Docker image
|
||||
id: attest_main
|
||||
if: ${{ !inputs.dry_run }}
|
||||
continue-on-error: true
|
||||
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
|
||||
with:
|
||||
subject-name: ${{ steps.build_docker.outputs.multiarch_image }}
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/release.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
# Get the latest tag name for attestation
|
||||
- name: Get latest tag name
|
||||
id: latest_tag
|
||||
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
|
||||
run: echo "tag=$(./scripts/image_tag.sh --version latest)" >> $GITHUB_OUTPUT
|
||||
|
||||
# If this is the highest version according to semver, also attest the "latest" tag
|
||||
- name: GitHub Attestation for "latest" Docker image
|
||||
id: attest_latest
|
||||
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: actions/attest@a63cfcc7d1aab266ee064c58250cfc2c7d07bc31 # v2.2.1
|
||||
with:
|
||||
subject-name: ${{ steps.latest_tag.outputs.tag }}
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/release.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
# Report attestation failures but don't fail the workflow
|
||||
- name: Check attestation status
|
||||
if: ${{ !inputs.dry_run }}
|
||||
run: |
|
||||
if [[ "${{ steps.attest_base.outcome }}" == "failure" && "${{ steps.attest_base.conclusion }}" != "skipped" ]]; then
|
||||
echo "::warning::GitHub attestation for base image failed"
|
||||
fi
|
||||
if [[ "${{ steps.attest_main.outcome }}" == "failure" ]]; then
|
||||
echo "::warning::GitHub attestation for main image failed"
|
||||
fi
|
||||
if [[ "${{ steps.attest_latest.outcome }}" == "failure" && "${{ steps.attest_latest.conclusion }}" != "skipped" ]]; then
|
||||
echo "::warning::GitHub attestation for latest image failed"
|
||||
fi
|
||||
|
||||
- name: Generate offline docs
|
||||
run: |
|
||||
version="$(./scripts/version.sh)"
|
||||
@@ -627,13 +358,13 @@ jobs:
|
||||
CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Setup GCloud SDK
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # 2.1.4
|
||||
uses: "google-github-actions/setup-gcloud@v2"
|
||||
|
||||
- name: Publish Helm Chart
|
||||
if: ${{ !inputs.dry_run }}
|
||||
@@ -652,7 +383,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts to actions (if dry-run)
|
||||
if: ${{ inputs.dry_run }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-artifacts
|
||||
path: |
|
||||
@@ -667,7 +398,7 @@ jobs:
|
||||
|
||||
- name: Send repository-dispatch event
|
||||
if: ${{ !inputs.dry_run }}
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
|
||||
repository: coder/packages
|
||||
@@ -683,11 +414,6 @@ jobs:
|
||||
steps:
|
||||
# TODO: skip this if it's not a new release (i.e. a backport). This is
|
||||
# fine right now because it just makes a PR that we can close.
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Update homebrew
|
||||
env:
|
||||
# Variables used by the `gh` command
|
||||
@@ -759,18 +485,13 @@ jobs:
|
||||
if: ${{ !inputs.dry_run }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Sync fork
|
||||
run: gh repo sync cdrci/winget-pkgs -b master
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -849,13 +570,8 @@ jobs:
|
||||
needs: release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
name: OpenSSF Scorecard
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: "27 7 * * 3" # A random time to run weekly
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
# Needed to publish results and get a badge (see publish_results below).
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_results: true
|
||||
|
||||
# Upload the results as artifacts.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -3,6 +3,7 @@ name: "security"
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -22,23 +23,16 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
permissions:
|
||||
security-events: write
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go, javascript
|
||||
|
||||
@@ -48,7 +42,7 @@ jobs:
|
||||
rm Makefile
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
- name: Send Slack notification on failure
|
||||
if: ${{ failure() }}
|
||||
@@ -62,17 +56,10 @@ jobs:
|
||||
"${{ secrets.SLACK_SECURITY_FAILURE_WEBHOOK_URL }}"
|
||||
|
||||
trivy:
|
||||
permissions:
|
||||
security-events: write
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -85,39 +72,26 @@ jobs:
|
||||
- name: Setup sqlc
|
||||
uses: ./.github/actions/setup-sqlc
|
||||
|
||||
- name: Install cosign
|
||||
uses: ./.github/actions/install-cosign
|
||||
|
||||
- name: Install syft
|
||||
uses: ./.github/actions/install-syft
|
||||
|
||||
- name: Install yq
|
||||
run: go run github.com/mikefarah/yq/v4@v4.44.3
|
||||
run: go run github.com/mikefarah/yq/v4@v4.30.6
|
||||
- name: Install mockgen
|
||||
run: go install go.uber.org/mock/mockgen@v0.5.0
|
||||
run: go install go.uber.org/mock/mockgen@v0.4.0
|
||||
- name: Install protoc-gen-go
|
||||
run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
|
||||
- name: Install protoc-gen-go-drpc
|
||||
run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34
|
||||
run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
|
||||
- name: Install Protoc
|
||||
run: |
|
||||
# protoc must be in lockstep with our dogfood Dockerfile or the
|
||||
# version in the comments will differ. This is also defined in
|
||||
# ci.yaml.
|
||||
set -euxo pipefail
|
||||
cd dogfood/coder
|
||||
mkdir -p /usr/local/bin
|
||||
mkdir -p /usr/local/include
|
||||
|
||||
set -x
|
||||
cd dogfood
|
||||
DOCKER_BUILDKIT=1 docker build . --target proto -t protoc
|
||||
protoc_path=/usr/local/bin/protoc
|
||||
docker run --rm --entrypoint cat protoc /tmp/bin/protoc > $protoc_path
|
||||
chmod +x $protoc_path
|
||||
protoc --version
|
||||
# Copy the generated files to the include directory.
|
||||
docker run --rm -v /usr/local/include:/target protoc cp -r /tmp/include/google /target/
|
||||
ls -la /usr/local/include/google/protobuf/
|
||||
stat /usr/local/include/google/protobuf/timestamp.proto
|
||||
|
||||
- name: Build Coder linux amd64 Docker image
|
||||
id: build
|
||||
@@ -136,13 +110,11 @@ jobs:
|
||||
# the registry.
|
||||
export CODER_IMAGE_BUILD_BASE_TAG="$(CODER_IMAGE_BASE=coder-base ./scripts/image_tag.sh --version "$version")"
|
||||
|
||||
# We would like to use make -j here, but it doesn't work with the some recent additions
|
||||
# to our code generation.
|
||||
make "$image_job"
|
||||
make -j "$image_job"
|
||||
echo "image=$(cat "$image_job")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5
|
||||
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8
|
||||
with:
|
||||
image-ref: ${{ steps.build.outputs.image }}
|
||||
format: sarif
|
||||
@@ -150,18 +122,28 @@ jobs:
|
||||
severity: "CRITICAL,HIGH"
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: "Trivy"
|
||||
|
||||
- name: Upload Trivy scan results as an artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: trivy
|
||||
path: trivy-results.sarif
|
||||
retention-days: 7
|
||||
|
||||
# Prisma cloud scan runs last because it fails the entire job if it
|
||||
# detects vulnerabilities. :|
|
||||
- name: Run Prisma Cloud image scan
|
||||
uses: PaloAltoNetworks/prisma-cloud-scan@v1
|
||||
with:
|
||||
pcc_console_url: ${{ secrets.PRISMA_CLOUD_URL }}
|
||||
pcc_user: ${{ secrets.PRISMA_CLOUD_ACCESS_KEY }}
|
||||
pcc_pass: ${{ secrets.PRISMA_CLOUD_SECRET_KEY }}
|
||||
image_name: ${{ steps.build.outputs.image }}
|
||||
|
||||
- name: Send Slack notification on failure
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
name: Stale Issue, Branch and Old Workflows Cleanup
|
||||
name: Stale Issue, Banch and Old Workflows Cleanup
|
||||
on:
|
||||
schedule:
|
||||
# Every day at midnight
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to close issues.
|
||||
issues: write
|
||||
# Needed to close PRs.
|
||||
pull-requests: write
|
||||
actions: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: stale
|
||||
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
uses: actions/stale@v9.0.0
|
||||
with:
|
||||
stale-issue-label: "stale"
|
||||
stale-pr-label: "stale"
|
||||
# days-before-stale: 180
|
||||
# essentially disabled for now while we work through polish issues
|
||||
days-before-stale: 3650
|
||||
|
||||
days-before-stale: 180
|
||||
# Pull Requests become stale more quickly due to merge conflicts.
|
||||
# Also, we promote minimizing WIP.
|
||||
days-before-pr-stale: 7
|
||||
@@ -44,7 +31,7 @@ jobs:
|
||||
# Start with the oldest issues, always.
|
||||
ascending: true
|
||||
- name: "Close old issues labeled likely-no"
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -70,7 +57,7 @@ jobs:
|
||||
});
|
||||
|
||||
const labelEvent = timeline.data.find(event => event.event === 'labeled' && event.label.name === 'likely-no');
|
||||
|
||||
|
||||
if (labelEvent) {
|
||||
console.log(`Issue #${issue.number} was labeled with 'likely-no' at ${labelEvent.created_at}`);
|
||||
|
||||
@@ -91,19 +78,11 @@ jobs:
|
||||
|
||||
branches:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to delete branches.
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
- name: Run delete-old-branches-action
|
||||
uses: beatlabs/delete-old-branches-action@4eeeb8740ff8b3cb310296ddd6b43c3387734588 # v0.0.11
|
||||
uses: beatlabs/delete-old-branches-action@v0.0.10
|
||||
with:
|
||||
repo_token: ${{ github.token }}
|
||||
date: "6 months ago"
|
||||
@@ -113,17 +92,9 @@ jobs:
|
||||
exclude_open_pr_branches: true
|
||||
del_runs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to delete workflow runs.
|
||||
actions: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Delete PR Cleanup workflow runs
|
||||
uses: Mattraks/delete-workflow-runs@39f0bbed25d76b34de5594dceab824811479e5de # v2.0.6
|
||||
uses: Mattraks/delete-workflow-runs@v2
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
@@ -132,7 +103,7 @@ jobs:
|
||||
delete_workflow_pattern: pr-cleanup.yaml
|
||||
|
||||
- name: Delete PR Deploy workflow skipped runs
|
||||
uses: Mattraks/delete-workflow-runs@39f0bbed25d76b34de5594dceab824811479e5de # v2.0.6
|
||||
uses: Mattraks/delete-workflow-runs@v2
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Start Workspace On Issue Creation or Comment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
environment: aidev
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Start Coder workspace
|
||||
uses: coder/start-workspace-action@26d3600161d67901f24d8612793d3b82771cde2d
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
trigger-phrase: "@coder"
|
||||
coder-url: ${{ secrets.CODER_URL }}
|
||||
coder-token: ${{ secrets.CODER_TOKEN }}
|
||||
template-name: ${{ secrets.CODER_TEMPLATE_NAME }}
|
||||
workspace-name: issue-${{ github.event.issue.number }}
|
||||
parameters: |-
|
||||
Coder Image: codercom/oss-dogfood:latest
|
||||
Coder Repository Base Directory: "~"
|
||||
AI Code Prompt: "Use the gh CLI tool to read the details of issue https://github.com/${{ github.repository }}/issues/${{ github.event.issue.number }} and then address it."
|
||||
Region: us-pittsburgh
|
||||
user-mapping: ${{ secrets.CODER_USER_MAPPING }}
|
||||
@@ -22,8 +22,6 @@ pn = "pn"
|
||||
EDE = "EDE"
|
||||
# HELO is an SMTP command
|
||||
HELO = "HELO"
|
||||
LKE = "LKE"
|
||||
byt = "byt"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
@@ -35,12 +33,11 @@ extend-exclude = [
|
||||
# These files contain base64 strings that confuse the detector
|
||||
"**XService**.ts",
|
||||
"**identity.go",
|
||||
"scripts/ci-report/testdata/**",
|
||||
"**/*_test.go",
|
||||
"**/*.test.tsx",
|
||||
"**/pnpm-lock.yaml",
|
||||
"tailnet/testdata/**",
|
||||
"site/src/pages/SetupPage/countries.tsx",
|
||||
"provisioner/terraform/testdata/**",
|
||||
# notifications' golden files confuse the detector because of quoted-printable encoding
|
||||
"coderd/notifications/testdata/**"
|
||||
]
|
||||
|
||||
@@ -10,33 +10,23 @@ on:
|
||||
paths:
|
||||
- "docs/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-docs:
|
||||
# later versions of Ubuntu have disabled unprivileged user namespaces, which are required by the action
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
pull-requests: write # required to post PR review comments by the action
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Check Markdown links
|
||||
uses: umbrelladocs/action-linkspector@49cf4f8da82db70e691bb8284053add5028fa244 # v1.3.2
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
id: markdown-link-check
|
||||
# checks all markdown files from /docs including all subfolders
|
||||
with:
|
||||
reporter: github-pr-review
|
||||
config_file: ".github/.linkspector.yml"
|
||||
fail_on_error: "true"
|
||||
filter_mode: "nofilter"
|
||||
use-quiet-mode: "yes"
|
||||
use-verbose-mode: "yes"
|
||||
config-file: ".github/workflows/mlc_config.json"
|
||||
folder-path: "docs/"
|
||||
file-path: "./README.md"
|
||||
|
||||
- name: Send Slack notification
|
||||
if: failure() && github.event_name == 'schedule'
|
||||
|
||||
+1
-12
@@ -17,8 +17,6 @@ yarn-error.log
|
||||
# Allow VSCode recommendations and default settings in project root.
|
||||
!/.vscode/extensions.json
|
||||
!/.vscode/settings.json
|
||||
# Allow code snippets
|
||||
!/.vscode/*.code-snippets
|
||||
|
||||
# Front-end ignore patterns.
|
||||
.next/
|
||||
@@ -32,12 +30,10 @@ site/e2e/.auth.json
|
||||
site/playwright-report/*
|
||||
site/.swc
|
||||
|
||||
# Make target for updating generated/golden files (any dir).
|
||||
.gen
|
||||
# Make target for updating golden files (any dir).
|
||||
.gen-golden
|
||||
|
||||
# Build
|
||||
bin/
|
||||
build/
|
||||
dist/
|
||||
out/
|
||||
@@ -56,7 +52,6 @@ site/stats/
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
*.test
|
||||
|
||||
# Loadtesting
|
||||
@@ -76,9 +71,3 @@ result
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
|
||||
# Zed
|
||||
.zed_server
|
||||
|
||||
# dlv debug binaries for go tests
|
||||
__debug_bin*
|
||||
|
||||
+32
-17
@@ -24,19 +24,30 @@ linters-settings:
|
||||
enabled-checks:
|
||||
# - appendAssign
|
||||
# - appendCombine
|
||||
- argOrder
|
||||
# - assignOp
|
||||
# - badCall
|
||||
- badCond
|
||||
- badLock
|
||||
- badRegexp
|
||||
- boolExprSimplify
|
||||
# - builtinShadow
|
||||
- builtinShadowDecl
|
||||
- captLocal
|
||||
- caseOrder
|
||||
- codegenComment
|
||||
# - commentedOutCode
|
||||
- commentedOutImport
|
||||
- commentFormatting
|
||||
- defaultCaseOrder
|
||||
- deferUnlambda
|
||||
# - deprecatedComment
|
||||
# - docStub
|
||||
- dupArg
|
||||
- dupBranchBody
|
||||
- dupCase
|
||||
- dupImport
|
||||
- dupSubExpr
|
||||
# - elseif
|
||||
- emptyFallthrough
|
||||
# - emptyStringTest
|
||||
@@ -45,6 +56,8 @@ linters-settings:
|
||||
# - exitAfterDefer
|
||||
# - exposedSyncMutex
|
||||
# - filepathJoin
|
||||
- flagDeref
|
||||
- flagName
|
||||
- hexLiteral
|
||||
# - httpNoBody
|
||||
# - hugeParam
|
||||
@@ -52,36 +65,47 @@ linters-settings:
|
||||
# - importShadow
|
||||
- indexAlloc
|
||||
- initClause
|
||||
- mapKey
|
||||
- methodExprCall
|
||||
# - nestingReduce
|
||||
- newDeref
|
||||
- nilValReturn
|
||||
# - octalLiteral
|
||||
- offBy1
|
||||
# - paramTypeCombine
|
||||
# - preferStringWriter
|
||||
# - preferWriteByte
|
||||
# - ptrToRefParam
|
||||
# - rangeExprCopy
|
||||
# - rangeValCopy
|
||||
- regexpMust
|
||||
- regexpPattern
|
||||
# - regexpSimplify
|
||||
- ruleguard
|
||||
- singleCaseSwitch
|
||||
- sloppyLen
|
||||
# - sloppyReassign
|
||||
- sloppyTypeAssert
|
||||
- sortSlice
|
||||
- sprintfQuotedString
|
||||
- sqlQuery
|
||||
# - stringConcatSimplify
|
||||
# - stringXbytes
|
||||
# - suspiciousSorting
|
||||
- switchTrue
|
||||
- truncateCmp
|
||||
- typeAssertChain
|
||||
# - typeDefFirst
|
||||
- typeSwitchVar
|
||||
# - typeUnparen
|
||||
- underef
|
||||
# - unlabelStmt
|
||||
# - unlambda
|
||||
# - unnamedResult
|
||||
# - unnecessaryBlock
|
||||
# - unnecessaryDefer
|
||||
# - unslice
|
||||
- valSwap
|
||||
- weakCond
|
||||
# - whyNoLint
|
||||
# - wrapperFunc
|
||||
@@ -151,6 +175,8 @@ linters-settings:
|
||||
- name: modifies-value-receiver
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: range-val-address
|
||||
- name: range-val-in-closure
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-of-int
|
||||
@@ -173,20 +199,8 @@ linters-settings:
|
||||
govet:
|
||||
disable:
|
||||
- loopclosure
|
||||
gosec:
|
||||
excludes:
|
||||
# Implicit memory aliasing of items from a range statement (irrelevant as of Go v1.22)
|
||||
- G601
|
||||
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- coderd/database/dbmem
|
||||
- node_modules
|
||||
- .git
|
||||
|
||||
exclude-files:
|
||||
- scripts/rules.go
|
||||
|
||||
# Rules listed here: https://github.com/securego/gosec#available-rules
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
@@ -195,20 +209,20 @@ issues:
|
||||
- errcheck
|
||||
- forcetypeassert
|
||||
- exhaustruct # This is unhelpful in tests.
|
||||
- revive # TODO(JonA): disabling in order to update golangci-lint
|
||||
- gosec # TODO(JonA): disabling in order to update golangci-lint
|
||||
- path: scripts/*
|
||||
linters:
|
||||
- exhaustruct
|
||||
- path: scripts/rules.go
|
||||
linters:
|
||||
- ALL
|
||||
|
||||
fix: true
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- node_modules
|
||||
- .git
|
||||
skip-files:
|
||||
- scripts/rules.go
|
||||
timeout: 10m
|
||||
|
||||
# Over time, add more and more linters from
|
||||
@@ -224,6 +238,7 @@ linters:
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustruct
|
||||
- exportloopref
|
||||
- forcetypeassert
|
||||
- gocritic
|
||||
# gocyclo is may be useful in the future when we start caring
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Example markdownlint configuration with all properties set to their default value
|
||||
{
|
||||
"MD010": { "spaces_per_tab": 4}, // No hard tabs: we use 4 spaces per tab
|
||||
|
||||
"MD013": false, // Line length: we are not following a strict line lnegth in markdown files
|
||||
|
||||
"MD024": { "siblings_only": true }, // Multiple headings with the same content:
|
||||
|
||||
"MD033": false, // Inline HTML: we use it in some places
|
||||
|
||||
"MD034": false, // Bare URL: we use it in some places in generated docs e.g.
|
||||
// codersdk/deployment.go L597, L1177, L2287, L2495, L2533
|
||||
// codersdk/workspaceproxy.go L196, L200-L201
|
||||
// coderd/tracing/exporter.go L26
|
||||
// cli/exp_scaletest.go L-9
|
||||
|
||||
"MD041": false, // First line in file should be a top level heading: All of our changelogs do not start with a top level heading
|
||||
// TODO: We need to update /home/coder/repos/coder/coder/scripts/release/generate_release_notes.sh to generate changelogs that follow this rule
|
||||
|
||||
"MD052": false, // Image reference: Not a valid reference in generated docs
|
||||
// docs/reference/cli/server.md L628
|
||||
|
||||
"MD055": false, // Table pipe style: Some of the generated tables do not have ending pipes
|
||||
// docs/reference/api/schema.md
|
||||
// docs/reference/api/templates.md
|
||||
// docs/reference/cli/server.md
|
||||
|
||||
"MD056": false // Table column count: Some of the auto-generated tables have issues. TODO: This is probably because of splitting cell content to multiple lines.
|
||||
// docs/reference/api/schema.md
|
||||
// docs/reference/api/templates.md
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# Code generated by Makefile (.gitignore .prettierignore.include). DO NOT EDIT.
|
||||
|
||||
# .gitignore:
|
||||
# Common ignore patterns, these rules applies in both root and subdirectories.
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
.gitpod.yml
|
||||
.idea
|
||||
**/*.swp
|
||||
gotests.coverage
|
||||
gotests.xml
|
||||
gotests_stats.json
|
||||
gotests.json
|
||||
node_modules/
|
||||
vendor/
|
||||
yarn-error.log
|
||||
|
||||
# VSCode settings.
|
||||
**/.vscode/*
|
||||
# Allow VSCode recommendations and default settings in project root.
|
||||
!/.vscode/extensions.json
|
||||
!/.vscode/settings.json
|
||||
|
||||
# Front-end ignore patterns.
|
||||
.next/
|
||||
site/build-storybook.log
|
||||
site/coverage/
|
||||
site/storybook-static/
|
||||
site/test-results/*
|
||||
site/e2e/test-results/*
|
||||
site/e2e/states/*.json
|
||||
site/e2e/.auth.json
|
||||
site/playwright-report/*
|
||||
site/.swc
|
||||
|
||||
# Make target for updating golden files (any dir).
|
||||
.gen-golden
|
||||
|
||||
# Build
|
||||
build/
|
||||
dist/
|
||||
out/
|
||||
|
||||
# Bundle analysis
|
||||
site/stats/
|
||||
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfplan
|
||||
*.lock.hcl
|
||||
.terraform/
|
||||
|
||||
**/.coderv2/*
|
||||
**/__debug_bin
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
*.test
|
||||
|
||||
# Loadtesting
|
||||
./scaletest/terraform/.terraform
|
||||
./scaletest/terraform/.terraform.lock.hcl
|
||||
scaletest/terraform/secrets.tfvars
|
||||
.terraform.tfstate.*
|
||||
|
||||
# Nix
|
||||
result
|
||||
|
||||
# Data dumps from unit tests
|
||||
**/*.test.sql
|
||||
|
||||
# Filebrowser.db
|
||||
**/filebrowser.db
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
# .prettierignore.include:
|
||||
# Helm templates contain variables that are invalid YAML and can't be formatted
|
||||
# by Prettier.
|
||||
helm/**/templates/*.yaml
|
||||
|
||||
# Terraform state files used in tests, these are automatically generated.
|
||||
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
|
||||
**/testdata/**/*.tf*.json
|
||||
|
||||
# Testdata shouldn't be formatted.
|
||||
scripts/apitypings/testdata/**/*.ts
|
||||
enterprise/tailnet/testdata/*.golden.html
|
||||
tailnet/testdata/*.golden.html
|
||||
|
||||
# Generated files shouldn't be formatted.
|
||||
site/e2e/provisionerGenerated.ts
|
||||
|
||||
**/pnpm-lock.yaml
|
||||
|
||||
# Ignore generated JSON (e.g. examples/examples.gen.json).
|
||||
**/*.gen.json
|
||||
@@ -0,0 +1,20 @@
|
||||
# Helm templates contain variables that are invalid YAML and can't be formatted
|
||||
# by Prettier.
|
||||
helm/**/templates/*.yaml
|
||||
|
||||
# Terraform state files used in tests, these are automatically generated.
|
||||
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
|
||||
**/testdata/**/*.tf*.json
|
||||
|
||||
# Testdata shouldn't be formatted.
|
||||
scripts/apitypings/testdata/**/*.ts
|
||||
enterprise/tailnet/testdata/*.golden.html
|
||||
tailnet/testdata/*.golden.html
|
||||
|
||||
# Generated files shouldn't be formatted.
|
||||
site/e2e/provisionerGenerated.ts
|
||||
|
||||
**/pnpm-lock.yaml
|
||||
|
||||
# Ignore generated JSON (e.g. examples/examples.gen.json).
|
||||
**/*.gen.json
|
||||
+3
-3
@@ -4,13 +4,13 @@
|
||||
printWidth: 80
|
||||
proseWrap: always
|
||||
trailingComma: all
|
||||
useTabs: true
|
||||
useTabs: false
|
||||
tabWidth: 2
|
||||
overrides:
|
||||
- files:
|
||||
- README.md
|
||||
- docs/reference/api/**/*.md
|
||||
- docs/reference/cli/**/*.md
|
||||
- docs/api/**/*.md
|
||||
- docs/cli/**/*.md
|
||||
- docs/changelogs/*.md
|
||||
- .github/**/*.{yaml,yml,toml}
|
||||
- scripts/**/*.{yaml,yml,toml}
|
||||
|
||||
Vendored
+13
-14
@@ -1,16 +1,15 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"biomejs.biome",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"emeraldwalk.runonsave",
|
||||
"foxundermoon.shell-format",
|
||||
"github.vscode-codeql",
|
||||
"golang.go",
|
||||
"hashicorp.terraform",
|
||||
"redhat.vscode-yaml",
|
||||
"tekumara.typos-vscode",
|
||||
"zxh404.vscode-proto3"
|
||||
]
|
||||
"recommendations": [
|
||||
"github.vscode-codeql",
|
||||
"golang.go",
|
||||
"hashicorp.terraform",
|
||||
"esbenp.prettier-vscode",
|
||||
"foxundermoon.shell-format",
|
||||
"emeraldwalk.runonsave",
|
||||
"zxh404.vscode-proto3",
|
||||
"redhat.vscode-yaml",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
-45
@@ -1,45 +0,0 @@
|
||||
{
|
||||
// For info about snippets, visit https://code.visualstudio.com/docs/editor/userdefinedsnippets
|
||||
// https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
|
||||
|
||||
"alert": {
|
||||
"prefix": "#alert",
|
||||
"body": [
|
||||
"> [!${1|CAUTION,IMPORTANT,NOTE,TIP,WARNING|}]",
|
||||
"> ${TM_SELECTED_TEXT:${2:add info here}}\n"
|
||||
],
|
||||
"description": "callout admonition caution important note tip warning"
|
||||
},
|
||||
"fenced code block": {
|
||||
"prefix": "#codeblock",
|
||||
"body": ["```${1|apache,bash,console,diff,Dockerfile,env,go,hcl,ini,json,lisp,md,powershell,shell,sql,text,tf,tsx,yaml|}", "${TM_SELECTED_TEXT}$0", "```"],
|
||||
"description": "fenced code block"
|
||||
},
|
||||
"image": {
|
||||
"prefix": "#image",
|
||||
"body": "$0",
|
||||
"description": "image"
|
||||
},
|
||||
"premium-feature": {
|
||||
"prefix": "#premium-feature",
|
||||
"body": [
|
||||
"> [!NOTE]\n",
|
||||
"> ${1:feature} ${2|is,are|} an Enterprise and Premium feature. [Learn more](https://coder.com/pricing#compare-plans).\n"
|
||||
]
|
||||
},
|
||||
"tabs": {
|
||||
"prefix": "#tabs",
|
||||
"body": [
|
||||
"<div class=\"tabs\">\n",
|
||||
"${1:optional description}\n",
|
||||
"## ${2:tab title}\n",
|
||||
"${TM_SELECTED_TEXT:${3:first tab content}}\n",
|
||||
"## ${4:tab title}\n",
|
||||
"${5:second tab content}\n",
|
||||
"## ${6:tab title}\n",
|
||||
"${7:third tab content}\n",
|
||||
"</div>\n"
|
||||
],
|
||||
"description": "tabs"
|
||||
}
|
||||
}
|
||||
Vendored
+225
-59
@@ -1,61 +1,227 @@
|
||||
{
|
||||
"emeraldwalk.runonsave": {
|
||||
"commands": [
|
||||
{
|
||||
"match": "database/queries/*.sql",
|
||||
"cmd": "make gen"
|
||||
},
|
||||
{
|
||||
"match": "provisionerd/proto/provisionerd.proto",
|
||||
"cmd": "make provisionerd/proto/provisionerd.pb.go"
|
||||
}
|
||||
]
|
||||
},
|
||||
"search.exclude": {
|
||||
"**.pb.go": true,
|
||||
"**/*.gen.json": true,
|
||||
"**/testdata/*": true,
|
||||
"coderd/apidoc/**": true,
|
||||
"docs/reference/api/*.md": true,
|
||||
"docs/reference/cli/*.md": true,
|
||||
"docs/templates/*.md": true,
|
||||
"LICENSE": true,
|
||||
"scripts/metricsdocgen/metrics": true,
|
||||
"site/out/**": true,
|
||||
"site/storybook-static/**": true,
|
||||
"**.map": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
// Ensure files always have a newline.
|
||||
"files.insertFinalNewline": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fast"],
|
||||
"go.coverageDecorator": {
|
||||
"type": "gutter",
|
||||
"coveredGutterStyle": "blockgreen",
|
||||
"uncoveredGutterStyle": "blockred"
|
||||
},
|
||||
// The codersdk is used by coderd another other packages extensively.
|
||||
// To reduce redundancy in tests, it's covered by other packages.
|
||||
// Since package coverage pairing can't be defined, all packages cover
|
||||
// all other packages.
|
||||
"go.testFlags": ["-short", "-coverpkg=./..."],
|
||||
// We often use a version of TypeScript that's ahead of the version shipped
|
||||
// with VS Code.
|
||||
"typescript.tsdk": "./site/node_modules/typescript/lib",
|
||||
// Playwright tests in VSCode will open a browser to live "view" the test.
|
||||
"playwright.reuseBrowser": true,
|
||||
|
||||
"[javascript][javascriptreact][json][jsonc][typescript][typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"quickfix.biome": "explicit"
|
||||
// "source.organizeImports.biome": "explicit"
|
||||
}
|
||||
},
|
||||
|
||||
"[css][html][markdown][yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"typos.config": ".github/workflows/typos.toml"
|
||||
"cSpell.words": [
|
||||
"afero",
|
||||
"agentsdk",
|
||||
"apps",
|
||||
"ASKPASS",
|
||||
"authcheck",
|
||||
"autostop",
|
||||
"awsidentity",
|
||||
"bodyclose",
|
||||
"buildinfo",
|
||||
"buildname",
|
||||
"circbuf",
|
||||
"cliflag",
|
||||
"cliui",
|
||||
"codecov",
|
||||
"coderd",
|
||||
"coderdenttest",
|
||||
"coderdtest",
|
||||
"codersdk",
|
||||
"contravariance",
|
||||
"cronstrue",
|
||||
"databasefake",
|
||||
"dbgen",
|
||||
"dbmem",
|
||||
"dbtype",
|
||||
"DERP",
|
||||
"derphttp",
|
||||
"derpmap",
|
||||
"devel",
|
||||
"devtunnel",
|
||||
"dflags",
|
||||
"drpc",
|
||||
"drpcconn",
|
||||
"drpcmux",
|
||||
"drpcserver",
|
||||
"Dsts",
|
||||
"embeddedpostgres",
|
||||
"enablements",
|
||||
"enterprisemeta",
|
||||
"errgroup",
|
||||
"eventsourcemock",
|
||||
"externalauth",
|
||||
"Failf",
|
||||
"fatih",
|
||||
"Formik",
|
||||
"gitauth",
|
||||
"gitsshkey",
|
||||
"goarch",
|
||||
"gographviz",
|
||||
"goleak",
|
||||
"gonet",
|
||||
"gossh",
|
||||
"gsyslog",
|
||||
"GTTY",
|
||||
"hashicorp",
|
||||
"hclsyntax",
|
||||
"httpapi",
|
||||
"httpmw",
|
||||
"idtoken",
|
||||
"Iflag",
|
||||
"incpatch",
|
||||
"initialisms",
|
||||
"ipnstate",
|
||||
"isatty",
|
||||
"Jobf",
|
||||
"Keygen",
|
||||
"kirsle",
|
||||
"Kubernetes",
|
||||
"ldflags",
|
||||
"magicsock",
|
||||
"manifoldco",
|
||||
"mapstructure",
|
||||
"mattn",
|
||||
"mitchellh",
|
||||
"moby",
|
||||
"namesgenerator",
|
||||
"namespacing",
|
||||
"netaddr",
|
||||
"netip",
|
||||
"netmap",
|
||||
"netns",
|
||||
"netstack",
|
||||
"nettype",
|
||||
"nfpms",
|
||||
"nhooyr",
|
||||
"nmcfg",
|
||||
"nolint",
|
||||
"nosec",
|
||||
"ntqry",
|
||||
"OIDC",
|
||||
"oneof",
|
||||
"opty",
|
||||
"paralleltest",
|
||||
"parameterscopeid",
|
||||
"pqtype",
|
||||
"prometheusmetrics",
|
||||
"promhttp",
|
||||
"protobuf",
|
||||
"provisionerd",
|
||||
"provisionerdserver",
|
||||
"provisionersdk",
|
||||
"ptty",
|
||||
"ptys",
|
||||
"ptytest",
|
||||
"quickstart",
|
||||
"reconfig",
|
||||
"replicasync",
|
||||
"retrier",
|
||||
"rpty",
|
||||
"SCIM",
|
||||
"sdkproto",
|
||||
"sdktrace",
|
||||
"Signup",
|
||||
"slogtest",
|
||||
"sourcemapped",
|
||||
"spinbutton",
|
||||
"Srcs",
|
||||
"stdbuf",
|
||||
"stretchr",
|
||||
"STTY",
|
||||
"stuntest",
|
||||
"tailbroker",
|
||||
"tailcfg",
|
||||
"tailexchange",
|
||||
"tailnet",
|
||||
"tailnettest",
|
||||
"Tailscale",
|
||||
"tanstack",
|
||||
"tbody",
|
||||
"TCGETS",
|
||||
"tcpip",
|
||||
"TCSETS",
|
||||
"templateversions",
|
||||
"testdata",
|
||||
"testid",
|
||||
"testutil",
|
||||
"tfexec",
|
||||
"tfjson",
|
||||
"tfplan",
|
||||
"tfstate",
|
||||
"thead",
|
||||
"tios",
|
||||
"tmpdir",
|
||||
"tokenconfig",
|
||||
"Topbar",
|
||||
"tparallel",
|
||||
"trialer",
|
||||
"trimprefix",
|
||||
"tsdial",
|
||||
"tslogger",
|
||||
"tstun",
|
||||
"turnconn",
|
||||
"typegen",
|
||||
"typesafe",
|
||||
"unconvert",
|
||||
"Untar",
|
||||
"Userspace",
|
||||
"VMID",
|
||||
"walkthrough",
|
||||
"weblinks",
|
||||
"webrtc",
|
||||
"wgcfg",
|
||||
"wgconfig",
|
||||
"wgengine",
|
||||
"wgmonitor",
|
||||
"wgnet",
|
||||
"workspaceagent",
|
||||
"workspaceagents",
|
||||
"workspaceapp",
|
||||
"workspaceapps",
|
||||
"workspacebuilds",
|
||||
"workspacename",
|
||||
"wsjson",
|
||||
"xerrors",
|
||||
"xlarge",
|
||||
"xsmall",
|
||||
"yamux"
|
||||
],
|
||||
"cSpell.ignorePaths": ["site/package.json", ".vscode/settings.json"],
|
||||
"emeraldwalk.runonsave": {
|
||||
"commands": [
|
||||
{
|
||||
"match": "database/queries/*.sql",
|
||||
"cmd": "make gen"
|
||||
},
|
||||
{
|
||||
"match": "provisionerd/proto/provisionerd.proto",
|
||||
"cmd": "make provisionerd/proto/provisionerd.pb.go"
|
||||
}
|
||||
]
|
||||
},
|
||||
"eslint.workingDirectories": ["./site"],
|
||||
"search.exclude": {
|
||||
"**.pb.go": true,
|
||||
"**/*.gen.json": true,
|
||||
"**/testdata/*": true,
|
||||
"coderd/apidoc/**": true,
|
||||
"docs/api/*.md": true,
|
||||
"docs/templates/*.md": true,
|
||||
"LICENSE": true,
|
||||
"scripts/metricsdocgen/metrics": true,
|
||||
"site/out/**": true,
|
||||
"site/storybook-static/**": true,
|
||||
"**.map": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
// Ensure files always have a newline.
|
||||
"files.insertFinalNewline": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fast"],
|
||||
"go.coverageDecorator": {
|
||||
"type": "gutter",
|
||||
"coveredGutterStyle": "blockgreen",
|
||||
"uncoveredGutterStyle": "blockred"
|
||||
},
|
||||
// The codersdk is used by coderd another other packages extensively.
|
||||
// To reduce redundancy in tests, it's covered by other packages.
|
||||
// Since package coverage pairing can't be defined, all packages cover
|
||||
// all other packages.
|
||||
"go.testFlags": ["-short", "-coverpkg=./..."],
|
||||
// We often use a version of TypeScript that's ahead of the version shipped
|
||||
// with VS Code.
|
||||
"typescript.tsdk": "./site/node_modules/typescript/lib",
|
||||
// Playwright tests in VSCode will open a browser to live "view" the test.
|
||||
"playwright.reuseBrowser": true
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# These APIs are versioned, so any changes need to be carefully reviewed for whether
|
||||
# to bump API major or minor versions.
|
||||
agent/proto/ @spikecurtis @johnstcn
|
||||
tailnet/proto/ @spikecurtis @johnstcn
|
||||
vpn/vpn.proto @spikecurtis @johnstcn
|
||||
vpn/version.go @spikecurtis @johnstcn
|
||||
@@ -1,2 +0,0 @@
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
[https://coder.com/docs/contributing/CODE_OF_CONDUCT](https://coder.com/docs/contributing/CODE_OF_CONDUCT)
|
||||
@@ -1,2 +0,0 @@
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
[https://coder.com/docs/CONTRIBUTING](https://coder.com/docs/CONTRIBUTING)
|
||||
@@ -54,16 +54,6 @@ FIND_EXCLUSIONS= \
|
||||
-not \( \( -path '*/.git/*' -o -path './build/*' -o -path './vendor/*' -o -path './.coderv2/*' -o -path '*/node_modules/*' -o -path '*/out/*' -o -path './coderd/apidoc/*' -o -path '*/.next/*' -o -path '*/.terraform/*' \) -prune \)
|
||||
# Source files used for make targets, evaluated on use.
|
||||
GO_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go' -not -name '*_test.go')
|
||||
# Same as GO_SRC_FILES but excluding certain files that have problematic
|
||||
# Makefile dependencies (e.g. pnpm).
|
||||
MOST_GO_SRC_FILES := $(shell \
|
||||
find . \
|
||||
$(FIND_EXCLUSIONS) \
|
||||
-type f \
|
||||
-name '*.go' \
|
||||
-not -name '*_test.go' \
|
||||
-not -wholename './agent/agentcontainers/dcspec/dcspec_gen.go' \
|
||||
)
|
||||
# All the shell files in the repo, excluding ignored files.
|
||||
SHELL_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.sh')
|
||||
|
||||
@@ -89,12 +79,8 @@ PACKAGE_OS_ARCHES := linux_amd64 linux_armv7 linux_arm64
|
||||
# All architectures we build Docker images for (Linux only).
|
||||
DOCKER_ARCHES := amd64 arm64 armv7
|
||||
|
||||
# All ${OS}_${ARCH} combos we build the desktop dylib for.
|
||||
DYLIB_ARCHES := darwin_amd64 darwin_arm64
|
||||
|
||||
# Computed variables based on the above.
|
||||
CODER_SLIM_BINARIES := $(addprefix build/coder-slim_$(VERSION)_,$(OS_ARCHES))
|
||||
CODER_DYLIBS := $(foreach os_arch, $(DYLIB_ARCHES), build/coder-vpn_$(VERSION)_$(os_arch).dylib)
|
||||
CODER_FAT_BINARIES := $(addprefix build/coder_$(VERSION)_,$(OS_ARCHES))
|
||||
CODER_ALL_BINARIES := $(CODER_SLIM_BINARIES) $(CODER_FAT_BINARIES)
|
||||
CODER_TAR_GZ_ARCHIVES := $(foreach os_arch, $(ARCHIVE_TAR_GZ), build/coder_$(VERSION)_$(os_arch).tar.gz)
|
||||
@@ -126,7 +112,7 @@ endif
|
||||
|
||||
clean:
|
||||
rm -rf build/ site/build/ site/out/
|
||||
mkdir -p build/
|
||||
mkdir -p build/ site/out/bin/
|
||||
git restore site/out/
|
||||
.PHONY: clean
|
||||
|
||||
@@ -252,26 +238,6 @@ $(CODER_ALL_BINARIES): go.mod go.sum \
|
||||
cp "$@" "./site/out/bin/coder-$$os-$$arch$$dot_ext"
|
||||
fi
|
||||
|
||||
# This task builds Coder Desktop dylibs
|
||||
$(CODER_DYLIBS): go.mod go.sum $(MOST_GO_SRC_FILES)
|
||||
@if [ "$(shell uname)" = "Darwin" ]; then
|
||||
$(get-mode-os-arch-ext)
|
||||
./scripts/build_go.sh \
|
||||
--os "$$os" \
|
||||
--arch "$$arch" \
|
||||
--version "$(VERSION)" \
|
||||
--output "$@" \
|
||||
--dylib
|
||||
|
||||
else
|
||||
echo "ERROR: Can't build dylib on non-Darwin OS" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# This task builds both dylibs
|
||||
build/coder-dylib: $(CODER_DYLIBS)
|
||||
.PHONY: build/coder-dylib
|
||||
|
||||
# This task builds all archives. It parses the target name to get the metadata
|
||||
# for the build, so it must be specified in this format:
|
||||
# build/coder_${version}_${os}_${arch}.${format}
|
||||
@@ -398,40 +364,15 @@ $(foreach chart,$(charts),build/$(chart)_helm_$(VERSION).tgz): build/%_helm_$(VE
|
||||
--chart $* \
|
||||
--output "$@"
|
||||
|
||||
node_modules/.installed: package.json pnpm-lock.yaml
|
||||
./scripts/pnpm_install.sh
|
||||
touch "$@"
|
||||
|
||||
offlinedocs/node_modules/.installed: offlinedocs/package.json offlinedocs/pnpm-lock.yaml
|
||||
(cd offlinedocs/ && ../scripts/pnpm_install.sh)
|
||||
touch "$@"
|
||||
|
||||
site/node_modules/.installed: site/package.json site/pnpm-lock.yaml
|
||||
(cd site/ && ../scripts/pnpm_install.sh)
|
||||
touch "$@"
|
||||
|
||||
scripts/apidocgen/node_modules/.installed: scripts/apidocgen/package.json scripts/apidocgen/pnpm-lock.yaml
|
||||
(cd scripts/apidocgen && ../../scripts/pnpm_install.sh)
|
||||
touch "$@"
|
||||
|
||||
SITE_GEN_FILES := \
|
||||
site/src/api/typesGenerated.ts \
|
||||
site/src/api/rbacresourcesGenerated.ts \
|
||||
site/src/api/countriesGenerated.ts \
|
||||
site/src/theme/icons.json
|
||||
|
||||
site/out/index.html: \
|
||||
site/node_modules/.installed \
|
||||
site/static/install.sh \
|
||||
$(SITE_GEN_FILES) \
|
||||
$(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
|
||||
cd site/
|
||||
site/out/index.html: site/package.json $(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
|
||||
cd site
|
||||
# prevents this directory from getting to big, and causing "too much data" errors
|
||||
rm -rf out/assets/
|
||||
../scripts/pnpm_install.sh
|
||||
pnpm build
|
||||
|
||||
offlinedocs/out/index.html: offlinedocs/node_modules/.installed $(shell find ./offlinedocs $(FIND_EXCLUSIONS) -type f) $(shell find ./docs $(FIND_EXCLUSIONS) -type f | sed 's: :\\ :g')
|
||||
cd offlinedocs/
|
||||
offlinedocs/out/index.html: $(shell find ./offlinedocs $(FIND_EXCLUSIONS) -type f) $(shell find ./docs $(FIND_EXCLUSIONS) -type f | sed 's: :\\ :g')
|
||||
cd offlinedocs
|
||||
../scripts/pnpm_install.sh
|
||||
pnpm export
|
||||
|
||||
@@ -450,40 +391,32 @@ BOLD := $(shell tput bold 2>/dev/null)
|
||||
GREEN := $(shell tput setaf 2 2>/dev/null)
|
||||
RESET := $(shell tput sgr0 2>/dev/null)
|
||||
|
||||
fmt: fmt/ts fmt/go fmt/terraform fmt/shfmt fmt/biome fmt/markdown
|
||||
fmt: fmt/eslint fmt/prettier fmt/terraform fmt/shfmt fmt/go
|
||||
.PHONY: fmt
|
||||
|
||||
fmt/go:
|
||||
go mod tidy
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/go$(RESET)"
|
||||
# VS Code users should check out
|
||||
# https://github.com/mvdan/gofumpt#visual-studio-code
|
||||
find . $(FIND_EXCLUSIONS) -type f -name '*.go' -print0 | \
|
||||
xargs -0 grep --null -L "DO NOT EDIT" | \
|
||||
xargs -0 go run mvdan.cc/gofumpt@v0.4.0 -w -l
|
||||
go run mvdan.cc/gofumpt@v0.4.0 -w -l .
|
||||
.PHONY: fmt/go
|
||||
|
||||
fmt/ts: site/node_modules/.installed
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/ts$(RESET)"
|
||||
fmt/eslint:
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/eslint$(RESET)"
|
||||
cd site
|
||||
# Avoid writing files in CI to reduce file write activity
|
||||
ifdef CI
|
||||
pnpm run check --linter-enabled=false
|
||||
else
|
||||
pnpm run check:fix
|
||||
endif
|
||||
.PHONY: fmt/ts
|
||||
pnpm run lint:fix
|
||||
.PHONY: fmt/eslint
|
||||
|
||||
fmt/biome: site/node_modules/.installed
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/biome$(RESET)"
|
||||
cd site/
|
||||
fmt/prettier:
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/prettier$(RESET)"
|
||||
cd site
|
||||
# Avoid writing files in CI to reduce file write activity
|
||||
ifdef CI
|
||||
pnpm run format:check
|
||||
else
|
||||
pnpm run format
|
||||
endif
|
||||
.PHONY: fmt/biome
|
||||
.PHONY: fmt/prettier
|
||||
|
||||
fmt/terraform: $(wildcard *.tf)
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/terraform$(RESET)"
|
||||
@@ -500,27 +433,21 @@ else
|
||||
endif
|
||||
.PHONY: fmt/shfmt
|
||||
|
||||
fmt/markdown: node_modules/.installed
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/markdown$(RESET)"
|
||||
pnpm format-docs
|
||||
.PHONY: fmt/markdown
|
||||
|
||||
lint: lint/shellcheck lint/go lint/ts lint/examples lint/helm lint/site-icons lint/markdown
|
||||
lint: lint/shellcheck lint/go lint/ts lint/examples lint/helm lint/site-icons
|
||||
.PHONY: lint
|
||||
|
||||
lint/site-icons:
|
||||
./scripts/check_site_icons.sh
|
||||
.PHONY: lint/site-icons
|
||||
|
||||
lint/ts: site/node_modules/.installed
|
||||
cd site/
|
||||
pnpm lint
|
||||
lint/ts:
|
||||
cd site
|
||||
pnpm i && pnpm lint
|
||||
.PHONY: lint/ts
|
||||
|
||||
lint/go:
|
||||
./scripts/check_enterprise_imports.sh
|
||||
./scripts/check_codersdk_imports.sh
|
||||
linter_ver=$(shell egrep -o 'GOLANGCI_LINT_VERSION=\S+' dogfood/coder/Dockerfile | cut -d '=' -f 2)
|
||||
linter_ver=$(shell egrep -o 'GOLANGCI_LINT_VERSION=\S+' dogfood/Dockerfile | cut -d '=' -f 2)
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v$$linter_ver run
|
||||
.PHONY: lint/go
|
||||
|
||||
@@ -535,18 +462,13 @@ lint/shellcheck: $(SHELL_SRC_FILES)
|
||||
.PHONY: lint/shellcheck
|
||||
|
||||
lint/helm:
|
||||
cd helm/
|
||||
cd helm
|
||||
make lint
|
||||
.PHONY: lint/helm
|
||||
|
||||
lint/markdown: node_modules/.installed
|
||||
pnpm lint-docs
|
||||
.PHONY: lint/markdown
|
||||
|
||||
# All files generated by the database should be added here, and this can be used
|
||||
# as a target for jobs that need to run after the database is generated.
|
||||
DB_GEN_FILES := \
|
||||
coderd/database/dump.sql \
|
||||
coderd/database/querier.go \
|
||||
coderd/database/unique_constraint.go \
|
||||
coderd/database/dbmem/dbmem.go \
|
||||
@@ -554,55 +476,36 @@ DB_GEN_FILES := \
|
||||
coderd/database/dbauthz/dbauthz.go \
|
||||
coderd/database/dbmock/dbmock.go
|
||||
|
||||
TAILNETTEST_MOCKS := \
|
||||
tailnet/tailnettest/coordinatormock.go \
|
||||
tailnet/tailnettest/coordinateemock.go \
|
||||
tailnet/tailnettest/workspaceupdatesprovidermock.go \
|
||||
tailnet/tailnettest/subscriptionmock.go
|
||||
|
||||
GEN_FILES := \
|
||||
# all gen targets should be added here and to gen/mark-fresh
|
||||
gen: \
|
||||
tailnet/proto/tailnet.pb.go \
|
||||
agent/proto/agent.pb.go \
|
||||
provisionersdk/proto/provisioner.pb.go \
|
||||
provisionerd/proto/provisionerd.pb.go \
|
||||
vpn/vpn.pb.go \
|
||||
coderd/database/dump.sql \
|
||||
$(DB_GEN_FILES) \
|
||||
$(SITE_GEN_FILES) \
|
||||
site/src/api/typesGenerated.ts \
|
||||
coderd/rbac/object_gen.go \
|
||||
codersdk/rbacresources_gen.go \
|
||||
docs/admin/integrations/prometheus.md \
|
||||
docs/reference/cli/index.md \
|
||||
docs/admin/security/audit-logs.md \
|
||||
site/src/api/rbacresources_gen.ts \
|
||||
docs/admin/prometheus.md \
|
||||
docs/cli.md \
|
||||
docs/admin/audit-logs.md \
|
||||
coderd/apidoc/swagger.json \
|
||||
docs/manifest.json \
|
||||
.prettierignore.include \
|
||||
.prettierignore \
|
||||
provisioner/terraform/testdata/version \
|
||||
site/.prettierrc.yaml \
|
||||
site/.prettierignore \
|
||||
site/.eslintignore \
|
||||
site/e2e/provisionerGenerated.ts \
|
||||
site/src/theme/icons.json \
|
||||
examples/examples.gen.json \
|
||||
$(TAILNETTEST_MOCKS) \
|
||||
coderd/database/pubsub/psmock/psmock.go \
|
||||
agent/agentcontainers/acmock/acmock.go \
|
||||
agent/agentcontainers/dcspec/dcspec_gen.go \
|
||||
coderd/httpmw/loggermw/loggermock/loggermock.go
|
||||
|
||||
# all gen targets should be added here and to gen/mark-fresh
|
||||
gen: gen/db gen/golden-files $(GEN_FILES)
|
||||
tailnet/tailnettest/coordinatormock.go \
|
||||
tailnet/tailnettest/coordinateemock.go \
|
||||
tailnet/tailnettest/multiagentmock.go
|
||||
.PHONY: gen
|
||||
|
||||
gen/db: $(DB_GEN_FILES)
|
||||
.PHONY: gen/db
|
||||
|
||||
gen/golden-files: \
|
||||
cli/testdata/.gen-golden \
|
||||
coderd/.gen-golden \
|
||||
coderd/notifications/.gen-golden \
|
||||
enterprise/cli/testdata/.gen-golden \
|
||||
enterprise/tailnet/testdata/.gen-golden \
|
||||
helm/coder/tests/testdata/.gen-golden \
|
||||
helm/provisioner/tests/testdata/.gen-golden \
|
||||
provisioner/terraform/testdata/.gen-golden \
|
||||
tailnet/testdata/.gen-golden
|
||||
.PHONY: gen/golden-files
|
||||
|
||||
# Mark all generated files as fresh so make thinks they're up-to-date. This is
|
||||
# used during releases so we don't run generation scripts.
|
||||
gen/mark-fresh:
|
||||
@@ -611,29 +514,28 @@ gen/mark-fresh:
|
||||
agent/proto/agent.pb.go \
|
||||
provisionersdk/proto/provisioner.pb.go \
|
||||
provisionerd/proto/provisionerd.pb.go \
|
||||
vpn/vpn.pb.go \
|
||||
coderd/database/dump.sql \
|
||||
$(DB_GEN_FILES) \
|
||||
site/src/api/typesGenerated.ts \
|
||||
coderd/rbac/object_gen.go \
|
||||
codersdk/rbacresources_gen.go \
|
||||
site/src/api/rbacresourcesGenerated.ts \
|
||||
site/src/api/countriesGenerated.ts \
|
||||
docs/admin/integrations/prometheus.md \
|
||||
docs/reference/cli/index.md \
|
||||
docs/admin/security/audit-logs.md \
|
||||
site/src/api/rbacresources_gen.ts \
|
||||
docs/admin/prometheus.md \
|
||||
docs/cli.md \
|
||||
docs/admin/audit-logs.md \
|
||||
coderd/apidoc/swagger.json \
|
||||
docs/manifest.json \
|
||||
.prettierignore.include \
|
||||
.prettierignore \
|
||||
site/.prettierrc.yaml \
|
||||
site/.prettierignore \
|
||||
site/.eslintignore \
|
||||
site/e2e/provisionerGenerated.ts \
|
||||
site/src/theme/icons.json \
|
||||
examples/examples.gen.json \
|
||||
$(TAILNETTEST_MOCKS) \
|
||||
coderd/database/pubsub/psmock/psmock.go \
|
||||
agent/agentcontainers/acmock/acmock.go \
|
||||
agent/agentcontainers/dcspec/dcspec_gen.go \
|
||||
coderd/httpmw/loggermw/loggermock/loggermock.go \
|
||||
"
|
||||
|
||||
tailnet/tailnettest/coordinatormock.go \
|
||||
tailnet/tailnettest/coordinateemock.go \
|
||||
tailnet/tailnettest/multiagentmock.go \
|
||||
"
|
||||
for file in $$files; do
|
||||
echo "$$file"
|
||||
if [ ! -f "$$file" ]; then
|
||||
@@ -642,7 +544,7 @@ gen/mark-fresh:
|
||||
fi
|
||||
|
||||
# touch sets the mtime of the file to the current time
|
||||
touch "$$file"
|
||||
touch $$file
|
||||
done
|
||||
.PHONY: gen/mark-fresh
|
||||
|
||||
@@ -650,42 +552,21 @@ gen/mark-fresh:
|
||||
# applied.
|
||||
coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/database/migrations/*.sql)
|
||||
go run ./coderd/database/gen/dump/main.go
|
||||
touch "$@"
|
||||
|
||||
# Generates Go code for querying the database.
|
||||
# coderd/database/queries.sql.go
|
||||
# coderd/database/models.go
|
||||
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
|
||||
./coderd/database/generate.sh
|
||||
touch "$@"
|
||||
|
||||
coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier.go
|
||||
go generate ./coderd/database/dbmock/
|
||||
touch "$@"
|
||||
|
||||
coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go
|
||||
go generate ./coderd/database/pubsub/psmock
|
||||
touch "$@"
|
||||
|
||||
agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go
|
||||
go generate ./agent/agentcontainers/acmock/
|
||||
touch "$@"
|
||||
|
||||
coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.go
|
||||
go generate ./coderd/httpmw/loggermw/loggermock/
|
||||
touch "$@"
|
||||
|
||||
agent/agentcontainers/dcspec/dcspec_gen.go: \
|
||||
node_modules/.installed \
|
||||
agent/agentcontainers/dcspec/devContainer.base.schema.json \
|
||||
agent/agentcontainers/dcspec/gen.sh \
|
||||
agent/agentcontainers/dcspec/doc.go
|
||||
DCSPEC_QUIET=true go generate ./agent/agentcontainers/dcspec/
|
||||
touch "$@"
|
||||
|
||||
$(TAILNETTEST_MOCKS): tailnet/coordinator.go tailnet/service.go
|
||||
tailnet/tailnettest/coordinatormock.go tailnet/tailnettest/multiagentmock.go tailnet/tailnettest/coordinateemock.go: tailnet/coordinator.go tailnet/multiagent.go
|
||||
go generate ./tailnet/tailnettest/
|
||||
touch "$@"
|
||||
|
||||
tailnet/proto/tailnet.pb.go: tailnet/proto/tailnet.proto
|
||||
protoc \
|
||||
@@ -719,122 +600,68 @@ provisionerd/proto/provisionerd.pb.go: provisionerd/proto/provisionerd.proto
|
||||
--go-drpc_opt=paths=source_relative \
|
||||
./provisionerd/proto/provisionerd.proto
|
||||
|
||||
vpn/vpn.pb.go: vpn/vpn.proto
|
||||
protoc \
|
||||
--go_out=. \
|
||||
--go_opt=paths=source_relative \
|
||||
./vpn/vpn.proto
|
||||
site/src/api/typesGenerated.ts: $(wildcard scripts/apitypings/*) $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go')
|
||||
go run ./scripts/apitypings/ > $@
|
||||
./scripts/pnpm_install.sh
|
||||
pnpm exec prettier --write "$@"
|
||||
|
||||
site/src/api/typesGenerated.ts: site/node_modules/.installed $(wildcard scripts/apitypings/*) $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go')
|
||||
# -C sets the directory for the go run command
|
||||
go run -C ./scripts/apitypings main.go > $@
|
||||
(cd site/ && pnpm exec biome format --write src/api/typesGenerated.ts)
|
||||
touch "$@"
|
||||
site/e2e/provisionerGenerated.ts: provisionerd/proto/provisionerd.pb.go provisionersdk/proto/provisioner.pb.go
|
||||
cd site
|
||||
../scripts/pnpm_install.sh
|
||||
pnpm run gen:provisioner
|
||||
|
||||
site/e2e/provisionerGenerated.ts: site/node_modules/.installed provisionerd/proto/provisionerd.pb.go provisionersdk/proto/provisioner.pb.go
|
||||
(cd site/ && pnpm run gen:provisioner)
|
||||
touch "$@"
|
||||
|
||||
site/src/theme/icons.json: site/node_modules/.installed $(wildcard scripts/gensite/*) $(wildcard site/static/icon/*)
|
||||
site/src/theme/icons.json: $(wildcard scripts/gensite/*) $(wildcard site/static/icon/*)
|
||||
go run ./scripts/gensite/ -icons "$@"
|
||||
(cd site/ && pnpm exec biome format --write src/theme/icons.json)
|
||||
touch "$@"
|
||||
./scripts/pnpm_install.sh
|
||||
pnpm exec prettier --write "$@"
|
||||
|
||||
examples/examples.gen.json: scripts/examplegen/main.go examples/examples.go $(shell find ./examples/templates)
|
||||
go run ./scripts/examplegen/main.go > examples/examples.gen.json
|
||||
touch "$@"
|
||||
|
||||
coderd/rbac/object_gen.go: scripts/typegen/rbacobject.gotmpl scripts/typegen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
|
||||
tempdir=$(shell mktemp -d /tmp/typegen_rbac_object.XXXXXX)
|
||||
go run ./scripts/typegen/main.go rbac object > "$$tempdir/object_gen.go"
|
||||
mv -v "$$tempdir/object_gen.go" coderd/rbac/object_gen.go
|
||||
rmdir -v "$$tempdir"
|
||||
touch "$@"
|
||||
coderd/rbac/object_gen.go: scripts/rbacgen/rbacobject.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
|
||||
go run scripts/rbacgen/main.go rbac > coderd/rbac/object_gen.go
|
||||
|
||||
codersdk/rbacresources_gen.go: scripts/typegen/codersdk.gotmpl scripts/typegen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
|
||||
# Do no overwrite codersdk/rbacresources_gen.go directly, as it would make the file empty, breaking
|
||||
# the `codersdk` package and any parallel build targets.
|
||||
go run scripts/typegen/main.go rbac codersdk > /tmp/rbacresources_gen.go
|
||||
mv /tmp/rbacresources_gen.go codersdk/rbacresources_gen.go
|
||||
touch "$@"
|
||||
codersdk/rbacresources_gen.go: scripts/rbacgen/codersdk.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
|
||||
go run scripts/rbacgen/main.go codersdk > codersdk/rbacresources_gen.go
|
||||
|
||||
site/src/api/rbacresourcesGenerated.ts: site/node_modules/.installed scripts/typegen/codersdk.gotmpl scripts/typegen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
|
||||
go run scripts/typegen/main.go rbac typescript > "$@"
|
||||
(cd site/ && pnpm exec biome format --write src/api/rbacresourcesGenerated.ts)
|
||||
touch "$@"
|
||||
site/src/api/rbacresources_gen.ts: scripts/rbacgen/codersdk.gotmpl scripts/rbacgen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go
|
||||
go run scripts/rbacgen/main.go typescript > site/src/api/rbacresources_gen.ts
|
||||
|
||||
site/src/api/countriesGenerated.ts: site/node_modules/.installed scripts/typegen/countries.tstmpl scripts/typegen/main.go codersdk/countries.go
|
||||
go run scripts/typegen/main.go countries > "$@"
|
||||
(cd site/ && pnpm exec biome format --write src/api/countriesGenerated.ts)
|
||||
touch "$@"
|
||||
|
||||
docs/admin/integrations/prometheus.md: node_modules/.installed scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
|
||||
docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
|
||||
go run scripts/metricsdocgen/main.go
|
||||
pnpm exec markdownlint-cli2 --fix ./docs/admin/integrations/prometheus.md
|
||||
pnpm exec markdown-table-formatter ./docs/admin/integrations/prometheus.md
|
||||
touch "$@"
|
||||
./scripts/pnpm_install.sh
|
||||
pnpm exec prettier --write ./docs/admin/prometheus.md
|
||||
|
||||
docs/reference/cli/index.md: node_modules/.installed scripts/clidocgen/main.go examples/examples.gen.json $(GO_SRC_FILES)
|
||||
docs/cli.md: scripts/clidocgen/main.go examples/examples.gen.json $(GO_SRC_FILES)
|
||||
CI=true BASE_PATH="." go run ./scripts/clidocgen
|
||||
pnpm exec markdownlint-cli2 --fix ./docs/reference/cli/*.md
|
||||
pnpm exec markdown-table-formatter ./docs/reference/cli/*.md
|
||||
touch "$@"
|
||||
./scripts/pnpm_install.sh
|
||||
pnpm exec prettier --write ./docs/cli.md ./docs/cli/*.md ./docs/manifest.json
|
||||
|
||||
docs/admin/security/audit-logs.md: node_modules/.installed coderd/database/querier.go scripts/auditdocgen/main.go enterprise/audit/table.go coderd/rbac/object_gen.go
|
||||
docs/admin/audit-logs.md: coderd/database/querier.go scripts/auditdocgen/main.go enterprise/audit/table.go coderd/rbac/object_gen.go
|
||||
go run scripts/auditdocgen/main.go
|
||||
pnpm exec markdownlint-cli2 --fix ./docs/admin/security/audit-logs.md
|
||||
pnpm exec markdown-table-formatter ./docs/admin/security/audit-logs.md
|
||||
touch "$@"
|
||||
./scripts/pnpm_install.sh
|
||||
pnpm exec prettier --write ./docs/admin/audit-logs.md
|
||||
|
||||
coderd/apidoc/.gen: \
|
||||
node_modules/.installed \
|
||||
scripts/apidocgen/node_modules/.installed \
|
||||
$(wildcard coderd/*.go) \
|
||||
$(wildcard enterprise/coderd/*.go) \
|
||||
$(wildcard codersdk/*.go) \
|
||||
$(wildcard enterprise/wsproxy/wsproxysdk/*.go) \
|
||||
$(DB_GEN_FILES) \
|
||||
coderd/rbac/object_gen.go \
|
||||
.swaggo \
|
||||
scripts/apidocgen/generate.sh \
|
||||
$(wildcard scripts/apidocgen/postprocess/*) \
|
||||
$(wildcard scripts/apidocgen/markdown-template/*)
|
||||
coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS) -type f) $(wildcard coderd/*.go) $(wildcard enterprise/coderd/*.go) $(wildcard codersdk/*.go) $(wildcard enterprise/wsproxy/wsproxysdk/*.go) $(DB_GEN_FILES) .swaggo docs/manifest.json coderd/rbac/object_gen.go
|
||||
./scripts/apidocgen/generate.sh
|
||||
pnpm exec markdownlint-cli2 --fix ./docs/reference/api/*.md
|
||||
pnpm exec markdown-table-formatter ./docs/reference/api/*.md
|
||||
touch "$@"
|
||||
./scripts/pnpm_install.sh
|
||||
pnpm exec prettier --write ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json
|
||||
|
||||
docs/manifest.json: site/node_modules/.installed coderd/apidoc/.gen docs/reference/cli/index.md
|
||||
(cd site/ && pnpm exec biome format --write ../docs/manifest.json)
|
||||
touch "$@"
|
||||
|
||||
coderd/apidoc/swagger.json: site/node_modules/.installed coderd/apidoc/.gen
|
||||
(cd site/ && pnpm exec biome format --write ../coderd/apidoc/swagger.json)
|
||||
touch "$@"
|
||||
|
||||
update-golden-files:
|
||||
echo 'WARNING: This target is deprecated. Use "make gen/golden-files" instead.' 2>&1
|
||||
echo 'Running "make gen/golden-files"' 2>&1
|
||||
make gen/golden-files
|
||||
update-golden-files: \
|
||||
cli/testdata/.gen-golden \
|
||||
helm/coder/tests/testdata/.gen-golden \
|
||||
helm/provisioner/tests/testdata/.gen-golden \
|
||||
scripts/ci-report/testdata/.gen-golden \
|
||||
enterprise/cli/testdata/.gen-golden \
|
||||
enterprise/tailnet/testdata/.gen-golden \
|
||||
tailnet/testdata/.gen-golden \
|
||||
coderd/.gen-golden \
|
||||
provisioner/terraform/testdata/.gen-golden
|
||||
.PHONY: update-golden-files
|
||||
|
||||
clean/golden-files:
|
||||
find . -type f -name '.gen-golden' -delete
|
||||
find \
|
||||
cli/testdata \
|
||||
coderd/notifications/testdata \
|
||||
coderd/testdata \
|
||||
enterprise/cli/testdata \
|
||||
enterprise/tailnet/testdata \
|
||||
helm/coder/tests/testdata \
|
||||
helm/provisioner/tests/testdata \
|
||||
provisioner/terraform/testdata \
|
||||
tailnet/testdata \
|
||||
-type f -name '*.golden' -delete
|
||||
.PHONY: clean/golden-files
|
||||
|
||||
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
|
||||
go test ./cli -run="Test(CommandHelp|ServerYAML|ErrorExamples|.*Golden)" -update
|
||||
go test ./cli -run="Test(CommandHelp|ServerYAML|ErrorExamples)" -update
|
||||
touch "$@"
|
||||
|
||||
enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard enterprise/cli/*_test.go)
|
||||
@@ -861,10 +688,6 @@ coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wil
|
||||
go test ./coderd -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
|
||||
coderd/notifications/.gen-golden: $(wildcard coderd/notifications/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/notifications/*_test.go)
|
||||
go test ./coderd/notifications -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
|
||||
provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard provisioner/terraform/*_test.go)
|
||||
go test ./provisioner/terraform -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
@@ -875,13 +698,73 @@ provisioner/terraform/testdata/version:
|
||||
fi
|
||||
.PHONY: provisioner/terraform/testdata/version
|
||||
|
||||
test:
|
||||
$(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./... $(if $(RUN),-run $(RUN))
|
||||
.PHONY: test
|
||||
scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*) $(wildcard scripts/ci-report/*.go)
|
||||
go test ./scripts/ci-report -run=TestOutputMatchesGoldenFile -update
|
||||
touch "$@"
|
||||
|
||||
test-cli:
|
||||
$(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./cli/...
|
||||
.PHONY: test-cli
|
||||
# Generate a prettierrc for the site package that uses relative paths for
|
||||
# overrides. This allows us to share the same prettier config between the
|
||||
# site and the root of the repo.
|
||||
site/.prettierrc.yaml: .prettierrc.yaml
|
||||
. ./scripts/lib.sh
|
||||
dependencies yq
|
||||
|
||||
echo "# Code generated by Makefile (../$<). DO NOT EDIT." > "$@"
|
||||
echo "" >> "$@"
|
||||
|
||||
# Replace all listed override files with relative paths inside site/.
|
||||
# - ./ -> ../
|
||||
# - ./site -> ./
|
||||
yq \
|
||||
'.overrides[].files |= map(. | sub("^./"; "") | sub("^"; "../") | sub("../site/"; "./") | sub("../!"; "!../"))' \
|
||||
"$<" >> "$@"
|
||||
|
||||
# Combine .gitignore with .prettierignore.include to generate .prettierignore.
|
||||
.prettierignore: .gitignore .prettierignore.include
|
||||
echo "# Code generated by Makefile ($^). DO NOT EDIT." > "$@"
|
||||
echo "" >> "$@"
|
||||
for f in $^; do
|
||||
echo "# $${f}:" >> "$@"
|
||||
cat "$$f" >> "$@"
|
||||
done
|
||||
|
||||
# Generate ignore files based on gitignore into the site directory. We turn all
|
||||
# rules into relative paths for the `site/` directory (where applicable),
|
||||
# following the pattern format defined by git:
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
#
|
||||
# This is done for compatibility reasons, see:
|
||||
# https://github.com/prettier/prettier/issues/8048
|
||||
# https://github.com/prettier/prettier/issues/8506
|
||||
# https://github.com/prettier/prettier/issues/8679
|
||||
site/.eslintignore site/.prettierignore: .prettierignore Makefile
|
||||
rm -f "$@"
|
||||
touch "$@"
|
||||
# Skip generated by header, inherit `.prettierignore` header as-is.
|
||||
while read -r rule; do
|
||||
# Remove leading ! if present to simplify rule, added back at the end.
|
||||
tmp="$${rule#!}"
|
||||
ignore="$${rule%"$$tmp"}"
|
||||
rule="$$tmp"
|
||||
case "$$rule" in
|
||||
# Comments or empty lines (include).
|
||||
\#*|'') ;;
|
||||
# Generic rules (include).
|
||||
\*\**) ;;
|
||||
# Site prefixed rules (include).
|
||||
site/*) rule="$${rule#site/}";;
|
||||
./site/*) rule="$${rule#./site/}";;
|
||||
# Rules that are non-generic and don't start with site (rewrite).
|
||||
/*) rule=.."$$rule";;
|
||||
*/?*) rule=../"$$rule";;
|
||||
*) ;;
|
||||
esac
|
||||
echo "$${ignore}$${rule}" >> "$@"
|
||||
done < "$<"
|
||||
|
||||
test:
|
||||
$(GIT_FLAGS) gotestsum --format standard-quiet -- -v -short -count=1 ./...
|
||||
.PHONY: test
|
||||
|
||||
# sqlc-cloud-is-setup will fail if no SQLc auth token is set. Use this as a
|
||||
# dependency for any sqlc-cloud related targets.
|
||||
@@ -916,7 +799,7 @@ sqlc-vet: test-postgres-docker
|
||||
test-postgres: test-postgres-docker
|
||||
# The postgres test is prone to failure, so we limit parallelism for
|
||||
# more consistent execution.
|
||||
$(GIT_FLAGS) DB=ci gotestsum \
|
||||
$(GIT_FLAGS) DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \
|
||||
--junitfile="gotests.xml" \
|
||||
--jsonfile="gotests.json" \
|
||||
--packages="./..." -- \
|
||||
@@ -935,35 +818,10 @@ test-migrations: test-postgres-docker
|
||||
if [[ "$${COMMIT_FROM}" == "$${COMMIT_TO}" ]]; then echo "Nothing to do!"; exit 0; fi
|
||||
echo "DROP DATABASE IF EXISTS migrate_test_$${COMMIT_FROM}; CREATE DATABASE migrate_test_$${COMMIT_FROM};" | psql 'postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable'
|
||||
go run ./scripts/migrate-test/main.go --from="$$COMMIT_FROM" --to="$$COMMIT_TO" --postgres-url="postgresql://postgres:postgres@localhost:5432/migrate_test_$${COMMIT_FROM}?sslmode=disable"
|
||||
.PHONY: test-migrations
|
||||
|
||||
# NOTE: we set --memory to the same size as a GitHub runner.
|
||||
test-postgres-docker:
|
||||
docker rm -f test-postgres-docker-${POSTGRES_VERSION} || true
|
||||
|
||||
# Try pulling up to three times to avoid CI flakes.
|
||||
docker pull gcr.io/coder-dev-1/postgres:${POSTGRES_VERSION} || {
|
||||
retries=2
|
||||
for try in $(seq 1 ${retries}); do
|
||||
echo "Failed to pull image, retrying (${try}/${retries})..."
|
||||
sleep 1
|
||||
if docker pull gcr.io/coder-dev-1/postgres:${POSTGRES_VERSION}; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Make sure to not overallocate work_mem and max_connections as each
|
||||
# connection will be allowed to use this much memory. Try adjusting
|
||||
# shared_buffers instead, if needed.
|
||||
#
|
||||
# - work_mem=8MB * max_connections=1000 = 8GB
|
||||
# - shared_buffers=2GB + effective_cache_size=1GB = 3GB
|
||||
#
|
||||
# This leaves 5GB for the rest of the system _and_ storing the
|
||||
# database in memory (--tmpfs).
|
||||
#
|
||||
# https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-WORK-MEM
|
||||
docker run \
|
||||
--env POSTGRES_PASSWORD=postgres \
|
||||
--env POSTGRES_USER=postgres \
|
||||
@@ -976,9 +834,9 @@ test-postgres-docker:
|
||||
--detach \
|
||||
--memory 16GB \
|
||||
gcr.io/coder-dev-1/postgres:${POSTGRES_VERSION} \
|
||||
-c shared_buffers=2GB \
|
||||
-c shared_buffers=1GB \
|
||||
-c work_mem=1GB \
|
||||
-c effective_cache_size=1GB \
|
||||
-c work_mem=8MB \
|
||||
-c max_connections=1000 \
|
||||
-c fsync=off \
|
||||
-c synchronous_commit=off \
|
||||
@@ -993,7 +851,7 @@ test-postgres-docker:
|
||||
|
||||
# Make sure to keep this in sync with test-go-race from .github/workflows/ci.yaml.
|
||||
test-race:
|
||||
$(GIT_FLAGS) gotestsum --junitfile="gotests.xml" -- -race -count=1 -parallel 4 -p 4 ./...
|
||||
$(GIT_FLAGS) gotestsum --junitfile="gotests.xml" -- -race -count=1 ./...
|
||||
.PHONY: test-race
|
||||
|
||||
test-tailnet-integration:
|
||||
@@ -1007,7 +865,6 @@ test-tailnet-integration:
|
||||
-timeout=5m \
|
||||
-count=1 \
|
||||
./tailnet/test/integration
|
||||
.PHONY: test-tailnet-integration
|
||||
|
||||
# Note: we used to add this to the test target, but it's not necessary and we can
|
||||
# achieve the desired result by specifying -count=1 in the go test invocation
|
||||
@@ -1016,19 +873,6 @@ test-clean:
|
||||
go clean -testcache
|
||||
.PHONY: test-clean
|
||||
|
||||
site/e2e/bin/coder: go.mod go.sum $(GO_SRC_FILES)
|
||||
go build -o $@ \
|
||||
-tags ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube \
|
||||
./enterprise/cmd/coder
|
||||
|
||||
test-e2e: site/e2e/bin/coder site/node_modules/.installed site/out/index.html
|
||||
cd site/
|
||||
ifdef CI
|
||||
DEBUG=pw:api pnpm playwright:test --forbid-only --workers 1
|
||||
else
|
||||
pnpm playwright:test
|
||||
endif
|
||||
.PHONY: test-e2e
|
||||
|
||||
dogfood/coder/nix.hash: flake.nix flake.lock
|
||||
sha256sum flake.nix flake.lock >./dogfood/coder/nix.hash
|
||||
test-e2e:
|
||||
cd ./site && DEBUG=pw:api pnpm playwright:test --forbid-only --workers 1
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
<div align="center">
|
||||
<a href="https://coder.com#gh-light-mode-only">
|
||||
<img src="./docs/images/logo-black.png" alt="Coder Logo Light" style="width: 128px">
|
||||
<img src="./docs/images/logo-black.png" style="width: 128px">
|
||||
</a>
|
||||
<a href="https://coder.com#gh-dark-mode-only">
|
||||
<img src="./docs/images/logo-white.png" alt="Coder Logo Dark" style="width: 128px">
|
||||
<img src="./docs/images/logo-white.png" style="width: 128px">
|
||||
</a>
|
||||
|
||||
<h1>
|
||||
@@ -12,23 +11,21 @@
|
||||
</h1>
|
||||
|
||||
<a href="https://coder.com#gh-light-mode-only">
|
||||
<img src="./docs/images/banner-black.png" alt="Coder Banner Light" style="width: 650px">
|
||||
<img src="./docs/images/banner-black.png" style="width: 650px">
|
||||
</a>
|
||||
<a href="https://coder.com#gh-dark-mode-only">
|
||||
<img src="./docs/images/banner-white.png" alt="Coder Banner Dark" style="width: 650px">
|
||||
<img src="./docs/images/banner-white.png" style="width: 650px">
|
||||
</a>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[Quickstart](#quickstart) | [Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Premium](https://coder.com/pricing#compare-plans)
|
||||
[Quickstart](#quickstart) | [Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Enterprise](https://coder.com/docs/enterprise)
|
||||
|
||||
[](https://discord.gg/coder)
|
||||
[](https://github.com/coder/coder/releases/latest)
|
||||
[](https://pkg.go.dev/github.com/coder/coder)
|
||||
[](https://goreportcard.com/report/github.com/coder/coder/v2)
|
||||
[](https://www.bestpractices.dev/projects/9511)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com%2Fcoder%2Fcoder)
|
||||
[](./LICENSE)
|
||||
|
||||
</div>
|
||||
@@ -41,14 +38,14 @@
|
||||
- Onboard developers in seconds instead of days
|
||||
|
||||
<p align="center">
|
||||
<img src="./docs/images/hero-image.png" alt="Coder Hero Image">
|
||||
<img src="./docs/images/hero-image.png">
|
||||
</p>
|
||||
|
||||
## Quickstart
|
||||
|
||||
The most convenient way to try Coder is to install it on your local machine and experiment with provisioning cloud development environments using Docker (works on Linux, macOS, and Windows).
|
||||
|
||||
```shell
|
||||
```
|
||||
# First, install Coder
|
||||
curl -L https://coder.com/install.sh | sh
|
||||
|
||||
@@ -66,7 +63,7 @@ The easiest way to install Coder is to use our
|
||||
and macOS. For Windows, use the latest `..._installer.exe` file from GitHub
|
||||
Releases.
|
||||
|
||||
```shell
|
||||
```bash
|
||||
curl -L https://coder.com/install.sh | sh
|
||||
```
|
||||
|
||||
@@ -94,7 +91,7 @@ Browse our docs [here](https://coder.com/docs) or visit a specific section below
|
||||
- [**Workspaces**](https://coder.com/docs/workspaces): Workspaces contain the IDEs, dependencies, and configuration information needed for software development
|
||||
- [**IDEs**](https://coder.com/docs/ides): Connect your existing editor to a workspace
|
||||
- [**Administration**](https://coder.com/docs/admin): Learn how to operate Coder
|
||||
- [**Premium**](https://coder.com/pricing#compare-plans): Learn about our paid features built for large teams
|
||||
- [**Enterprise**](https://coder.com/docs/enterprise): Learn about our paid features built for large teams
|
||||
|
||||
## Support
|
||||
|
||||
@@ -114,7 +111,6 @@ We are always working on new integrations. Please feel free to open an issue and
|
||||
- [**Module Registry**](https://registry.coder.com): Extend development environments with common use-cases
|
||||
- [**Kubernetes Log Stream**](https://github.com/coder/coder-logstream-kube): Stream Kubernetes Pod events to the Coder startup logs
|
||||
- [**Self-Hosted VS Code Extension Marketplace**](https://github.com/coder/code-marketplace): A private extension marketplace that works in restricted or airgapped networks integrating with [code-server](https://github.com/coder/code-server).
|
||||
- [**Setup Coder**](https://github.com/marketplace/actions/setup-coder): An action to setup coder CLI in GitHub workflows.
|
||||
|
||||
### Community
|
||||
|
||||
|
||||
+6
-6
@@ -8,7 +8,7 @@ to us, what we expect, what you can expect from us.
|
||||
|
||||
You can see the pretty version [here](https://coder.com/security/policy)
|
||||
|
||||
## Why Coder's security matters
|
||||
# Why Coder's security matters
|
||||
|
||||
If an attacker could fully compromise a Coder installation, they could spin up
|
||||
expensive workstations, steal valuable credentials, or steal proprietary source
|
||||
@@ -16,13 +16,13 @@ code. We take this risk very seriously and employ routine pen testing,
|
||||
vulnerability scanning, and code reviews. We also welcome the contributions from
|
||||
the community that helped make this product possible.
|
||||
|
||||
## Where should I report security issues?
|
||||
# Where should I report security issues?
|
||||
|
||||
Please report security issues to <security@coder.com>, providing all relevant
|
||||
Please report security issues to security@coder.com, providing all relevant
|
||||
information. The more details you provide, the easier it will be for us to
|
||||
triage and fix the issue.
|
||||
|
||||
## Out of Scope
|
||||
# Out of Scope
|
||||
|
||||
Our primary concern is around an abuse of the Coder application that allows an
|
||||
attacker to gain access to another users workspace, or spin up unwanted
|
||||
@@ -40,7 +40,7 @@ workspaces.
|
||||
out-of-scope systems should be reported to the appropriate vendor or
|
||||
applicable authority.
|
||||
|
||||
## Our Commitments
|
||||
# Our Commitments
|
||||
|
||||
When working with us, according to this policy, you can expect us to:
|
||||
|
||||
@@ -53,7 +53,7 @@ When working with us, according to this policy, you can expect us to:
|
||||
- Extend Safe Harbor for your vulnerability research that is related to this
|
||||
policy.
|
||||
|
||||
## Our Expectations
|
||||
# Our Expectations
|
||||
|
||||
In participating in our vulnerability disclosure program in good faith, we ask
|
||||
that you:
|
||||
|
||||
+503
-444
File diff suppressed because it is too large
Load Diff
+418
-552
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: .. (interfaces: Lister)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination ./acmock.go -package acmock .. Lister
|
||||
//
|
||||
|
||||
// Package acmock is a generated GoMock package.
|
||||
package acmock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
codersdk "github.com/coder/coder/v2/codersdk"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockLister is a mock of Lister interface.
|
||||
type MockLister struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockListerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockListerMockRecorder is the mock recorder for MockLister.
|
||||
type MockListerMockRecorder struct {
|
||||
mock *MockLister
|
||||
}
|
||||
|
||||
// NewMockLister creates a new mock instance.
|
||||
func NewMockLister(ctrl *gomock.Controller) *MockLister {
|
||||
mock := &MockLister{ctrl: ctrl}
|
||||
mock.recorder = &MockListerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockLister) EXPECT() *MockListerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// List mocks base method.
|
||||
func (m *MockLister) List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "List", ctx)
|
||||
ret0, _ := ret[0].(codersdk.WorkspaceAgentListContainersResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// List indicates an expected call of List.
|
||||
func (mr *MockListerMockRecorder) List(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockLister)(nil).List), ctx)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Package acmock contains a mock implementation of agentcontainers.Lister for use in tests.
|
||||
package acmock
|
||||
|
||||
//go:generate mockgen -destination ./acmock.go -package acmock .. Lister
|
||||
@@ -1,151 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultGetContainersCacheDuration = 10 * time.Second
|
||||
dockerCreatedAtTimeFormat = "2006-01-02 15:04:05 -0700 MST"
|
||||
getContainersTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type devcontainersHandler struct {
|
||||
cacheDuration time.Duration
|
||||
cl Lister
|
||||
clock quartz.Clock
|
||||
|
||||
// lockCh protects the below fields. We use a channel instead of a mutex so we
|
||||
// can handle cancellation properly.
|
||||
lockCh chan struct{}
|
||||
containers *codersdk.WorkspaceAgentListContainersResponse
|
||||
mtime time.Time
|
||||
}
|
||||
|
||||
// Option is a functional option for devcontainersHandler.
|
||||
type Option func(*devcontainersHandler)
|
||||
|
||||
// WithLister sets the agentcontainers.Lister implementation to use.
|
||||
// The default implementation uses the Docker CLI to list containers.
|
||||
func WithLister(cl Lister) Option {
|
||||
return func(ch *devcontainersHandler) {
|
||||
ch.cl = cl
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new devcontainersHandler with the given options applied.
|
||||
func New(options ...Option) http.Handler {
|
||||
ch := &devcontainersHandler{
|
||||
lockCh: make(chan struct{}, 1),
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(ch)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (ch *devcontainersHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
// Client went away.
|
||||
return
|
||||
default:
|
||||
ct, err := ch.getContainers(r.Context())
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
httpapi.Write(r.Context(), rw, http.StatusRequestTimeout, codersdk.Response{
|
||||
Message: "Could not get containers.",
|
||||
Detail: "Took too long to list containers.",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Could not get containers.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, ct)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *devcontainersHandler) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err()
|
||||
default:
|
||||
ch.lockCh <- struct{}{}
|
||||
}
|
||||
defer func() {
|
||||
<-ch.lockCh
|
||||
}()
|
||||
|
||||
// make zero-value usable
|
||||
if ch.cacheDuration == 0 {
|
||||
ch.cacheDuration = defaultGetContainersCacheDuration
|
||||
}
|
||||
if ch.cl == nil {
|
||||
ch.cl = &DockerCLILister{}
|
||||
}
|
||||
if ch.containers == nil {
|
||||
ch.containers = &codersdk.WorkspaceAgentListContainersResponse{}
|
||||
}
|
||||
if ch.clock == nil {
|
||||
ch.clock = quartz.NewReal()
|
||||
}
|
||||
|
||||
now := ch.clock.Now()
|
||||
if now.Sub(ch.mtime) < ch.cacheDuration {
|
||||
// Return a copy of the cached data to avoid accidental modification by the caller.
|
||||
cpy := codersdk.WorkspaceAgentListContainersResponse{
|
||||
Containers: slices.Clone(ch.containers.Containers),
|
||||
Warnings: slices.Clone(ch.containers.Warnings),
|
||||
}
|
||||
return cpy, nil
|
||||
}
|
||||
|
||||
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, getContainersTimeout)
|
||||
defer timeoutCancel()
|
||||
updated, err := ch.cl.List(timeoutCtx)
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("get containers: %w", err)
|
||||
}
|
||||
ch.containers = &updated
|
||||
ch.mtime = now
|
||||
|
||||
// Return a copy of the cached data to avoid accidental modification by the
|
||||
// caller.
|
||||
cpy := codersdk.WorkspaceAgentListContainersResponse{
|
||||
Containers: slices.Clone(ch.containers.Containers),
|
||||
Warnings: slices.Clone(ch.containers.Warnings),
|
||||
}
|
||||
return cpy, nil
|
||||
}
|
||||
|
||||
// Lister is an interface for listing containers visible to the
|
||||
// workspace agent.
|
||||
type Lister interface {
|
||||
// List returns a list of containers visible to the workspace agent.
|
||||
// This should include running and stopped containers.
|
||||
List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error)
|
||||
}
|
||||
|
||||
// NoopLister is a Lister interface that never returns any containers.
|
||||
type NoopLister struct{}
|
||||
|
||||
var _ Lister = NoopLister{}
|
||||
|
||||
func (NoopLister) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, nil
|
||||
}
|
||||
@@ -1,522 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/user"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/dcspec"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// DockerCLILister is a ContainerLister that lists containers using the docker CLI
|
||||
type DockerCLILister struct {
|
||||
execer agentexec.Execer
|
||||
}
|
||||
|
||||
var _ Lister = &DockerCLILister{}
|
||||
|
||||
func NewDocker(execer agentexec.Execer) Lister {
|
||||
return &DockerCLILister{
|
||||
execer: agentexec.DefaultExecer,
|
||||
}
|
||||
}
|
||||
|
||||
// DockerEnvInfoer is an implementation of agentssh.EnvInfoer that returns
|
||||
// information about a container.
|
||||
type DockerEnvInfoer struct {
|
||||
usershell.SystemEnvInfo
|
||||
container string
|
||||
user *user.User
|
||||
userShell string
|
||||
env []string
|
||||
}
|
||||
|
||||
// EnvInfo returns information about the environment of a container.
|
||||
func EnvInfo(ctx context.Context, execer agentexec.Execer, container, containerUser string) (*DockerEnvInfoer, error) {
|
||||
var dei DockerEnvInfoer
|
||||
dei.container = container
|
||||
|
||||
if containerUser == "" {
|
||||
// Get the "default" user of the container if no user is specified.
|
||||
// TODO: handle different container runtimes.
|
||||
cmd, args := wrapDockerExec(container, "", "whoami")
|
||||
stdout, stderr, err := run(ctx, execer, cmd, args...)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get container user: run whoami: %w: %s", err, stderr)
|
||||
}
|
||||
if len(stdout) == 0 {
|
||||
return nil, xerrors.Errorf("get container user: run whoami: empty output")
|
||||
}
|
||||
containerUser = stdout
|
||||
}
|
||||
// Now that we know the username, get the required info from the container.
|
||||
// We can't assume the presence of `getent` so we'll just have to sniff /etc/passwd.
|
||||
cmd, args := wrapDockerExec(container, containerUser, "cat", "/etc/passwd")
|
||||
stdout, stderr, err := run(ctx, execer, cmd, args...)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get container user: read /etc/passwd: %w: %q", err, stderr)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout))
|
||||
var foundLine string
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if !strings.HasPrefix(line, containerUser+":") {
|
||||
continue
|
||||
}
|
||||
foundLine = line
|
||||
break
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, xerrors.Errorf("get container user: scan /etc/passwd: %w", err)
|
||||
}
|
||||
if foundLine == "" {
|
||||
return nil, xerrors.Errorf("get container user: no matching entry for %q found in /etc/passwd", containerUser)
|
||||
}
|
||||
|
||||
// Parse the output of /etc/passwd. It looks like this:
|
||||
// postgres:x:999:999::/var/lib/postgresql:/bin/bash
|
||||
passwdFields := strings.Split(foundLine, ":")
|
||||
if len(passwdFields) != 7 {
|
||||
return nil, xerrors.Errorf("get container user: invalid line in /etc/passwd: %q", foundLine)
|
||||
}
|
||||
|
||||
// The fifth entry in /etc/passwd contains GECOS information, which is a
|
||||
// comma-separated list of fields. The first field is the user's full name.
|
||||
gecos := strings.Split(passwdFields[4], ",")
|
||||
fullName := ""
|
||||
if len(gecos) > 1 {
|
||||
fullName = gecos[0]
|
||||
}
|
||||
|
||||
dei.user = &user.User{
|
||||
Gid: passwdFields[3],
|
||||
HomeDir: passwdFields[5],
|
||||
Name: fullName,
|
||||
Uid: passwdFields[2],
|
||||
Username: containerUser,
|
||||
}
|
||||
dei.userShell = passwdFields[6]
|
||||
|
||||
// We need to inspect the container labels for remoteEnv and append these to
|
||||
// the resulting docker exec command.
|
||||
// ref: https://code.visualstudio.com/docs/devcontainers/attach-container
|
||||
env, err := devcontainerEnv(ctx, execer, container)
|
||||
if err != nil { // best effort.
|
||||
return nil, xerrors.Errorf("read devcontainer remoteEnv: %w", err)
|
||||
}
|
||||
dei.env = env
|
||||
|
||||
return &dei, nil
|
||||
}
|
||||
|
||||
func (dei *DockerEnvInfoer) User() (*user.User, error) {
|
||||
// Clone the user so that the caller can't modify it
|
||||
u := *dei.user
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (dei *DockerEnvInfoer) Shell(string) (string, error) {
|
||||
return dei.userShell, nil
|
||||
}
|
||||
|
||||
func (dei *DockerEnvInfoer) ModifyCommand(cmd string, args ...string) (string, []string) {
|
||||
// Wrap the command with `docker exec` and run it as the container user.
|
||||
// There is some additional munging here regarding the container user and environment.
|
||||
dockerArgs := []string{
|
||||
"exec",
|
||||
// The assumption is that this command will be a shell command, so allocate a PTY.
|
||||
"--interactive",
|
||||
"--tty",
|
||||
// Run the command as the user in the container.
|
||||
"--user",
|
||||
dei.user.Username,
|
||||
// Set the working directory to the user's home directory as a sane default.
|
||||
"--workdir",
|
||||
dei.user.HomeDir,
|
||||
}
|
||||
|
||||
// Append the environment variables from the container.
|
||||
for _, e := range dei.env {
|
||||
dockerArgs = append(dockerArgs, "--env", e)
|
||||
}
|
||||
|
||||
// Append the container name and the command.
|
||||
dockerArgs = append(dockerArgs, dei.container, cmd)
|
||||
return "docker", append(dockerArgs, args...)
|
||||
}
|
||||
|
||||
// devcontainerEnv is a helper function that inspects the container labels to
|
||||
// find the required environment variables for running a command in the container.
|
||||
func devcontainerEnv(ctx context.Context, execer agentexec.Execer, container string) ([]string, error) {
|
||||
stdout, stderr, err := runDockerInspect(ctx, execer, container)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("inspect container: %w: %q", err, stderr)
|
||||
}
|
||||
|
||||
ins, _, err := convertDockerInspect(stdout)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("inspect container: %w", err)
|
||||
}
|
||||
|
||||
if len(ins) != 1 {
|
||||
return nil, xerrors.Errorf("inspect container: expected 1 container, got %d", len(ins))
|
||||
}
|
||||
|
||||
in := ins[0]
|
||||
if in.Labels == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// We want to look for the devcontainer metadata, which is in the
|
||||
// value of the label `devcontainer.metadata`.
|
||||
rawMeta, ok := in.Labels["devcontainer.metadata"]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
meta := make([]dcspec.DevContainer, 0)
|
||||
if err := json.Unmarshal([]byte(rawMeta), &meta); err != nil {
|
||||
return nil, xerrors.Errorf("unmarshal devcontainer.metadata: %w", err)
|
||||
}
|
||||
|
||||
// The environment variables are stored in the `remoteEnv` key.
|
||||
env := make([]string, 0)
|
||||
for _, m := range meta {
|
||||
for k, v := range m.RemoteEnv {
|
||||
if v == nil { // *string per spec
|
||||
// devcontainer-cli will set this to the string "null" if the value is
|
||||
// not set. Explicitly setting to an empty string here as this would be
|
||||
// more expected here.
|
||||
v = ptr.Ref("")
|
||||
}
|
||||
env = append(env, fmt.Sprintf("%s=%s", k, *v))
|
||||
}
|
||||
}
|
||||
slices.Sort(env)
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// wrapDockerExec is a helper function that wraps the given command and arguments
|
||||
// with a docker exec command that runs as the given user in the given
|
||||
// container. This is used to fetch information about a container prior to
|
||||
// running the actual command.
|
||||
func wrapDockerExec(containerName, userName, cmd string, args ...string) (string, []string) {
|
||||
dockerArgs := []string{"exec", "--interactive"}
|
||||
if userName != "" {
|
||||
dockerArgs = append(dockerArgs, "--user", userName)
|
||||
}
|
||||
dockerArgs = append(dockerArgs, containerName, cmd)
|
||||
return "docker", append(dockerArgs, args...)
|
||||
}
|
||||
|
||||
// Helper function to run a command and return its stdout and stderr.
|
||||
// We want to differentiate stdout and stderr instead of using CombinedOutput.
|
||||
// We also want to differentiate between a command running successfully with
|
||||
// output to stderr and a non-zero exit code.
|
||||
func run(ctx context.Context, execer agentexec.Execer, cmd string, args ...string) (stdout, stderr string, err error) {
|
||||
var stdoutBuf, stderrBuf strings.Builder
|
||||
execCmd := execer.CommandContext(ctx, cmd, args...)
|
||||
execCmd.Stdout = &stdoutBuf
|
||||
execCmd.Stderr = &stderrBuf
|
||||
err = execCmd.Run()
|
||||
stdout = strings.TrimSpace(stdoutBuf.String())
|
||||
stderr = strings.TrimSpace(stderrBuf.String())
|
||||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
// List all container IDs, one per line, with no truncation
|
||||
cmd := dcl.execer.CommandContext(ctx, "docker", "ps", "--all", "--quiet", "--no-trunc")
|
||||
cmd.Stdout = &stdoutBuf
|
||||
cmd.Stderr = &stderrBuf
|
||||
if err := cmd.Run(); err != nil {
|
||||
// TODO(Cian): detect specific errors:
|
||||
// - docker not installed
|
||||
// - docker not running
|
||||
// - no permissions to talk to docker
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker ps: %w: %q", err, strings.TrimSpace(stderrBuf.String()))
|
||||
}
|
||||
|
||||
ids := make([]string, 0)
|
||||
scanner := bufio.NewScanner(&stdoutBuf)
|
||||
for scanner.Scan() {
|
||||
tmp := strings.TrimSpace(scanner.Text())
|
||||
if tmp == "" {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, tmp)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("scan docker ps output: %w", err)
|
||||
}
|
||||
|
||||
res := codersdk.WorkspaceAgentListContainersResponse{
|
||||
Containers: make([]codersdk.WorkspaceAgentContainer, 0, len(ids)),
|
||||
Warnings: make([]string, 0),
|
||||
}
|
||||
dockerPsStderr := strings.TrimSpace(stderrBuf.String())
|
||||
if dockerPsStderr != "" {
|
||||
res.Warnings = append(res.Warnings, dockerPsStderr)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// now we can get the detailed information for each container
|
||||
// Run `docker inspect` on each container ID.
|
||||
// NOTE: There is an unavoidable potential race condition where a
|
||||
// container is removed between `docker ps` and `docker inspect`.
|
||||
// In this case, stderr will contain an error message but stdout
|
||||
// will still contain valid JSON. We will just end up missing
|
||||
// information about the removed container. We could potentially
|
||||
// log this error, but I'm not sure it's worth it.
|
||||
dockerInspectStdout, dockerInspectStderr, err := runDockerInspect(ctx, dcl.execer, ids...)
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("run docker inspect: %w: %s", err, dockerInspectStderr)
|
||||
}
|
||||
|
||||
if len(dockerInspectStderr) > 0 {
|
||||
res.Warnings = append(res.Warnings, string(dockerInspectStderr))
|
||||
}
|
||||
|
||||
outs, warns, err := convertDockerInspect(dockerInspectStdout)
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("convert docker inspect output: %w", err)
|
||||
}
|
||||
res.Warnings = append(res.Warnings, warns...)
|
||||
res.Containers = append(res.Containers, outs...)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// runDockerInspect is a helper function that runs `docker inspect` on the given
|
||||
// container IDs and returns the parsed output.
|
||||
// The stderr output is also returned for logging purposes.
|
||||
func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...string) (stdout, stderr []byte, err error) {
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
cmd := execer.CommandContext(ctx, "docker", append([]string{"inspect"}, ids...)...)
|
||||
cmd.Stdout = &stdoutBuf
|
||||
cmd.Stderr = &stderrBuf
|
||||
err = cmd.Run()
|
||||
stdout = bytes.TrimSpace(stdoutBuf.Bytes())
|
||||
stderr = bytes.TrimSpace(stderrBuf.Bytes())
|
||||
if err != nil {
|
||||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
return stdout, stderr, nil
|
||||
}
|
||||
|
||||
// To avoid a direct dependency on the Docker API, we use the docker CLI
|
||||
// to fetch information about containers.
|
||||
type dockerInspect struct {
|
||||
ID string `json:"Id"`
|
||||
Created time.Time `json:"Created"`
|
||||
Config dockerInspectConfig `json:"Config"`
|
||||
Name string `json:"Name"`
|
||||
Mounts []dockerInspectMount `json:"Mounts"`
|
||||
State dockerInspectState `json:"State"`
|
||||
NetworkSettings dockerInspectNetworkSettings `json:"NetworkSettings"`
|
||||
}
|
||||
|
||||
type dockerInspectConfig struct {
|
||||
Image string `json:"Image"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
type dockerInspectPort struct {
|
||||
HostIP string `json:"HostIp"`
|
||||
HostPort string `json:"HostPort"`
|
||||
}
|
||||
|
||||
type dockerInspectMount struct {
|
||||
Source string `json:"Source"`
|
||||
Destination string `json:"Destination"`
|
||||
Type string `json:"Type"`
|
||||
}
|
||||
|
||||
type dockerInspectState struct {
|
||||
Running bool `json:"Running"`
|
||||
ExitCode int `json:"ExitCode"`
|
||||
Error string `json:"Error"`
|
||||
}
|
||||
|
||||
type dockerInspectNetworkSettings struct {
|
||||
Ports map[string][]dockerInspectPort `json:"Ports"`
|
||||
}
|
||||
|
||||
func (dis dockerInspectState) String() string {
|
||||
if dis.Running {
|
||||
return "running"
|
||||
}
|
||||
var sb strings.Builder
|
||||
_, _ = sb.WriteString("exited")
|
||||
if dis.ExitCode != 0 {
|
||||
_, _ = sb.WriteString(fmt.Sprintf(" with code %d", dis.ExitCode))
|
||||
} else {
|
||||
_, _ = sb.WriteString(" successfully")
|
||||
}
|
||||
if dis.Error != "" {
|
||||
_, _ = sb.WriteString(fmt.Sprintf(": %s", dis.Error))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func convertDockerInspect(raw []byte) ([]codersdk.WorkspaceAgentContainer, []string, error) {
|
||||
var warns []string
|
||||
var ins []dockerInspect
|
||||
if err := json.NewDecoder(bytes.NewReader(raw)).Decode(&ins); err != nil {
|
||||
return nil, nil, xerrors.Errorf("decode docker inspect output: %w", err)
|
||||
}
|
||||
outs := make([]codersdk.WorkspaceAgentContainer, 0, len(ins))
|
||||
|
||||
// Say you have two containers:
|
||||
// - Container A with Host IP 127.0.0.1:8000 mapped to container port 8001
|
||||
// - Container B with Host IP [::1]:8000 mapped to container port 8001
|
||||
// A request to localhost:8000 may be routed to either container.
|
||||
// We don't know which one for sure, so we need to surface this to the user.
|
||||
// Keep track of all host ports we see. If we see the same host port
|
||||
// mapped to multiple containers on different host IPs, we need to
|
||||
// warn the user about this.
|
||||
// Note that we only do this for loopback or unspecified IPs.
|
||||
// We'll assume that the user knows what they're doing if they bind to
|
||||
// a specific IP address.
|
||||
hostPortContainers := make(map[int][]string)
|
||||
|
||||
for _, in := range ins {
|
||||
out := codersdk.WorkspaceAgentContainer{
|
||||
CreatedAt: in.Created,
|
||||
// Remove the leading slash from the container name
|
||||
FriendlyName: strings.TrimPrefix(in.Name, "/"),
|
||||
ID: in.ID,
|
||||
Image: in.Config.Image,
|
||||
Labels: in.Config.Labels,
|
||||
Ports: make([]codersdk.WorkspaceAgentContainerPort, 0),
|
||||
Running: in.State.Running,
|
||||
Status: in.State.String(),
|
||||
Volumes: make(map[string]string, len(in.Mounts)),
|
||||
}
|
||||
|
||||
if in.NetworkSettings.Ports == nil {
|
||||
in.NetworkSettings.Ports = make(map[string][]dockerInspectPort)
|
||||
}
|
||||
portKeys := maps.Keys(in.NetworkSettings.Ports)
|
||||
// Sort the ports for deterministic output.
|
||||
sort.Strings(portKeys)
|
||||
// If we see the same port bound to both ipv4 and ipv6 loopback or unspecified
|
||||
// interfaces to the same container port, there is no point in adding it multiple times.
|
||||
loopbackHostPortContainerPorts := make(map[int]uint16, 0)
|
||||
for _, pk := range portKeys {
|
||||
for _, p := range in.NetworkSettings.Ports[pk] {
|
||||
cp, network, err := convertDockerPort(pk)
|
||||
if err != nil {
|
||||
warns = append(warns, fmt.Sprintf("convert docker port: %s", err.Error()))
|
||||
// Default network to "tcp" if we can't parse it.
|
||||
network = "tcp"
|
||||
}
|
||||
hp, err := strconv.Atoi(p.HostPort)
|
||||
if err != nil {
|
||||
warns = append(warns, fmt.Sprintf("convert docker host port: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
if hp > 65535 || hp < 1 { // invalid port
|
||||
warns = append(warns, fmt.Sprintf("convert docker host port: invalid host port %d", hp))
|
||||
continue
|
||||
}
|
||||
|
||||
// Deduplicate host ports for loopback and unspecified IPs.
|
||||
if isLoopbackOrUnspecified(p.HostIP) {
|
||||
if found, ok := loopbackHostPortContainerPorts[hp]; ok && found == cp {
|
||||
// We've already seen this port, so skip it.
|
||||
continue
|
||||
}
|
||||
loopbackHostPortContainerPorts[hp] = cp
|
||||
// Also keep track of the host port and the container ID.
|
||||
hostPortContainers[hp] = append(hostPortContainers[hp], in.ID)
|
||||
}
|
||||
out.Ports = append(out.Ports, codersdk.WorkspaceAgentContainerPort{
|
||||
Network: network,
|
||||
Port: cp,
|
||||
// #nosec G115 - Safe conversion since Docker ports are limited to uint16 range
|
||||
HostPort: uint16(hp),
|
||||
HostIP: p.HostIP,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if in.Mounts == nil {
|
||||
in.Mounts = []dockerInspectMount{}
|
||||
}
|
||||
// Sort the mounts for deterministic output.
|
||||
sort.Slice(in.Mounts, func(i, j int) bool {
|
||||
return in.Mounts[i].Source < in.Mounts[j].Source
|
||||
})
|
||||
for _, k := range in.Mounts {
|
||||
out.Volumes[k.Source] = k.Destination
|
||||
}
|
||||
outs = append(outs, out)
|
||||
}
|
||||
|
||||
// Check if any host ports are mapped to multiple containers.
|
||||
for hp, ids := range hostPortContainers {
|
||||
if len(ids) > 1 {
|
||||
warns = append(warns, fmt.Sprintf("host port %d is mapped to multiple containers on different interfaces: %s", hp, strings.Join(ids, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
return outs, warns, nil
|
||||
}
|
||||
|
||||
// convertDockerPort converts a Docker port string to a port number and network
|
||||
// example: "8080/tcp" -> 8080, "tcp"
|
||||
//
|
||||
// "8080" -> 8080, "tcp"
|
||||
func convertDockerPort(in string) (uint16, string, error) {
|
||||
parts := strings.Split(in, "/")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// assume it's a TCP port
|
||||
p, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, "", xerrors.Errorf("invalid port format: %s", in)
|
||||
}
|
||||
// #nosec G115 - Safe conversion since Docker TCP ports are limited to uint16 range
|
||||
return uint16(p), "tcp", nil
|
||||
case 2:
|
||||
p, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, "", xerrors.Errorf("invalid port format: %s", in)
|
||||
}
|
||||
// #nosec G115 - Safe conversion since Docker ports are limited to uint16 range
|
||||
return uint16(p), parts[1], nil
|
||||
default:
|
||||
return 0, "", xerrors.Errorf("invalid port format: %s", in)
|
||||
}
|
||||
}
|
||||
|
||||
// convenience function to check if an IP address is loopback or unspecified
|
||||
func isLoopbackOrUnspecified(ips string) bool {
|
||||
nip := net.ParseIP(ips)
|
||||
if nip == nil {
|
||||
return false // technically correct, I suppose
|
||||
}
|
||||
return nip.IsLoopback() || nip.IsUnspecified()
|
||||
}
|
||||
@@ -1,839 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/acmock"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
// TestIntegrationDocker tests agentcontainers functionality using a real
|
||||
// Docker container. It starts a container with a known
|
||||
// label, lists the containers, and verifies that the expected container is
|
||||
// returned. It also executes a sample command inside the container.
|
||||
// The container is deleted after the test is complete.
|
||||
// As this test creates containers, it is skipped by default.
|
||||
// It can be run manually as follows:
|
||||
//
|
||||
// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerCLIContainerLister
|
||||
//
|
||||
//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel.
|
||||
func TestIntegrationDocker(t *testing.T) {
|
||||
if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" {
|
||||
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
|
||||
}
|
||||
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
testLabelValue := uuid.New().String()
|
||||
// Create a temporary directory to validate that we surface mounts correctly.
|
||||
testTempDir := t.TempDir()
|
||||
// Pick a random port to expose for testing port bindings.
|
||||
testRandPort := testutil.RandomPortNoListen(t)
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "busybox",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"sleep", "infnity"},
|
||||
Labels: map[string]string{
|
||||
"com.coder.test": testLabelValue,
|
||||
"devcontainer.metadata": `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`,
|
||||
},
|
||||
Mounts: []string{testTempDir + ":" + testTempDir},
|
||||
ExposedPorts: []string{fmt.Sprintf("%d/tcp", testRandPort)},
|
||||
PortBindings: map[docker.Port][]docker.PortBinding{
|
||||
docker.Port(fmt.Sprintf("%d/tcp", testRandPort)): {
|
||||
{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: strconv.FormatInt(int64(testRandPort), 10),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
require.NoError(t, err, "Could not start test docker container")
|
||||
t.Logf("Created container %q", ct.Container.Name)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name)
|
||||
t.Logf("Purged container %q", ct.Container.Name)
|
||||
})
|
||||
// Wait for container to start
|
||||
require.Eventually(t, func() bool {
|
||||
ct, ok := pool.ContainerByName(ct.Container.Name)
|
||||
return ok && ct.Container.State.Running
|
||||
}, testutil.WaitShort, testutil.IntervalSlow, "Container did not start in time")
|
||||
|
||||
dcl := NewDocker(agentexec.DefaultExecer)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
actual, err := dcl.List(ctx)
|
||||
require.NoError(t, err, "Could not list containers")
|
||||
require.Empty(t, actual.Warnings, "Expected no warnings")
|
||||
var found bool
|
||||
for _, foundContainer := range actual.Containers {
|
||||
if foundContainer.ID == ct.Container.ID {
|
||||
found = true
|
||||
assert.Equal(t, ct.Container.Created, foundContainer.CreatedAt)
|
||||
// ory/dockertest pre-pends a forward slash to the container name.
|
||||
assert.Equal(t, strings.TrimPrefix(ct.Container.Name, "/"), foundContainer.FriendlyName)
|
||||
// ory/dockertest returns the sha256 digest of the image.
|
||||
assert.Equal(t, "busybox:latest", foundContainer.Image)
|
||||
assert.Equal(t, ct.Container.Config.Labels, foundContainer.Labels)
|
||||
assert.True(t, foundContainer.Running)
|
||||
assert.Equal(t, "running", foundContainer.Status)
|
||||
if assert.Len(t, foundContainer.Ports, 1) {
|
||||
assert.Equal(t, testRandPort, foundContainer.Ports[0].Port)
|
||||
assert.Equal(t, "tcp", foundContainer.Ports[0].Network)
|
||||
}
|
||||
if assert.Len(t, foundContainer.Volumes, 1) {
|
||||
assert.Equal(t, testTempDir, foundContainer.Volumes[testTempDir])
|
||||
}
|
||||
// Test that EnvInfo is able to correctly modify a command to be
|
||||
// executed inside the container.
|
||||
dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, "")
|
||||
require.NoError(t, err, "Expected no error from DockerEnvInfo()")
|
||||
ptyWrappedCmd, ptyWrappedArgs := dei.ModifyCommand("/bin/sh", "--norc")
|
||||
ptyCmd, ptyPs, err := pty.Start(agentexec.DefaultExecer.PTYCommandContext(ctx, ptyWrappedCmd, ptyWrappedArgs...))
|
||||
require.NoError(t, err, "failed to start pty command")
|
||||
t.Cleanup(func() {
|
||||
_ = ptyPs.Kill()
|
||||
_ = ptyCmd.Close()
|
||||
})
|
||||
tr := testutil.NewTerminalReader(t, ptyCmd.OutputReader())
|
||||
matchPrompt := func(line string) bool {
|
||||
return strings.Contains(line, "#")
|
||||
}
|
||||
matchHostnameCmd := func(line string) bool {
|
||||
return strings.Contains(strings.TrimSpace(line), "hostname")
|
||||
}
|
||||
matchHostnameOuput := func(line string) bool {
|
||||
return strings.Contains(strings.TrimSpace(line), ct.Container.Config.Hostname)
|
||||
}
|
||||
matchEnvCmd := func(line string) bool {
|
||||
return strings.Contains(strings.TrimSpace(line), "env")
|
||||
}
|
||||
matchEnvOutput := func(line string) bool {
|
||||
return strings.Contains(line, "FOO=bar") || strings.Contains(line, "MULTILINE=foo")
|
||||
}
|
||||
require.NoError(t, tr.ReadUntil(ctx, matchPrompt), "failed to match prompt")
|
||||
t.Logf("Matched prompt")
|
||||
_, err = ptyCmd.InputWriter().Write([]byte("hostname\r\n"))
|
||||
require.NoError(t, err, "failed to write to pty")
|
||||
t.Logf("Wrote hostname command")
|
||||
require.NoError(t, tr.ReadUntil(ctx, matchHostnameCmd), "failed to match hostname command")
|
||||
t.Logf("Matched hostname command")
|
||||
require.NoError(t, tr.ReadUntil(ctx, matchHostnameOuput), "failed to match hostname output")
|
||||
t.Logf("Matched hostname output")
|
||||
_, err = ptyCmd.InputWriter().Write([]byte("env\r\n"))
|
||||
require.NoError(t, err, "failed to write to pty")
|
||||
t.Logf("Wrote env command")
|
||||
require.NoError(t, tr.ReadUntil(ctx, matchEnvCmd), "failed to match env command")
|
||||
t.Logf("Matched env command")
|
||||
require.NoError(t, tr.ReadUntil(ctx, matchEnvOutput), "failed to match env output")
|
||||
t.Logf("Matched env output")
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "Expected to find container with label 'com.coder.test=%s'", testLabelValue)
|
||||
}
|
||||
|
||||
func TestWrapDockerExec(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
containerUser string
|
||||
cmdArgs []string
|
||||
wantCmd []string
|
||||
}{
|
||||
{
|
||||
name: "cmd with no args",
|
||||
containerUser: "my-user",
|
||||
cmdArgs: []string{"my-cmd"},
|
||||
wantCmd: []string{"docker", "exec", "--interactive", "--user", "my-user", "my-container", "my-cmd"},
|
||||
},
|
||||
{
|
||||
name: "cmd with args",
|
||||
containerUser: "my-user",
|
||||
cmdArgs: []string{"my-cmd", "arg1", "--arg2", "arg3", "--arg4"},
|
||||
wantCmd: []string{"docker", "exec", "--interactive", "--user", "my-user", "my-container", "my-cmd", "arg1", "--arg2", "arg3", "--arg4"},
|
||||
},
|
||||
{
|
||||
name: "no user specified",
|
||||
containerUser: "",
|
||||
cmdArgs: []string{"my-cmd"},
|
||||
wantCmd: []string{"docker", "exec", "--interactive", "my-container", "my-cmd"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt // appease the linter even though this isn't needed anymore
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actualCmd, actualArgs := wrapDockerExec("my-container", tt.containerUser, tt.cmdArgs[0], tt.cmdArgs[1:]...)
|
||||
assert.Equal(t, tt.wantCmd[0], actualCmd)
|
||||
assert.Equal(t, tt.wantCmd[1:], actualArgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestContainersHandler tests the containersHandler.getContainers method using
|
||||
// a mock implementation. It specifically tests caching behavior.
|
||||
func TestContainersHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("list", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fakeCt := fakeContainer(t)
|
||||
fakeCt2 := fakeContainer(t)
|
||||
makeResponse := func(cts ...codersdk.WorkspaceAgentContainer) codersdk.WorkspaceAgentListContainersResponse {
|
||||
return codersdk.WorkspaceAgentListContainersResponse{Containers: cts}
|
||||
}
|
||||
|
||||
// Each test case is called multiple times to ensure idempotency
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
// data to be stored in the handler
|
||||
cacheData codersdk.WorkspaceAgentListContainersResponse
|
||||
// duration of cache
|
||||
cacheDur time.Duration
|
||||
// relative age of the cached data
|
||||
cacheAge time.Duration
|
||||
// function to set up expectations for the mock
|
||||
setupMock func(*acmock.MockLister)
|
||||
// expected result
|
||||
expected codersdk.WorkspaceAgentListContainersResponse
|
||||
// expected error
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no cache",
|
||||
setupMock: func(mcl *acmock.MockLister) {
|
||||
mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes()
|
||||
},
|
||||
expected: makeResponse(fakeCt),
|
||||
},
|
||||
{
|
||||
name: "no data",
|
||||
cacheData: makeResponse(),
|
||||
cacheAge: 2 * time.Second,
|
||||
cacheDur: time.Second,
|
||||
setupMock: func(mcl *acmock.MockLister) {
|
||||
mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes()
|
||||
},
|
||||
expected: makeResponse(fakeCt),
|
||||
},
|
||||
{
|
||||
name: "cached data",
|
||||
cacheAge: time.Second,
|
||||
cacheData: makeResponse(fakeCt),
|
||||
cacheDur: 2 * time.Second,
|
||||
expected: makeResponse(fakeCt),
|
||||
},
|
||||
{
|
||||
name: "lister error",
|
||||
setupMock: func(mcl *acmock.MockLister) {
|
||||
mcl.EXPECT().List(gomock.Any()).Return(makeResponse(), assert.AnError).AnyTimes()
|
||||
},
|
||||
expectedErr: assert.AnError.Error(),
|
||||
},
|
||||
{
|
||||
name: "stale cache",
|
||||
cacheAge: 2 * time.Second,
|
||||
cacheData: makeResponse(fakeCt),
|
||||
cacheDur: time.Second,
|
||||
setupMock: func(mcl *acmock.MockLister) {
|
||||
mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt2), nil).AnyTimes()
|
||||
},
|
||||
expected: makeResponse(fakeCt2),
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
ctx = testutil.Context(t, testutil.WaitShort)
|
||||
clk = quartz.NewMock(t)
|
||||
ctrl = gomock.NewController(t)
|
||||
mockLister = acmock.NewMockLister(ctrl)
|
||||
now = time.Now().UTC()
|
||||
ch = devcontainersHandler{
|
||||
cacheDuration: tc.cacheDur,
|
||||
cl: mockLister,
|
||||
clock: clk,
|
||||
containers: &tc.cacheData,
|
||||
lockCh: make(chan struct{}, 1),
|
||||
}
|
||||
)
|
||||
if tc.cacheAge != 0 {
|
||||
ch.mtime = now.Add(-tc.cacheAge)
|
||||
}
|
||||
if tc.setupMock != nil {
|
||||
tc.setupMock(mockLister)
|
||||
}
|
||||
|
||||
clk.Set(now).MustWait(ctx)
|
||||
|
||||
// Repeat the test to ensure idempotency
|
||||
for i := 0; i < 2; i++ {
|
||||
actual, err := ch.getContainers(ctx)
|
||||
if tc.expectedErr != "" {
|
||||
require.Empty(t, actual, "expected no data (attempt %d)", i)
|
||||
require.ErrorContains(t, err, tc.expectedErr, "expected error (attempt %d)", i)
|
||||
} else {
|
||||
require.NoError(t, err, "expected no error (attempt %d)", i)
|
||||
require.Equal(t, tc.expected, actual, "expected containers to be equal (attempt %d)", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertDockerPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//nolint:paralleltest // variable recapture no longer required
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in string
|
||||
expectPort uint16
|
||||
expectNetwork string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "empty port",
|
||||
in: "",
|
||||
expectError: "invalid port",
|
||||
},
|
||||
{
|
||||
name: "valid tcp port",
|
||||
in: "8080/tcp",
|
||||
expectPort: 8080,
|
||||
expectNetwork: "tcp",
|
||||
},
|
||||
{
|
||||
name: "valid udp port",
|
||||
in: "8080/udp",
|
||||
expectPort: 8080,
|
||||
expectNetwork: "udp",
|
||||
},
|
||||
{
|
||||
name: "valid port no network",
|
||||
in: "8080",
|
||||
expectPort: 8080,
|
||||
expectNetwork: "tcp",
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
in: "invalid/tcp",
|
||||
expectError: "invalid port",
|
||||
},
|
||||
{
|
||||
name: "invalid port no network",
|
||||
in: "invalid",
|
||||
expectError: "invalid port",
|
||||
},
|
||||
{
|
||||
name: "multiple network",
|
||||
in: "8080/tcp/udp",
|
||||
expectError: "invalid port",
|
||||
},
|
||||
} {
|
||||
//nolint: paralleltest // variable recapture no longer required
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actualPort, actualNetwork, actualErr := convertDockerPort(tc.in)
|
||||
if tc.expectError != "" {
|
||||
assert.Zero(t, actualPort, "expected no port")
|
||||
assert.Empty(t, actualNetwork, "expected no network")
|
||||
assert.ErrorContains(t, actualErr, tc.expectError)
|
||||
} else {
|
||||
assert.NoError(t, actualErr, "expected no error")
|
||||
assert.Equal(t, tc.expectPort, actualPort, "expected port to match")
|
||||
assert.Equal(t, tc.expectNetwork, actualNetwork, "expected network to match")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDockerVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in string
|
||||
expectHostPath string
|
||||
expectContainerPath string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "empty volume",
|
||||
in: "",
|
||||
expectError: "invalid volume",
|
||||
},
|
||||
{
|
||||
name: "length 1 volume",
|
||||
in: "/path/to/something",
|
||||
expectHostPath: "/path/to/something",
|
||||
expectContainerPath: "/path/to/something",
|
||||
},
|
||||
{
|
||||
name: "length 2 volume",
|
||||
in: "/path/to/something=/path/to/something/else",
|
||||
expectHostPath: "/path/to/something",
|
||||
expectContainerPath: "/path/to/something/else",
|
||||
},
|
||||
{
|
||||
name: "invalid length volume",
|
||||
in: "/path/to/something=/path/to/something/else=/path/to/something/else/else",
|
||||
expectError: "invalid volume",
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertDockerInspect tests the convertDockerInspect function using
|
||||
// fixtures from ./testdata.
|
||||
func TestConvertDockerInspect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//nolint:paralleltest // variable recapture no longer required
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
expect []codersdk.WorkspaceAgentContainer
|
||||
expectWarns []string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "container_simple",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 55, 58, 91280203, time.UTC),
|
||||
ID: "6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286",
|
||||
FriendlyName: "eloquent_kowalevski",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container_labels",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 20, 3, 28, 71706536, time.UTC),
|
||||
ID: "bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f",
|
||||
FriendlyName: "fervent_bardeen",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{"baz": "zap", "foo": "bar"},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container_binds",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 58, 43, 522505027, time.UTC),
|
||||
ID: "fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a",
|
||||
FriendlyName: "silly_beaver",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{},
|
||||
Volumes: map[string]string{
|
||||
"/tmp/test/a": "/var/coder/a",
|
||||
"/tmp/test/b": "/var/coder/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container_sameport",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 56, 34, 842164541, time.UTC),
|
||||
ID: "4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2",
|
||||
FriendlyName: "modest_varahamihira",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{
|
||||
{
|
||||
Network: "tcp",
|
||||
Port: 12345,
|
||||
HostPort: 12345,
|
||||
HostIP: "0.0.0.0",
|
||||
},
|
||||
},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container_differentport",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 57, 8, 862545133, time.UTC),
|
||||
ID: "3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea",
|
||||
FriendlyName: "boring_ellis",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{
|
||||
{
|
||||
Network: "tcp",
|
||||
Port: 23456,
|
||||
HostPort: 12345,
|
||||
HostIP: "0.0.0.0",
|
||||
},
|
||||
},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container_sameportdiffip",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 56, 34, 842164541, time.UTC),
|
||||
ID: "a",
|
||||
FriendlyName: "a",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{
|
||||
{
|
||||
Network: "tcp",
|
||||
Port: 8001,
|
||||
HostPort: 8000,
|
||||
HostIP: "0.0.0.0",
|
||||
},
|
||||
},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 56, 34, 842164541, time.UTC),
|
||||
ID: "b",
|
||||
FriendlyName: "b",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{
|
||||
{
|
||||
Network: "tcp",
|
||||
Port: 8001,
|
||||
HostPort: 8000,
|
||||
HostIP: "::",
|
||||
},
|
||||
},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
expectWarns: []string{"host port 8000 is mapped to multiple containers on different interfaces: a, b"},
|
||||
},
|
||||
{
|
||||
name: "container_volume",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 59, 42, 39484134, time.UTC),
|
||||
ID: "b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e",
|
||||
FriendlyName: "upbeat_carver",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{},
|
||||
Volumes: map[string]string{
|
||||
"/var/lib/docker/volumes/testvol/_data": "/testvol",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "devcontainer_simple",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 1, 5, 751972661, time.UTC),
|
||||
ID: "0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed",
|
||||
FriendlyName: "optimistic_hopper",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{
|
||||
"devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_simple.json",
|
||||
"devcontainer.metadata": "[]",
|
||||
},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "devcontainer_forwardport",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 3, 55, 22053072, time.UTC),
|
||||
ID: "4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067",
|
||||
FriendlyName: "serene_khayyam",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{
|
||||
"devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_forwardport.json",
|
||||
"devcontainer.metadata": "[]",
|
||||
},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "devcontainer_appport",
|
||||
expect: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
CreatedAt: time.Date(2025, 3, 11, 17, 2, 42, 613747761, time.UTC),
|
||||
ID: "52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3",
|
||||
FriendlyName: "suspicious_margulis",
|
||||
Image: "debian:bookworm",
|
||||
Labels: map[string]string{
|
||||
"devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_appport.json",
|
||||
"devcontainer.metadata": "[]",
|
||||
},
|
||||
Running: true,
|
||||
Status: "running",
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{
|
||||
{
|
||||
Network: "tcp",
|
||||
Port: 8080,
|
||||
HostPort: 32768,
|
||||
HostIP: "0.0.0.0",
|
||||
},
|
||||
},
|
||||
Volumes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
// nolint:paralleltest // variable recapture no longer required
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
bs, err := os.ReadFile(filepath.Join("testdata", tt.name, "docker_inspect.json"))
|
||||
require.NoError(t, err, "failed to read testdata file")
|
||||
actual, warns, err := convertDockerInspect(bs)
|
||||
if len(tt.expectWarns) > 0 {
|
||||
assert.Len(t, warns, len(tt.expectWarns), "expected warnings")
|
||||
for _, warn := range tt.expectWarns {
|
||||
assert.Contains(t, warns, warn)
|
||||
}
|
||||
}
|
||||
if tt.expectError != "" {
|
||||
assert.Empty(t, actual, "expected no data")
|
||||
assert.ErrorContains(t, err, tt.expectError)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err, "expected no error")
|
||||
if diff := cmp.Diff(tt.expect, actual); diff != "" {
|
||||
t.Errorf("unexpected diff (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDockerEnvInfoer tests the ability of EnvInfo to extract information from
|
||||
// running containers. Containers are deleted after the test is complete.
|
||||
// As this test creates containers, it is skipped by default.
|
||||
// It can be run manually as follows:
|
||||
//
|
||||
// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerEnvInfoer
|
||||
//
|
||||
//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel.
|
||||
func TestDockerEnvInfoer(t *testing.T) {
|
||||
if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" {
|
||||
t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test")
|
||||
}
|
||||
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
// nolint:paralleltest // variable recapture no longer required
|
||||
for idx, tt := range []struct {
|
||||
image string
|
||||
labels map[string]string
|
||||
expectedEnv []string
|
||||
containerUser string
|
||||
expectedUsername string
|
||||
expectedUserShell string
|
||||
}{
|
||||
{
|
||||
image: "busybox:latest",
|
||||
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
|
||||
|
||||
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
|
||||
expectedUsername: "root",
|
||||
expectedUserShell: "/bin/sh",
|
||||
},
|
||||
{
|
||||
image: "busybox:latest",
|
||||
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
|
||||
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
|
||||
containerUser: "root",
|
||||
expectedUsername: "root",
|
||||
expectedUserShell: "/bin/sh",
|
||||
},
|
||||
{
|
||||
image: "codercom/enterprise-minimal:ubuntu",
|
||||
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
|
||||
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
|
||||
expectedUsername: "coder",
|
||||
expectedUserShell: "/bin/bash",
|
||||
},
|
||||
{
|
||||
image: "codercom/enterprise-minimal:ubuntu",
|
||||
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
|
||||
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
|
||||
containerUser: "coder",
|
||||
expectedUsername: "coder",
|
||||
expectedUserShell: "/bin/bash",
|
||||
},
|
||||
{
|
||||
image: "codercom/enterprise-minimal:ubuntu",
|
||||
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`},
|
||||
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
|
||||
containerUser: "root",
|
||||
expectedUsername: "root",
|
||||
expectedUserShell: "/bin/bash",
|
||||
},
|
||||
{
|
||||
image: "codercom/enterprise-minimal:ubuntu",
|
||||
labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar"}},{"remoteEnv": {"MULTILINE": "foo\nbar\nbaz"}}]`},
|
||||
expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"},
|
||||
containerUser: "root",
|
||||
expectedUsername: "root",
|
||||
expectedUserShell: "/bin/bash",
|
||||
},
|
||||
} {
|
||||
//nolint:paralleltest // variable recapture no longer required
|
||||
t.Run(fmt.Sprintf("#%d", idx), func(t *testing.T) {
|
||||
// Start a container with the given image
|
||||
// and environment variables
|
||||
image := strings.Split(tt.image, ":")[0]
|
||||
tag := strings.Split(tt.image, ":")[1]
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: image,
|
||||
Tag: tag,
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
Labels: tt.labels,
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
require.NoError(t, err, "Could not start test docker container")
|
||||
t.Logf("Created container %q", ct.Container.Name)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name)
|
||||
t.Logf("Purged container %q", ct.Container.Name)
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, tt.containerUser)
|
||||
require.NoError(t, err, "Expected no error from DockerEnvInfo()")
|
||||
|
||||
u, err := dei.User()
|
||||
require.NoError(t, err, "Expected no error from CurrentUser()")
|
||||
require.Equal(t, tt.expectedUsername, u.Username, "Expected username to match")
|
||||
|
||||
hd, err := dei.HomeDir()
|
||||
require.NoError(t, err, "Expected no error from UserHomeDir()")
|
||||
require.NotEmpty(t, hd, "Expected user homedir to be non-empty")
|
||||
|
||||
sh, err := dei.Shell(tt.containerUser)
|
||||
require.NoError(t, err, "Expected no error from UserShell()")
|
||||
require.Equal(t, tt.expectedUserShell, sh, "Expected user shell to match")
|
||||
|
||||
// We don't need to test the actual environment variables here.
|
||||
environ := dei.Environ()
|
||||
require.NotEmpty(t, environ, "Expected environ to be non-empty")
|
||||
|
||||
// Test that the environment variables are present in modified command
|
||||
// output.
|
||||
envCmd, envArgs := dei.ModifyCommand("env")
|
||||
for _, env := range tt.expectedEnv {
|
||||
require.Subset(t, envArgs, []string{"--env", env})
|
||||
}
|
||||
// Run the command in the container and check the output
|
||||
// HACK: we remove the --tty argument because we're not running in a tty
|
||||
envArgs = slices.DeleteFunc(envArgs, func(s string) bool { return s == "--tty" })
|
||||
stdout, stderr, err := run(ctx, agentexec.DefaultExecer, envCmd, envArgs...)
|
||||
require.Empty(t, stderr, "Expected no stderr output")
|
||||
require.NoError(t, err, "Expected no error from running command")
|
||||
for _, env := range tt.expectedEnv {
|
||||
require.Contains(t, stdout, env)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fakeContainer(t *testing.T, mut ...func(*codersdk.WorkspaceAgentContainer)) codersdk.WorkspaceAgentContainer {
|
||||
t.Helper()
|
||||
ct := codersdk.WorkspaceAgentContainer{
|
||||
CreatedAt: time.Now().UTC(),
|
||||
ID: uuid.New().String(),
|
||||
FriendlyName: testutil.GetRandomName(t),
|
||||
Image: testutil.GetRandomName(t) + ":" + strings.Split(uuid.New().String(), "-")[0],
|
||||
Labels: map[string]string{
|
||||
testutil.GetRandomName(t): testutil.GetRandomName(t),
|
||||
},
|
||||
Running: true,
|
||||
Ports: []codersdk.WorkspaceAgentContainerPort{
|
||||
{
|
||||
Network: "tcp",
|
||||
Port: testutil.RandomPortNoListen(t),
|
||||
HostPort: testutil.RandomPortNoListen(t),
|
||||
//nolint:gosec // this is a test
|
||||
HostIP: []string{"127.0.0.1", "[::1]", "localhost", "0.0.0.0", "[::]", testutil.GetRandomName(t)}[rand.Intn(6)],
|
||||
},
|
||||
},
|
||||
Status: testutil.MustRandString(t, 10),
|
||||
Volumes: map[string]string{testutil.GetRandomName(t): testutil.GetRandomName(t)},
|
||||
}
|
||||
for _, m := range mut {
|
||||
m(&ct)
|
||||
}
|
||||
return ct
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
// Code generated by dcspec/gen.sh. DO NOT EDIT.
|
||||
package dcspec
|
||||
|
||||
// Defines a dev container
|
||||
type DevContainer struct {
|
||||
// Docker build-related options.
|
||||
Build *BuildOptions `json:"build,omitempty"`
|
||||
// The location of the context folder for building the Docker image. The path is relative to
|
||||
// the folder containing the `devcontainer.json` file.
|
||||
Context *string `json:"context,omitempty"`
|
||||
// The location of the Dockerfile that defines the contents of the container. The path is
|
||||
// relative to the folder containing the `devcontainer.json` file.
|
||||
DockerFile *string `json:"dockerFile,omitempty"`
|
||||
// The docker image that will be used to create the container.
|
||||
Image *string `json:"image,omitempty"`
|
||||
// Application ports that are exposed by the container. This can be a single port or an
|
||||
// array of ports. Each port can be a number or a string. A number is mapped to the same
|
||||
// port on the host. A string is passed to Docker unchanged and can be used to map ports
|
||||
// differently, e.g. "8000:8010".
|
||||
AppPort *DevContainerAppPort `json:"appPort"`
|
||||
// Whether to overwrite the command specified in the image. The default is true.
|
||||
//
|
||||
// Whether to overwrite the command specified in the image. The default is false.
|
||||
OverrideCommand *bool `json:"overrideCommand,omitempty"`
|
||||
// The arguments required when starting in the container.
|
||||
RunArgs []string `json:"runArgs,omitempty"`
|
||||
// Action to take when the user disconnects from the container in their editor. The default
|
||||
// is to stop the container.
|
||||
//
|
||||
// Action to take when the user disconnects from the primary container in their editor. The
|
||||
// default is to stop all of the compose containers.
|
||||
ShutdownAction *ShutdownAction `json:"shutdownAction,omitempty"`
|
||||
// The path of the workspace folder inside the container.
|
||||
//
|
||||
// The path of the workspace folder inside the container. This is typically the target path
|
||||
// of a volume mount in the docker-compose.yml.
|
||||
WorkspaceFolder *string `json:"workspaceFolder,omitempty"`
|
||||
// The --mount parameter for docker run. The default is to mount the project folder at
|
||||
// /workspaces/$project.
|
||||
WorkspaceMount *string `json:"workspaceMount,omitempty"`
|
||||
// The name of the docker-compose file(s) used to start the services.
|
||||
DockerComposeFile *CacheFrom `json:"dockerComposeFile"`
|
||||
// An array of services that should be started and stopped.
|
||||
RunServices []string `json:"runServices,omitempty"`
|
||||
// The service you want to work on. This is considered the primary container for your dev
|
||||
// environment which your editor will connect to.
|
||||
Service *string `json:"service,omitempty"`
|
||||
// The JSON schema of the `devcontainer.json` file.
|
||||
Schema *string `json:"$schema,omitempty"`
|
||||
AdditionalProperties map[string]interface{} `json:"additionalProperties,omitempty"`
|
||||
// Passes docker capabilities to include when creating the dev container.
|
||||
CapAdd []string `json:"capAdd,omitempty"`
|
||||
// Container environment variables.
|
||||
ContainerEnv map[string]string `json:"containerEnv,omitempty"`
|
||||
// The user the container will be started with. The default is the user on the Docker image.
|
||||
ContainerUser *string `json:"containerUser,omitempty"`
|
||||
// Tool-specific configuration. Each tool should use a JSON object subproperty with a unique
|
||||
// name to group its customizations.
|
||||
Customizations map[string]interface{} `json:"customizations,omitempty"`
|
||||
// Features to add to the dev container.
|
||||
Features *Features `json:"features,omitempty"`
|
||||
// Ports that are forwarded from the container to the local machine. Can be an integer port
|
||||
// number, or a string of the format "host:port_number".
|
||||
ForwardPorts []ForwardPort `json:"forwardPorts,omitempty"`
|
||||
// Host hardware requirements.
|
||||
HostRequirements *HostRequirements `json:"hostRequirements,omitempty"`
|
||||
// Passes the --init flag when creating the dev container.
|
||||
Init *bool `json:"init,omitempty"`
|
||||
// A command to run locally (i.e Your host machine, cloud VM) before anything else. This
|
||||
// command is run before "onCreateCommand". If this is a single string, it will be run in a
|
||||
// shell. If this is an array of strings, it will be run as a single command without shell.
|
||||
// If this is an object, each provided command will be run in parallel.
|
||||
InitializeCommand *Command `json:"initializeCommand"`
|
||||
// Mount points to set up when creating the container. See Docker's documentation for the
|
||||
// --mount option for the supported syntax.
|
||||
Mounts []MountElement `json:"mounts,omitempty"`
|
||||
// A name for the dev container which can be displayed to the user.
|
||||
Name *string `json:"name,omitempty"`
|
||||
// A command to run when creating the container. This command is run after
|
||||
// "initializeCommand" and before "updateContentCommand". If this is a single string, it
|
||||
// will be run in a shell. If this is an array of strings, it will be run as a single
|
||||
// command without shell. If this is an object, each provided command will be run in
|
||||
// parallel.
|
||||
OnCreateCommand *Command `json:"onCreateCommand"`
|
||||
OtherPortsAttributes *OtherPortsAttributes `json:"otherPortsAttributes,omitempty"`
|
||||
// Array consisting of the Feature id (without the semantic version) of Features in the
|
||||
// order the user wants them to be installed.
|
||||
OverrideFeatureInstallOrder []string `json:"overrideFeatureInstallOrder,omitempty"`
|
||||
PortsAttributes *PortsAttributes `json:"portsAttributes,omitempty"`
|
||||
// A command to run when attaching to the container. This command is run after
|
||||
// "postStartCommand". If this is a single string, it will be run in a shell. If this is an
|
||||
// array of strings, it will be run as a single command without shell. If this is an object,
|
||||
// each provided command will be run in parallel.
|
||||
PostAttachCommand *Command `json:"postAttachCommand"`
|
||||
// A command to run after creating the container. This command is run after
|
||||
// "updateContentCommand" and before "postStartCommand". If this is a single string, it will
|
||||
// be run in a shell. If this is an array of strings, it will be run as a single command
|
||||
// without shell. If this is an object, each provided command will be run in parallel.
|
||||
PostCreateCommand *Command `json:"postCreateCommand"`
|
||||
// A command to run after starting the container. This command is run after
|
||||
// "postCreateCommand" and before "postAttachCommand". If this is a single string, it will
|
||||
// be run in a shell. If this is an array of strings, it will be run as a single command
|
||||
// without shell. If this is an object, each provided command will be run in parallel.
|
||||
PostStartCommand *Command `json:"postStartCommand"`
|
||||
// Passes the --privileged flag when creating the dev container.
|
||||
Privileged *bool `json:"privileged,omitempty"`
|
||||
// Remote environment variables to set for processes spawned in the container including
|
||||
// lifecycle scripts and any remote editor/IDE server process.
|
||||
RemoteEnv map[string]*string `json:"remoteEnv,omitempty"`
|
||||
// The username to use for spawning processes in the container including lifecycle scripts
|
||||
// and any remote editor/IDE server process. The default is the same user as the container.
|
||||
RemoteUser *string `json:"remoteUser,omitempty"`
|
||||
// Recommended secrets for this dev container. Recommendations are provided as environment
|
||||
// variable keys with optional metadata.
|
||||
Secrets *Secrets `json:"secrets,omitempty"`
|
||||
// Passes docker security options to include when creating the dev container.
|
||||
SecurityOpt []string `json:"securityOpt,omitempty"`
|
||||
// A command to run when creating the container and rerun when the workspace content was
|
||||
// updated while creating the container. This command is run after "onCreateCommand" and
|
||||
// before "postCreateCommand". If this is a single string, it will be run in a shell. If
|
||||
// this is an array of strings, it will be run as a single command without shell. If this is
|
||||
// an object, each provided command will be run in parallel.
|
||||
UpdateContentCommand *Command `json:"updateContentCommand"`
|
||||
// Controls whether on Linux the container's user should be updated with the local user's
|
||||
// UID and GID. On by default when opening from a local folder.
|
||||
UpdateRemoteUserUID *bool `json:"updateRemoteUserUID,omitempty"`
|
||||
// User environment probe to run. The default is "loginInteractiveShell".
|
||||
UserEnvProbe *UserEnvProbe `json:"userEnvProbe,omitempty"`
|
||||
// The user command to wait for before continuing execution in the background while the UI
|
||||
// is starting up. The default is "updateContentCommand".
|
||||
WaitFor *WaitFor `json:"waitFor,omitempty"`
|
||||
}
|
||||
|
||||
// Docker build-related options.
|
||||
type BuildOptions struct {
|
||||
// The location of the context folder for building the Docker image. The path is relative to
|
||||
// the folder containing the `devcontainer.json` file.
|
||||
Context *string `json:"context,omitempty"`
|
||||
// The location of the Dockerfile that defines the contents of the container. The path is
|
||||
// relative to the folder containing the `devcontainer.json` file.
|
||||
Dockerfile *string `json:"dockerfile,omitempty"`
|
||||
// Build arguments.
|
||||
Args map[string]string `json:"args,omitempty"`
|
||||
// The image to consider as a cache. Use an array to specify multiple images.
|
||||
CacheFrom *CacheFrom `json:"cacheFrom"`
|
||||
// Additional arguments passed to the build command.
|
||||
Options []string `json:"options,omitempty"`
|
||||
// Target stage in a multi-stage build.
|
||||
Target *string `json:"target,omitempty"`
|
||||
}
|
||||
|
||||
// Features to add to the dev container.
|
||||
type Features struct {
|
||||
Fish interface{} `json:"fish"`
|
||||
Gradle interface{} `json:"gradle"`
|
||||
Homebrew interface{} `json:"homebrew"`
|
||||
Jupyterlab interface{} `json:"jupyterlab"`
|
||||
Maven interface{} `json:"maven"`
|
||||
}
|
||||
|
||||
// Host hardware requirements.
|
||||
type HostRequirements struct {
|
||||
// Number of required CPUs.
|
||||
Cpus *int64 `json:"cpus,omitempty"`
|
||||
GPU *GPUUnion `json:"gpu"`
|
||||
// Amount of required RAM in bytes. Supports units tb, gb, mb and kb.
|
||||
Memory *string `json:"memory,omitempty"`
|
||||
// Amount of required disk space in bytes. Supports units tb, gb, mb and kb.
|
||||
Storage *string `json:"storage,omitempty"`
|
||||
}
|
||||
|
||||
// Indicates whether a GPU is required. The string "optional" indicates that a GPU is
|
||||
// optional. An object value can be used to configure more detailed requirements.
|
||||
type GPUClass struct {
|
||||
// Number of required cores.
|
||||
Cores *int64 `json:"cores,omitempty"`
|
||||
// Amount of required RAM in bytes. Supports units tb, gb, mb and kb.
|
||||
Memory *string `json:"memory,omitempty"`
|
||||
}
|
||||
|
||||
type Mount struct {
|
||||
// Mount source.
|
||||
Source *string `json:"source,omitempty"`
|
||||
// Mount target.
|
||||
Target string `json:"target"`
|
||||
// Mount type.
|
||||
Type Type `json:"type"`
|
||||
}
|
||||
|
||||
type OtherPortsAttributes struct {
|
||||
// Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is
|
||||
// required if the local port is a privileged port.
|
||||
ElevateIfNeeded *bool `json:"elevateIfNeeded,omitempty"`
|
||||
// Label that will be shown in the UI for this port.
|
||||
Label *string `json:"label,omitempty"`
|
||||
// Defines the action that occurs when the port is discovered for automatic forwarding
|
||||
OnAutoForward *OnAutoForward `json:"onAutoForward,omitempty"`
|
||||
// The protocol to use when forwarding this port.
|
||||
Protocol *Protocol `json:"protocol,omitempty"`
|
||||
RequireLocalPort *bool `json:"requireLocalPort,omitempty"`
|
||||
}
|
||||
|
||||
type PortsAttributes struct{}
|
||||
|
||||
// Recommended secrets for this dev container. Recommendations are provided as environment
|
||||
// variable keys with optional metadata.
|
||||
type Secrets struct{}
|
||||
|
||||
type GPUEnum string
|
||||
|
||||
const (
|
||||
Optional GPUEnum = "optional"
|
||||
)
|
||||
|
||||
// Mount type.
|
||||
type Type string
|
||||
|
||||
const (
|
||||
Bind Type = "bind"
|
||||
Volume Type = "volume"
|
||||
)
|
||||
|
||||
// Defines the action that occurs when the port is discovered for automatic forwarding
|
||||
type OnAutoForward string
|
||||
|
||||
const (
|
||||
Ignore OnAutoForward = "ignore"
|
||||
Notify OnAutoForward = "notify"
|
||||
OpenBrowser OnAutoForward = "openBrowser"
|
||||
OpenPreview OnAutoForward = "openPreview"
|
||||
Silent OnAutoForward = "silent"
|
||||
)
|
||||
|
||||
// The protocol to use when forwarding this port.
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
HTTP Protocol = "http"
|
||||
HTTPS Protocol = "https"
|
||||
)
|
||||
|
||||
// Action to take when the user disconnects from the container in their editor. The default
|
||||
// is to stop the container.
|
||||
//
|
||||
// Action to take when the user disconnects from the primary container in their editor. The
|
||||
// default is to stop all of the compose containers.
|
||||
type ShutdownAction string
|
||||
|
||||
const (
|
||||
ShutdownActionNone ShutdownAction = "none"
|
||||
StopCompose ShutdownAction = "stopCompose"
|
||||
StopContainer ShutdownAction = "stopContainer"
|
||||
)
|
||||
|
||||
// User environment probe to run. The default is "loginInteractiveShell".
|
||||
type UserEnvProbe string
|
||||
|
||||
const (
|
||||
InteractiveShell UserEnvProbe = "interactiveShell"
|
||||
LoginInteractiveShell UserEnvProbe = "loginInteractiveShell"
|
||||
LoginShell UserEnvProbe = "loginShell"
|
||||
UserEnvProbeNone UserEnvProbe = "none"
|
||||
)
|
||||
|
||||
// The user command to wait for before continuing execution in the background while the UI
|
||||
// is starting up. The default is "updateContentCommand".
|
||||
type WaitFor string
|
||||
|
||||
const (
|
||||
InitializeCommand WaitFor = "initializeCommand"
|
||||
OnCreateCommand WaitFor = "onCreateCommand"
|
||||
PostCreateCommand WaitFor = "postCreateCommand"
|
||||
PostStartCommand WaitFor = "postStartCommand"
|
||||
UpdateContentCommand WaitFor = "updateContentCommand"
|
||||
)
|
||||
|
||||
// Application ports that are exposed by the container. This can be a single port or an
|
||||
// array of ports. Each port can be a number or a string. A number is mapped to the same
|
||||
// port on the host. A string is passed to Docker unchanged and can be used to map ports
|
||||
// differently, e.g. "8000:8010".
|
||||
type DevContainerAppPort struct {
|
||||
Integer *int64
|
||||
String *string
|
||||
UnionArray []AppPortElement
|
||||
}
|
||||
|
||||
// Application ports that are exposed by the container. This can be a single port or an
|
||||
// array of ports. Each port can be a number or a string. A number is mapped to the same
|
||||
// port on the host. A string is passed to Docker unchanged and can be used to map ports
|
||||
// differently, e.g. "8000:8010".
|
||||
type AppPortElement struct {
|
||||
Integer *int64
|
||||
String *string
|
||||
}
|
||||
|
||||
// The image to consider as a cache. Use an array to specify multiple images.
|
||||
//
|
||||
// The name of the docker-compose file(s) used to start the services.
|
||||
type CacheFrom struct {
|
||||
String *string
|
||||
StringArray []string
|
||||
}
|
||||
|
||||
type ForwardPort struct {
|
||||
Integer *int64
|
||||
String *string
|
||||
}
|
||||
|
||||
type GPUUnion struct {
|
||||
Bool *bool
|
||||
Enum *GPUEnum
|
||||
GPUClass *GPUClass
|
||||
}
|
||||
|
||||
// A command to run locally (i.e Your host machine, cloud VM) before anything else. This
|
||||
// command is run before "onCreateCommand". If this is a single string, it will be run in a
|
||||
// shell. If this is an array of strings, it will be run as a single command without shell.
|
||||
// If this is an object, each provided command will be run in parallel.
|
||||
//
|
||||
// A command to run when creating the container. This command is run after
|
||||
// "initializeCommand" and before "updateContentCommand". If this is a single string, it
|
||||
// will be run in a shell. If this is an array of strings, it will be run as a single
|
||||
// command without shell. If this is an object, each provided command will be run in
|
||||
// parallel.
|
||||
//
|
||||
// A command to run when attaching to the container. This command is run after
|
||||
// "postStartCommand". If this is a single string, it will be run in a shell. If this is an
|
||||
// array of strings, it will be run as a single command without shell. If this is an object,
|
||||
// each provided command will be run in parallel.
|
||||
//
|
||||
// A command to run after creating the container. This command is run after
|
||||
// "updateContentCommand" and before "postStartCommand". If this is a single string, it will
|
||||
// be run in a shell. If this is an array of strings, it will be run as a single command
|
||||
// without shell. If this is an object, each provided command will be run in parallel.
|
||||
//
|
||||
// A command to run after starting the container. This command is run after
|
||||
// "postCreateCommand" and before "postAttachCommand". If this is a single string, it will
|
||||
// be run in a shell. If this is an array of strings, it will be run as a single command
|
||||
// without shell. If this is an object, each provided command will be run in parallel.
|
||||
//
|
||||
// A command to run when creating the container and rerun when the workspace content was
|
||||
// updated while creating the container. This command is run after "onCreateCommand" and
|
||||
// before "postCreateCommand". If this is a single string, it will be run in a shell. If
|
||||
// this is an array of strings, it will be run as a single command without shell. If this is
|
||||
// an object, each provided command will be run in parallel.
|
||||
type Command struct {
|
||||
String *string
|
||||
StringArray []string
|
||||
UnionMap map[string]*CacheFrom
|
||||
}
|
||||
|
||||
type MountElement struct {
|
||||
Mount *Mount
|
||||
String *string
|
||||
}
|
||||
@@ -1,771 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"description": "Defines a dev container",
|
||||
"allowComments": true,
|
||||
"allowTrailingCommas": false,
|
||||
"definitions": {
|
||||
"devContainerCommon": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "The JSON schema of the `devcontainer.json` file."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A name for the dev container which can be displayed to the user."
|
||||
},
|
||||
"features": {
|
||||
"type": "object",
|
||||
"description": "Features to add to the dev container.",
|
||||
"properties": {
|
||||
"fish": {
|
||||
"deprecated": true,
|
||||
"deprecationMessage": "Legacy feature not supported. Please check https://containers.dev/features for replacements."
|
||||
},
|
||||
"maven": {
|
||||
"deprecated": true,
|
||||
"deprecationMessage": "Legacy feature will be removed in the future. Please check https://containers.dev/features for replacements. E.g., `ghcr.io/devcontainers/features/java` has an option to install Maven."
|
||||
},
|
||||
"gradle": {
|
||||
"deprecated": true,
|
||||
"deprecationMessage": "Legacy feature will be removed in the future. Please check https://containers.dev/features for replacements. E.g., `ghcr.io/devcontainers/features/java` has an option to install Gradle."
|
||||
},
|
||||
"homebrew": {
|
||||
"deprecated": true,
|
||||
"deprecationMessage": "Legacy feature not supported. Please check https://containers.dev/features for replacements."
|
||||
},
|
||||
"jupyterlab": {
|
||||
"deprecated": true,
|
||||
"deprecationMessage": "Legacy feature will be removed in the future. Please check https://containers.dev/features for replacements. E.g., `ghcr.io/devcontainers/features/python` has an option to install JupyterLab."
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"overrideFeatureInstallOrder": {
|
||||
"type": "array",
|
||||
"description": "Array consisting of the Feature id (without the semantic version) of Features in the order the user wants them to be installed.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"description": "Recommended secrets for this dev container. Recommendations are provided as environment variable keys with optional metadata.",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z_][a-zA-Z0-9_]*$": {
|
||||
"type": "object",
|
||||
"description": "Environment variable keys following unix-style naming conventions. eg: ^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A description of the secret."
|
||||
},
|
||||
"documentationUrl": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "A URL to documentation about the secret."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"forwardPorts": {
|
||||
"type": "array",
|
||||
"description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer",
|
||||
"maximum": 65535,
|
||||
"minimum": 0
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^([a-z0-9-]+):(\\d{1,5})$"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"portsAttributes": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"(^\\d+(-\\d+)?$)|(.+)": {
|
||||
"type": "object",
|
||||
"description": "A port, range of ports (ex. \"40000-55000\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression.",
|
||||
"properties": {
|
||||
"onAutoForward": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"notify",
|
||||
"openBrowser",
|
||||
"openBrowserOnce",
|
||||
"openPreview",
|
||||
"silent",
|
||||
"ignore"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Shows a notification when a port is automatically forwarded.",
|
||||
"Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.",
|
||||
"Opens the browser when the port is automatically forwarded, but only the first time the port is forward during a session. Depending on your settings, this could open an embedded browser.",
|
||||
"Opens a preview in the same window when the port is automatically forwarded.",
|
||||
"Shows no notification and takes no action when this port is automatically forwarded.",
|
||||
"This port will not be automatically forwarded."
|
||||
],
|
||||
"description": "Defines the action that occurs when the port is discovered for automatic forwarding",
|
||||
"default": "notify"
|
||||
},
|
||||
"elevateIfNeeded": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.",
|
||||
"default": false
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Label that will be shown in the UI for this port.",
|
||||
"default": "Application"
|
||||
},
|
||||
"requireLocalPort": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.",
|
||||
"default": false
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"description": "The protocol to use when forwarding this port."
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"label": "Application",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
}
|
||||
},
|
||||
"markdownDescription": "Set default properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Application\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n},\n\".+\\\\/server.js\": {\n \"onAutoForward\": \"openPreview\"\n}\n```",
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"body": {
|
||||
"${1:3000}": {
|
||||
"label": "${2:Application}",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"otherPortsAttributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"onAutoForward": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"notify",
|
||||
"openBrowser",
|
||||
"openPreview",
|
||||
"silent",
|
||||
"ignore"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Shows a notification when a port is automatically forwarded.",
|
||||
"Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.",
|
||||
"Opens a preview in the same window when the port is automatically forwarded.",
|
||||
"Shows no notification and takes no action when this port is automatically forwarded.",
|
||||
"This port will not be automatically forwarded."
|
||||
],
|
||||
"description": "Defines the action that occurs when the port is discovered for automatic forwarding",
|
||||
"default": "notify"
|
||||
},
|
||||
"elevateIfNeeded": {
|
||||
"type": "boolean",
|
||||
"description": "Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.",
|
||||
"default": false
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Label that will be shown in the UI for this port.",
|
||||
"default": "Application"
|
||||
},
|
||||
"requireLocalPort": {
|
||||
"type": "boolean",
|
||||
"markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.",
|
||||
"default": false
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"description": "The protocol to use when forwarding this port."
|
||||
}
|
||||
},
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"body": {
|
||||
"onAutoForward": "ignore"
|
||||
}
|
||||
}
|
||||
],
|
||||
"markdownDescription": "Set default properties that are applied to all ports that don't get properties from the setting `remote.portsAttributes`. For example:\n\n```\n{\n \"onAutoForward\": \"ignore\"\n}\n```",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"updateRemoteUserUID": {
|
||||
"type": "boolean",
|
||||
"description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder."
|
||||
},
|
||||
"containerEnv": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Container environment variables."
|
||||
},
|
||||
"containerUser": {
|
||||
"type": "string",
|
||||
"description": "The user the container will be started with. The default is the user on the Docker image."
|
||||
},
|
||||
"mounts": {
|
||||
"type": "array",
|
||||
"description": "Mount points to set up when creating the container. See Docker's documentation for the --mount option for the supported syntax.",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Mount"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"type": "boolean",
|
||||
"description": "Passes the --init flag when creating the dev container."
|
||||
},
|
||||
"privileged": {
|
||||
"type": "boolean",
|
||||
"description": "Passes the --privileged flag when creating the dev container."
|
||||
},
|
||||
"capAdd": {
|
||||
"type": "array",
|
||||
"description": "Passes docker capabilities to include when creating the dev container.",
|
||||
"examples": [
|
||||
"SYS_PTRACE"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"securityOpt": {
|
||||
"type": "array",
|
||||
"description": "Passes docker security options to include when creating the dev container.",
|
||||
"examples": [
|
||||
"seccomp=unconfined"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"remoteEnv": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"description": "Remote environment variables to set for processes spawned in the container including lifecycle scripts and any remote editor/IDE server process."
|
||||
},
|
||||
"remoteUser": {
|
||||
"type": "string",
|
||||
"description": "The username to use for spawning processes in the container including lifecycle scripts and any remote editor/IDE server process. The default is the same user as the container."
|
||||
},
|
||||
"initializeCommand": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"object"
|
||||
],
|
||||
"description": "A command to run locally (i.e Your host machine, cloud VM) before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"onCreateCommand": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"object"
|
||||
],
|
||||
"description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateContentCommand": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"object"
|
||||
],
|
||||
"description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"object"
|
||||
],
|
||||
"description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postStartCommand": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"object"
|
||||
],
|
||||
"description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postAttachCommand": {
|
||||
"type": [
|
||||
"string",
|
||||
"array",
|
||||
"object"
|
||||
],
|
||||
"description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"waitFor": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"initializeCommand",
|
||||
"onCreateCommand",
|
||||
"updateContentCommand",
|
||||
"postCreateCommand",
|
||||
"postStartCommand"
|
||||
],
|
||||
"description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"."
|
||||
},
|
||||
"userEnvProbe": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"loginShell",
|
||||
"loginInteractiveShell",
|
||||
"interactiveShell"
|
||||
],
|
||||
"description": "User environment probe to run. The default is \"loginInteractiveShell\"."
|
||||
},
|
||||
"hostRequirements": {
|
||||
"type": "object",
|
||||
"description": "Host hardware requirements.",
|
||||
"properties": {
|
||||
"cpus": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Number of required CPUs."
|
||||
},
|
||||
"memory": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+([tgmk]b)?$",
|
||||
"description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb."
|
||||
},
|
||||
"storage": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+([tgmk]b)?$",
|
||||
"description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb."
|
||||
},
|
||||
"gpu": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": [
|
||||
"boolean",
|
||||
"string"
|
||||
],
|
||||
"enum": [
|
||||
true,
|
||||
false,
|
||||
"optional"
|
||||
],
|
||||
"description": "Indicates whether a GPU is required. The string \"optional\" indicates that a GPU is optional. An object value can be used to configure more detailed requirements."
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cores": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "Number of required cores."
|
||||
},
|
||||
"memory": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+([tgmk]b)?$",
|
||||
"description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb."
|
||||
}
|
||||
},
|
||||
"description": "Indicates whether a GPU is required. The string \"optional\" indicates that a GPU is optional. An object value can be used to configure more detailed requirements.",
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"unevaluatedProperties": false
|
||||
},
|
||||
"customizations": {
|
||||
"type": "object",
|
||||
"description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations."
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"nonComposeBase": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"appPort": {
|
||||
"type": [
|
||||
"integer",
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"description": "Application ports that are exposed by the container. This can be a single port or an array of ports. Each port can be a number or a string. A number is mapped to the same port on the host. A string is passed to Docker unchanged and can be used to map ports differently, e.g. \"8000:8010\".",
|
||||
"items": {
|
||||
"type": [
|
||||
"integer",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
},
|
||||
"runArgs": {
|
||||
"type": "array",
|
||||
"description": "The arguments required when starting in the container.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"shutdownAction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"stopContainer"
|
||||
],
|
||||
"description": "Action to take when the user disconnects from the container in their editor. The default is to stop the container."
|
||||
},
|
||||
"overrideCommand": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to overwrite the command specified in the image. The default is true."
|
||||
},
|
||||
"workspaceFolder": {
|
||||
"type": "string",
|
||||
"description": "The path of the workspace folder inside the container."
|
||||
},
|
||||
"workspaceMount": {
|
||||
"type": "string",
|
||||
"description": "The --mount parameter for docker run. The default is to mount the project folder at /workspaces/$project."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dockerfileContainer": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"build": {
|
||||
"type": "object",
|
||||
"description": "Docker build-related options.",
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dockerfile": {
|
||||
"type": "string",
|
||||
"description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file."
|
||||
},
|
||||
"context": {
|
||||
"type": "string",
|
||||
"description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dockerfile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/buildOptions"
|
||||
}
|
||||
],
|
||||
"unevaluatedProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dockerFile": {
|
||||
"type": "string",
|
||||
"description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file."
|
||||
},
|
||||
"context": {
|
||||
"type": "string",
|
||||
"description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dockerFile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"build": {
|
||||
"description": "Docker build-related options.",
|
||||
"$ref": "#/definitions/buildOptions"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"buildOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Target stage in a multi-stage build."
|
||||
},
|
||||
"args": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"description": "Build arguments."
|
||||
},
|
||||
"cacheFrom": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"description": "The image to consider as a cache. Use an array to specify multiple images.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"description": "Additional arguments passed to the build command.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageContainer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"image": {
|
||||
"type": "string",
|
||||
"description": "The docker image that will be used to create the container."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"image"
|
||||
]
|
||||
},
|
||||
"composeContainer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dockerComposeFile": {
|
||||
"type": [
|
||||
"string",
|
||||
"array"
|
||||
],
|
||||
"description": "The name of the docker-compose file(s) used to start the services.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "string",
|
||||
"description": "The service you want to work on. This is considered the primary container for your dev environment which your editor will connect to."
|
||||
},
|
||||
"runServices": {
|
||||
"type": "array",
|
||||
"description": "An array of services that should be started and stopped.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"workspaceFolder": {
|
||||
"type": "string",
|
||||
"description": "The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml."
|
||||
},
|
||||
"shutdownAction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"stopCompose"
|
||||
],
|
||||
"description": "Action to take when the user disconnects from the primary container in their editor. The default is to stop all of the compose containers."
|
||||
},
|
||||
"overrideCommand": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to overwrite the command specified in the image. The default is false."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"dockerComposeFile",
|
||||
"service",
|
||||
"workspaceFolder"
|
||||
]
|
||||
},
|
||||
"Mount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"bind",
|
||||
"volume"
|
||||
],
|
||||
"description": "Mount type."
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Mount source."
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Mount target."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"target"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/dockerfileContainer"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/imageContainer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/nonComposeBase"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/composeContainer"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/devContainerCommon"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/devContainerCommon",
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"unevaluatedProperties": false
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Package dcspec contains an automatically generated Devcontainer
|
||||
// specification.
|
||||
package dcspec
|
||||
|
||||
//go:generate ./gen.sh
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# This script requires quicktype to be installed.
|
||||
# While you can install it using npm, we have it in our devDependencies
|
||||
# in ${PROJECT_ROOT}/package.json.
|
||||
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||
if ! pnpm list | grep quicktype &>/dev/null; then
|
||||
echo "quicktype is required to run this script!"
|
||||
echo "Ensure that it is present in the devDependencies of ${PROJECT_ROOT}/package.json and then run pnpm install."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEST_FILENAME="dcspec_gen.go"
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
DEST_PATH="${SCRIPT_DIR}/${DEST_FILENAME}"
|
||||
|
||||
# Location of the JSON schema for the devcontainer specification.
|
||||
SCHEMA_SRC="https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.base.schema.json"
|
||||
SCHEMA_DEST="${SCRIPT_DIR}/devContainer.base.schema.json"
|
||||
|
||||
UPDATE_SCHEMA="${UPDATE_SCHEMA:-false}"
|
||||
if [[ "${UPDATE_SCHEMA}" = true || ! -f "${SCHEMA_DEST}" ]]; then
|
||||
# Download the latest schema.
|
||||
echo "Updating schema..."
|
||||
curl --fail --silent --show-error --location --output "${SCHEMA_DEST}" "${SCHEMA_SRC}"
|
||||
else
|
||||
echo "Using existing schema..."
|
||||
fi
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rfv "$TMPDIR"' EXIT
|
||||
|
||||
show_stderr=1
|
||||
exec 3>&2
|
||||
if [[ " $* " == *" --quiet "* ]] || [[ ${DCSPEC_QUIET:-false} == "true" ]]; then
|
||||
# Redirect stderr to log because quicktype can't infer all types and
|
||||
# we don't care right now.
|
||||
show_stderr=0
|
||||
exec 2>"${TMPDIR}/stderr.log"
|
||||
fi
|
||||
|
||||
if ! pnpm exec quicktype \
|
||||
--src-lang schema \
|
||||
--lang go \
|
||||
--just-types-and-package \
|
||||
--top-level "DevContainer" \
|
||||
--out "${TMPDIR}/${DEST_FILENAME}" \
|
||||
--package "dcspec" \
|
||||
"${SCHEMA_DEST}"; then
|
||||
echo "quicktype failed to generate Go code." >&3
|
||||
if [[ "${show_stderr}" -eq 1 ]]; then
|
||||
cat "${TMPDIR}/stderr.log" >&3
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${show_stderr}" -eq 0 ]]; then
|
||||
# Restore stderr.
|
||||
exec 2>&3
|
||||
fi
|
||||
exec 3>&-
|
||||
|
||||
# Format the generated code.
|
||||
go run mvdan.cc/gofumpt@v0.4.0 -w -l "${TMPDIR}/${DEST_FILENAME}"
|
||||
|
||||
# Add a header so that Go recognizes this as a generated file.
|
||||
if grep -q -- "\[-i extension\]" < <(sed -h 2>&1); then
|
||||
# darwin sed
|
||||
sed -i '' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
else
|
||||
sed -i'' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
fi
|
||||
|
||||
mv -v "${TMPDIR}/${DEST_FILENAME}" "${DEST_PATH}"
|
||||
@@ -1,98 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
const devcontainerUpScriptTemplate = `
|
||||
if ! which devcontainer > /dev/null 2>&1; then
|
||||
echo "ERROR: Unable to start devcontainer, @devcontainers/cli is not installed."
|
||||
exit 1
|
||||
fi
|
||||
devcontainer up %s
|
||||
`
|
||||
|
||||
// ExtractAndInitializeDevcontainerScripts extracts devcontainer scripts from
|
||||
// the given scripts and devcontainers. The devcontainer scripts are removed
|
||||
// from the returned scripts so that they can be run separately.
|
||||
//
|
||||
// Dev Containers have an inherent dependency on start scripts, since they
|
||||
// initialize the workspace (e.g. git clone, npm install, etc). This is
|
||||
// important if e.g. a Coder module to install @devcontainer/cli is used.
|
||||
func ExtractAndInitializeDevcontainerScripts(
|
||||
logger slog.Logger,
|
||||
expandPath func(string) (string, error),
|
||||
devcontainers []codersdk.WorkspaceAgentDevcontainer,
|
||||
scripts []codersdk.WorkspaceAgentScript,
|
||||
) (filteredScripts []codersdk.WorkspaceAgentScript, devcontainerScripts []codersdk.WorkspaceAgentScript) {
|
||||
ScriptLoop:
|
||||
for _, script := range scripts {
|
||||
for _, dc := range devcontainers {
|
||||
// The devcontainer scripts match the devcontainer ID for
|
||||
// identification.
|
||||
if script.ID == dc.ID {
|
||||
dc = expandDevcontainerPaths(logger, expandPath, dc)
|
||||
devcontainerScripts = append(devcontainerScripts, devcontainerStartupScript(dc, script))
|
||||
continue ScriptLoop
|
||||
}
|
||||
}
|
||||
|
||||
filteredScripts = append(filteredScripts, script)
|
||||
}
|
||||
|
||||
return filteredScripts, devcontainerScripts
|
||||
}
|
||||
|
||||
func devcontainerStartupScript(dc codersdk.WorkspaceAgentDevcontainer, script codersdk.WorkspaceAgentScript) codersdk.WorkspaceAgentScript {
|
||||
var args []string
|
||||
args = append(args, fmt.Sprintf("--workspace-folder %q", dc.WorkspaceFolder))
|
||||
if dc.ConfigPath != "" {
|
||||
args = append(args, fmt.Sprintf("--config %q", dc.ConfigPath))
|
||||
}
|
||||
cmd := fmt.Sprintf(devcontainerUpScriptTemplate, strings.Join(args, " "))
|
||||
script.Script = cmd
|
||||
// Disable RunOnStart, scripts have this set so that when devcontainers
|
||||
// have not been enabled, a warning will be surfaced in the agent logs.
|
||||
script.RunOnStart = false
|
||||
return script
|
||||
}
|
||||
|
||||
func expandDevcontainerPaths(logger slog.Logger, expandPath func(string) (string, error), dc codersdk.WorkspaceAgentDevcontainer) codersdk.WorkspaceAgentDevcontainer {
|
||||
logger = logger.With(slog.F("devcontainer", dc.Name), slog.F("workspace_folder", dc.WorkspaceFolder), slog.F("config_path", dc.ConfigPath))
|
||||
|
||||
if wf, err := expandPath(dc.WorkspaceFolder); err != nil {
|
||||
logger.Warn(context.Background(), "expand devcontainer workspace folder failed", slog.Error(err))
|
||||
} else {
|
||||
dc.WorkspaceFolder = wf
|
||||
}
|
||||
if dc.ConfigPath != "" {
|
||||
// Let expandPath handle home directory, otherwise assume relative to
|
||||
// workspace folder or absolute.
|
||||
if dc.ConfigPath[0] == '~' {
|
||||
if cp, err := expandPath(dc.ConfigPath); err != nil {
|
||||
logger.Warn(context.Background(), "expand devcontainer config path failed", slog.Error(err))
|
||||
} else {
|
||||
dc.ConfigPath = cp
|
||||
}
|
||||
} else {
|
||||
dc.ConfigPath = relativePathToAbs(dc.WorkspaceFolder, dc.ConfigPath)
|
||||
}
|
||||
}
|
||||
return dc
|
||||
}
|
||||
|
||||
func relativePathToAbs(workdir, path string) string {
|
||||
path = os.ExpandEnv(path)
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(workdir, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
package agentcontainers_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestExtractAndInitializeDevcontainerScripts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scriptIDs := []uuid.UUID{uuid.New(), uuid.New()}
|
||||
devcontainerIDs := []uuid.UUID{uuid.New(), uuid.New()}
|
||||
|
||||
type args struct {
|
||||
expandPath func(string) (string, error)
|
||||
devcontainers []codersdk.WorkspaceAgentDevcontainer
|
||||
scripts []codersdk.WorkspaceAgentScript
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantFilteredScripts []codersdk.WorkspaceAgentScript
|
||||
wantDevcontainerScripts []codersdk.WorkspaceAgentScript
|
||||
|
||||
skipOnWindowsDueToPathSeparator bool
|
||||
}{
|
||||
{
|
||||
name: "no scripts",
|
||||
args: args{
|
||||
expandPath: nil,
|
||||
devcontainers: nil,
|
||||
scripts: nil,
|
||||
},
|
||||
wantFilteredScripts: nil,
|
||||
wantDevcontainerScripts: nil,
|
||||
},
|
||||
{
|
||||
name: "no devcontainers",
|
||||
args: args{
|
||||
expandPath: nil,
|
||||
devcontainers: nil,
|
||||
scripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: scriptIDs[0]},
|
||||
{ID: scriptIDs[1]},
|
||||
},
|
||||
},
|
||||
wantFilteredScripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: scriptIDs[0]},
|
||||
{ID: scriptIDs[1]},
|
||||
},
|
||||
wantDevcontainerScripts: nil,
|
||||
},
|
||||
{
|
||||
name: "no scripts match devcontainers",
|
||||
args: args{
|
||||
expandPath: nil,
|
||||
devcontainers: []codersdk.WorkspaceAgentDevcontainer{
|
||||
{ID: devcontainerIDs[0]},
|
||||
{ID: devcontainerIDs[1]},
|
||||
},
|
||||
scripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: scriptIDs[0]},
|
||||
{ID: scriptIDs[1]},
|
||||
},
|
||||
},
|
||||
wantFilteredScripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: scriptIDs[0]},
|
||||
{ID: scriptIDs[1]},
|
||||
},
|
||||
wantDevcontainerScripts: nil,
|
||||
},
|
||||
{
|
||||
name: "scripts match devcontainers and sets RunOnStart=false",
|
||||
args: args{
|
||||
expandPath: nil,
|
||||
devcontainers: []codersdk.WorkspaceAgentDevcontainer{
|
||||
{ID: devcontainerIDs[0], WorkspaceFolder: "workspace1"},
|
||||
{ID: devcontainerIDs[1], WorkspaceFolder: "workspace2"},
|
||||
},
|
||||
scripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: scriptIDs[0], RunOnStart: true},
|
||||
{ID: scriptIDs[1], RunOnStart: true},
|
||||
{ID: devcontainerIDs[0], RunOnStart: true},
|
||||
{ID: devcontainerIDs[1], RunOnStart: true},
|
||||
},
|
||||
},
|
||||
wantFilteredScripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: scriptIDs[0], RunOnStart: true},
|
||||
{ID: scriptIDs[1], RunOnStart: true},
|
||||
},
|
||||
wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{
|
||||
{
|
||||
ID: devcontainerIDs[0],
|
||||
Script: "devcontainer up --workspace-folder \"workspace1\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
{
|
||||
ID: devcontainerIDs[1],
|
||||
Script: "devcontainer up --workspace-folder \"workspace2\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "scripts match devcontainers with config path",
|
||||
args: args{
|
||||
expandPath: nil,
|
||||
devcontainers: []codersdk.WorkspaceAgentDevcontainer{
|
||||
{
|
||||
ID: devcontainerIDs[0],
|
||||
WorkspaceFolder: "workspace1",
|
||||
ConfigPath: "config1",
|
||||
},
|
||||
{
|
||||
ID: devcontainerIDs[1],
|
||||
WorkspaceFolder: "workspace2",
|
||||
ConfigPath: "config2",
|
||||
},
|
||||
},
|
||||
scripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: devcontainerIDs[0]},
|
||||
{ID: devcontainerIDs[1]},
|
||||
},
|
||||
},
|
||||
wantFilteredScripts: []codersdk.WorkspaceAgentScript{},
|
||||
wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{
|
||||
{
|
||||
ID: devcontainerIDs[0],
|
||||
Script: "devcontainer up --workspace-folder \"workspace1\" --config \"workspace1/config1\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
{
|
||||
ID: devcontainerIDs[1],
|
||||
Script: "devcontainer up --workspace-folder \"workspace2\" --config \"workspace2/config2\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
},
|
||||
skipOnWindowsDueToPathSeparator: true,
|
||||
},
|
||||
{
|
||||
name: "scripts match devcontainers with expand path",
|
||||
args: args{
|
||||
expandPath: func(s string) (string, error) {
|
||||
return "/home/" + s, nil
|
||||
},
|
||||
devcontainers: []codersdk.WorkspaceAgentDevcontainer{
|
||||
{
|
||||
ID: devcontainerIDs[0],
|
||||
WorkspaceFolder: "workspace1",
|
||||
ConfigPath: "config1",
|
||||
},
|
||||
{
|
||||
ID: devcontainerIDs[1],
|
||||
WorkspaceFolder: "workspace2",
|
||||
ConfigPath: "config2",
|
||||
},
|
||||
},
|
||||
scripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: devcontainerIDs[0], RunOnStart: true},
|
||||
{ID: devcontainerIDs[1], RunOnStart: true},
|
||||
},
|
||||
},
|
||||
wantFilteredScripts: []codersdk.WorkspaceAgentScript{},
|
||||
wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{
|
||||
{
|
||||
ID: devcontainerIDs[0],
|
||||
Script: "devcontainer up --workspace-folder \"/home/workspace1\" --config \"/home/workspace1/config1\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
{
|
||||
ID: devcontainerIDs[1],
|
||||
Script: "devcontainer up --workspace-folder \"/home/workspace2\" --config \"/home/workspace2/config2\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
},
|
||||
skipOnWindowsDueToPathSeparator: true,
|
||||
},
|
||||
{
|
||||
name: "expand config path when ~",
|
||||
args: args{
|
||||
expandPath: func(s string) (string, error) {
|
||||
s = strings.Replace(s, "~/", "", 1)
|
||||
if filepath.IsAbs(s) {
|
||||
return s, nil
|
||||
}
|
||||
return "/home/" + s, nil
|
||||
},
|
||||
devcontainers: []codersdk.WorkspaceAgentDevcontainer{
|
||||
{
|
||||
ID: devcontainerIDs[0],
|
||||
WorkspaceFolder: "workspace1",
|
||||
ConfigPath: "~/config1",
|
||||
},
|
||||
{
|
||||
ID: devcontainerIDs[1],
|
||||
WorkspaceFolder: "workspace2",
|
||||
ConfigPath: "/config2",
|
||||
},
|
||||
},
|
||||
scripts: []codersdk.WorkspaceAgentScript{
|
||||
{ID: devcontainerIDs[0], RunOnStart: true},
|
||||
{ID: devcontainerIDs[1], RunOnStart: true},
|
||||
},
|
||||
},
|
||||
wantFilteredScripts: []codersdk.WorkspaceAgentScript{},
|
||||
wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{
|
||||
{
|
||||
ID: devcontainerIDs[0],
|
||||
Script: "devcontainer up --workspace-folder \"/home/workspace1\" --config \"/home/config1\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
{
|
||||
ID: devcontainerIDs[1],
|
||||
Script: "devcontainer up --workspace-folder \"/home/workspace2\" --config \"/config2\"",
|
||||
RunOnStart: false,
|
||||
},
|
||||
},
|
||||
skipOnWindowsDueToPathSeparator: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if tt.skipOnWindowsDueToPathSeparator && filepath.Separator == '\\' {
|
||||
t.Skip("Skipping test on Windows due to path separator difference.")
|
||||
}
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
if tt.args.expandPath == nil {
|
||||
tt.args.expandPath = func(s string) (string, error) {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
gotFilteredScripts, gotDevcontainerScripts := agentcontainers.ExtractAndInitializeDevcontainerScripts(
|
||||
logger,
|
||||
tt.args.expandPath,
|
||||
tt.args.devcontainers,
|
||||
tt.args.scripts,
|
||||
)
|
||||
|
||||
if diff := cmp.Diff(tt.wantFilteredScripts, gotFilteredScripts, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("ExtractAndInitializeDevcontainerScripts() gotFilteredScripts mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// Preprocess the devcontainer scripts to remove scripting part.
|
||||
for i := range gotDevcontainerScripts {
|
||||
gotDevcontainerScripts[i].Script = textGrep("devcontainer up", gotDevcontainerScripts[i].Script)
|
||||
require.NotEmpty(t, gotDevcontainerScripts[i].Script, "devcontainer up script not found")
|
||||
}
|
||||
if diff := cmp.Diff(tt.wantDevcontainerScripts, gotDevcontainerScripts); diff != "" {
|
||||
t.Errorf("ExtractAndInitializeDevcontainerScripts() gotDevcontainerScripts mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// textGrep returns matching lines from multiline string.
|
||||
func textGrep(want, got string) (filtered string) {
|
||||
var lines []string
|
||||
for _, line := range strings.Split(got, "\n") {
|
||||
if strings.Contains(line, want) {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a",
|
||||
"Created": "2025-03-11T17:58:43.522505027Z",
|
||||
"Path": "sleep",
|
||||
"Args": [
|
||||
"infinity"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 644296,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:58:43.569966691Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a/fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a-json.log",
|
||||
"Name": "/silly_beaver",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"/tmp/test/a:/var/coder/a:ro",
|
||||
"/tmp/test/b:/var/coder/b"
|
||||
],
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "fdc75ebefdc0243c0fce959e7685931691ac7aede278664a0e2c23af8a1e8d6a",
|
||||
"LowerDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/c1519be93f8e138757310f6ed8c3946a524cdae2580ad8579913d19c3fe9ffd2/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/tmp/test/a",
|
||||
"Destination": "/var/coder/a",
|
||||
"Mode": "ro",
|
||||
"RW": false,
|
||||
"Propagation": "rprivate"
|
||||
},
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/tmp/test/b",
|
||||
"Destination": "/var/coder/b",
|
||||
"Mode": "",
|
||||
"RW": true,
|
||||
"Propagation": "rprivate"
|
||||
}
|
||||
],
|
||||
"Config": {
|
||||
"Hostname": "fdc75ebefdc0",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sleep",
|
||||
"infinity"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [],
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "46f98b32002740b63709e3ebf87c78efe652adfaa8753b85d79b814f26d88107",
|
||||
"SandboxKey": "/var/run/docker/netns/46f98b320027",
|
||||
"Ports": {},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "356e429f15e354dd23250c7a3516aecf1a2afe9d58ea1dc2e97e33a75ac346a8",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "22:2c:26:d9:da:83",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "22:2c:26:d9:da:83",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "356e429f15e354dd23250c7a3516aecf1a2afe9d58ea1dc2e97e33a75ac346a8",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,222 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea",
|
||||
"Created": "2025-03-11T17:57:08.862545133Z",
|
||||
"Path": "sleep",
|
||||
"Args": [
|
||||
"infinity"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 640137,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:57:08.909898821Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea/3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea-json.log",
|
||||
"Name": "/boring_ellis",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {
|
||||
"23456/tcp": [
|
||||
{
|
||||
"HostIp": "",
|
||||
"HostPort": "12345"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "3090de8b72b1224758a94a11b827c82ba2b09c45524f1263dc4a2d83e19625ea",
|
||||
"LowerDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/e9f2dda207bde1f4b277f973457107d62cff259881901adcd9bcccfea9a92231/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "3090de8b72b1",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"23456/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sleep",
|
||||
"infinity"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [],
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "ebcd8b749b4c719f90d80605c352b7aa508e4c61d9dcd2919654f18f17eb2840",
|
||||
"SandboxKey": "/var/run/docker/netns/ebcd8b749b4c",
|
||||
"Ports": {
|
||||
"23456/tcp": [
|
||||
{
|
||||
"HostIp": "0.0.0.0",
|
||||
"HostPort": "12345"
|
||||
},
|
||||
{
|
||||
"HostIp": "::",
|
||||
"HostPort": "12345"
|
||||
}
|
||||
]
|
||||
},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "465824b3cc6bdd2b307e9c614815fd458b1baac113dee889c3620f0cac3183fa",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "52:b6:f6:7b:4b:5b",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "52:b6:f6:7b:4b:5b",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "465824b3cc6bdd2b307e9c614815fd458b1baac113dee889c3620f0cac3183fa",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,204 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f",
|
||||
"Created": "2025-03-11T20:03:28.071706536Z",
|
||||
"Path": "sleep",
|
||||
"Args": [
|
||||
"infinity"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 913862,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T20:03:28.123599065Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f/bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f-json.log",
|
||||
"Name": "/fervent_bardeen",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "bd8818e670230fc6f36145b21cf8d6d35580355662aa4d9fe5ae1b188a4c905f",
|
||||
"LowerDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/55fc45976c381040c7d261c198333e6331889c51afe1500e2e7837c189a1b794/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "bd8818e67023",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sleep",
|
||||
"infinity"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"baz": "zap",
|
||||
"foo": "bar"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "24faa8b9aaa58c651deca0d85a3f7bcc6c3e5e1a24b6369211f736d6e82f8ab0",
|
||||
"SandboxKey": "/var/run/docker/netns/24faa8b9aaa5",
|
||||
"Ports": {},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "c686f97d772d75c8ceed9285e06c1f671b71d4775d5513f93f26358c0f0b4671",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "96:88:4e:3b:11:44",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "96:88:4e:3b:11:44",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "c686f97d772d75c8ceed9285e06c1f671b71d4775d5513f93f26358c0f0b4671",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,222 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2",
|
||||
"Created": "2025-03-11T17:56:34.842164541Z",
|
||||
"Path": "sleep",
|
||||
"Args": [
|
||||
"infinity"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 638449,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:56:34.894488648Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2/4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2-json.log",
|
||||
"Name": "/modest_varahamihira",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {
|
||||
"12345/tcp": [
|
||||
{
|
||||
"HostIp": "",
|
||||
"HostPort": "12345"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "4eac5ce199d27b2329d0ff0ce1a6fc595612ced48eba3669aadb6c57ebef3fa2",
|
||||
"LowerDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/35deac62dd3f610275aaf145d091aaa487f73a3c99de5a90df8ab871c969bc0b/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "4eac5ce199d2",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"12345/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sleep",
|
||||
"infinity"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [],
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "5e966e97ba02013054e0ef15ef87f8629f359ad882fad4c57b33c768ad9b90dc",
|
||||
"SandboxKey": "/var/run/docker/netns/5e966e97ba02",
|
||||
"Ports": {
|
||||
"12345/tcp": [
|
||||
{
|
||||
"HostIp": "0.0.0.0",
|
||||
"HostPort": "12345"
|
||||
},
|
||||
{
|
||||
"HostIp": "::",
|
||||
"HostPort": "12345"
|
||||
}
|
||||
]
|
||||
},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "f9e1896fc0ef48f3ea9aff3b4e98bc4291ba246412178331345f7b0745cccba9",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "be:a6:89:39:7e:b0",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "be:a6:89:39:7e:b0",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "f9e1896fc0ef48f3ea9aff3b4e98bc4291ba246412178331345f7b0745cccba9",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,51 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "a",
|
||||
"Created": "2025-03-11T17:56:34.842164541Z",
|
||||
"State": {
|
||||
"Running": true,
|
||||
"ExitCode": 0,
|
||||
"Error": ""
|
||||
},
|
||||
"Name": "/a",
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Image": "debian:bookworm",
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Ports": {
|
||||
"8001/tcp": [
|
||||
{
|
||||
"HostIp": "0.0.0.0",
|
||||
"HostPort": "8000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Id": "b",
|
||||
"Created": "2025-03-11T17:56:34.842164541Z",
|
||||
"State": {
|
||||
"Running": true,
|
||||
"ExitCode": 0,
|
||||
"Error": ""
|
||||
},
|
||||
"Name": "/b",
|
||||
"Config": {
|
||||
"Image": "debian:bookworm",
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Ports": {
|
||||
"8001/tcp": [
|
||||
{
|
||||
"HostIp": "::",
|
||||
"HostPort": "8000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,201 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286",
|
||||
"Created": "2025-03-11T17:55:58.091280203Z",
|
||||
"Path": "sleep",
|
||||
"Args": [
|
||||
"infinity"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 636855,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:55:58.142417459Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286/6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286-json.log",
|
||||
"Name": "/eloquent_kowalevski",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "6b539b8c60f5230b8b0fde2502cd2332d31c0d526a3e6eb6eef1cc39439b3286",
|
||||
"LowerDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/4093560d7757c088e24060e5ff6f32807d8e733008c42b8af7057fe4fe6f56ba/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "6b539b8c60f5",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sleep",
|
||||
"infinity"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [],
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "08f2f3218a6d63ae149ab77672659d96b88bca350e85889240579ecb427e8011",
|
||||
"SandboxKey": "/var/run/docker/netns/08f2f3218a6d",
|
||||
"Ports": {},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "f83bd20711df6d6ff7e2f44f4b5799636cd94596ae25ffe507a70f424073532c",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "f6:84:26:7a:10:5b",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "f6:84:26:7a:10:5b",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "f83bd20711df6d6ff7e2f44f4b5799636cd94596ae25ffe507a70f424073532c",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,214 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e",
|
||||
"Created": "2025-03-11T17:59:42.039484134Z",
|
||||
"Path": "sleep",
|
||||
"Args": [
|
||||
"infinity"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 646777,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:59:42.081315917Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e/b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e-json.log",
|
||||
"Name": "/upbeat_carver",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"testvol:/testvol"
|
||||
],
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "b3688d98c007f53402a55e46d803f2f3ba9181d8e3f71a2eb19b392cf0377b4e",
|
||||
"LowerDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/d71790d2558bf17d7535451094e332780638a4e92697c021176f3447fc4c50f4/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "testvol",
|
||||
"Source": "/var/lib/docker/volumes/testvol/_data",
|
||||
"Destination": "/testvol",
|
||||
"Driver": "local",
|
||||
"Mode": "z",
|
||||
"RW": true,
|
||||
"Propagation": ""
|
||||
}
|
||||
],
|
||||
"Config": {
|
||||
"Hostname": "b3688d98c007",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sleep",
|
||||
"infinity"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [],
|
||||
"OnBuild": null,
|
||||
"Labels": {}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "e617ea865af5690d06c25df1c9a0154b98b4da6bbb9e0afae3b80ad29902538a",
|
||||
"SandboxKey": "/var/run/docker/netns/e617ea865af5",
|
||||
"Ports": {},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "1a7bb5bbe4af0674476c95c5d1c913348bc82a5f01fd1c1b394afc44d1cf5a49",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "4a:d8:a5:47:1c:54",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "4a:d8:a5:47:1c:54",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "1a7bb5bbe4af0674476c95c5d1c913348bc82a5f01fd1c1b394afc44d1cf5a49",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,230 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3",
|
||||
"Created": "2025-03-11T17:02:42.613747761Z",
|
||||
"Path": "/bin/sh",
|
||||
"Args": [
|
||||
"-c",
|
||||
"echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done",
|
||||
"-"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 526198,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:02:42.658905789Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3/52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3-json.log",
|
||||
"Name": "/suspicious_margulis",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {
|
||||
"8080/tcp": [
|
||||
{
|
||||
"HostIp": "",
|
||||
"HostPort": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "52d23691f4b954d083f117358ea763e20f69af584e1c08f479c5752629ee0be3",
|
||||
"LowerDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/e204eab11c98b3cacc18d5a0e7290c0c286a96d918c31e5c2fed4124132eec4f/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "52d23691f4b9",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"ExposedPorts": {
|
||||
"8080/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"-c",
|
||||
"echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done",
|
||||
"-"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/bin/sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_appport.json",
|
||||
"devcontainer.metadata": "[]"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "e4fa65f769e331c72e27f43af2d65073efca638fd413b7c57f763ee9ebf69020",
|
||||
"SandboxKey": "/var/run/docker/netns/e4fa65f769e3",
|
||||
"Ports": {
|
||||
"8080/tcp": [
|
||||
{
|
||||
"HostIp": "0.0.0.0",
|
||||
"HostPort": "32768"
|
||||
},
|
||||
{
|
||||
"HostIp": "::",
|
||||
"HostPort": "32768"
|
||||
}
|
||||
]
|
||||
},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "14531bbbb26052456a4509e6d23753de45096ca8355ac11684c631d1656248ad",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "36:88:48:04:4e:b4",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "36:88:48:04:4e:b4",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "14531bbbb26052456a4509e6d23753de45096ca8355ac11684c631d1656248ad",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,209 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067",
|
||||
"Created": "2025-03-11T17:03:55.022053072Z",
|
||||
"Path": "/bin/sh",
|
||||
"Args": [
|
||||
"-c",
|
||||
"echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done",
|
||||
"-"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 529591,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:03:55.064323762Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067/4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067-json.log",
|
||||
"Name": "/serene_khayyam",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "4a16af2293fb75dc827a6949a3905dd57ea28cc008823218ce24fab1cb66c067",
|
||||
"LowerDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/1974a49367024c771135c80dd6b62ba46cdebfa866e67a5408426c88a30bac3e/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "4a16af2293fb",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"-c",
|
||||
"echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done",
|
||||
"-"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/bin/sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_forwardport.json",
|
||||
"devcontainer.metadata": "[]"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "e1c3bddb359d16c45d6d132561b83205af7809b01ed5cb985a8cb1b416b2ddd5",
|
||||
"SandboxKey": "/var/run/docker/netns/e1c3bddb359d",
|
||||
"Ports": {},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "2899f34f5f8b928619952dc32566d82bc121b033453f72e5de4a743feabc423b",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "3e:94:61:83:1f:58",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "3e:94:61:83:1f:58",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "2899f34f5f8b928619952dc32566d82bc121b033453f72e5de4a743feabc423b",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,209 +0,0 @@
|
||||
[
|
||||
{
|
||||
"Id": "0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed",
|
||||
"Created": "2025-03-11T17:01:05.751972661Z",
|
||||
"Path": "/bin/sh",
|
||||
"Args": [
|
||||
"-c",
|
||||
"echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done",
|
||||
"-"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 521929,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2025-03-11T17:01:06.002539252Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:d4ccddb816ba27eaae22ef3d56175d53f47998e2acb99df1ae0e5b426b28a076",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed/0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed-json.log",
|
||||
"Name": "/optimistic_hopper",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "bridge",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "no",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
108,
|
||||
176
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "private",
|
||||
"Dns": [],
|
||||
"DnsOptions": [],
|
||||
"DnsSearch": [],
|
||||
"ExtraHosts": null,
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 10,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": [],
|
||||
"BlkioDeviceReadBps": [],
|
||||
"BlkioDeviceWriteBps": [],
|
||||
"BlkioDeviceReadIOps": [],
|
||||
"BlkioDeviceWriteIOps": [],
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": [],
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": null,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": [],
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware",
|
||||
"/sys/devices/virtual/powercap"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"ID": "0b2a9fcf5727d9562943ce47d445019f4520e37a2aa7c6d9346d01af4f4f9aed",
|
||||
"LowerDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219-init/diff:/var/lib/docker/overlay2/4b4c37dfbdc0dc01b68d4fb1ddb86109398a2d73555439b874dbd23b87cd5c4b/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/b698fd9f03f25014d4936cdc64ed258342fe685f0dfd8813ed6928dd6de75219/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "0b2a9fcf5727",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"-c",
|
||||
"echo Container started\ntrap \"exit 0\" 15\n\nexec \"$@\"\nwhile sleep 1 & wait $!; do :; done",
|
||||
"-"
|
||||
],
|
||||
"Image": "debian:bookworm",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/bin/sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"devcontainer.config_file": "/home/coder/src/coder/coder/agent/agentcontainers/testdata/devcontainer_simple.json",
|
||||
"devcontainer.metadata": "[]"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "25a29a57c1330e0d0d2342af6e3291ffd3e812aca1a6e3f6a1630e74b73d0fc6",
|
||||
"SandboxKey": "/var/run/docker/netns/25a29a57c133",
|
||||
"Ports": {},
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "5c5ebda526d8fca90e841886ea81b77d7cc97fed56980c2aa89d275b84af7df2",
|
||||
"Gateway": "172.17.0.1",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "32:b6:d9:ab:c3:61",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": null,
|
||||
"MacAddress": "32:b6:d9:ab:c3:61",
|
||||
"DriverOpts": null,
|
||||
"GwPriority": 0,
|
||||
"NetworkID": "c4dd768ab4945e41ad23fe3907c960dac811141592a861cc40038df7086a1ce1",
|
||||
"EndpointID": "5c5ebda526d8fca90e841886ea81b77d7cc97fed56980c2aa89d275b84af7df2",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"DNSNames": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,205 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package agentexec
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/xerrors"
|
||||
"kernel.org/pub/linux/libs/security/libcap/cap"
|
||||
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
)
|
||||
|
||||
// CLI runs the agent-exec command. It should only be called by the cli package.
|
||||
func CLI() error {
|
||||
// We lock the OS thread here to avoid a race condition where the nice priority
|
||||
// we set gets applied to a different thread than the one we exec the provided
|
||||
// command on.
|
||||
runtime.LockOSThread()
|
||||
// Nop on success but we do it anyway in case of an error.
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
var (
|
||||
fs = flag.NewFlagSet("agent-exec", flag.ExitOnError)
|
||||
nice = fs.Int("coder-nice", unset, "")
|
||||
oom = fs.Int("coder-oom", unset, "")
|
||||
)
|
||||
|
||||
if len(os.Args) < 3 {
|
||||
return xerrors.Errorf("malformed command %+v", os.Args)
|
||||
}
|
||||
|
||||
// Parse everything after "coder agent-exec".
|
||||
err := fs.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse flags: %w", err)
|
||||
}
|
||||
|
||||
// Get everything after "coder agent-exec --"
|
||||
args := execArgs(os.Args)
|
||||
if len(args) == 0 {
|
||||
return xerrors.Errorf("no exec command provided %+v", os.Args)
|
||||
}
|
||||
|
||||
if *oom == unset {
|
||||
// If an explicit oom score isn't set, we use the default.
|
||||
*oom, err = defaultOOMScore()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get default oom score: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if *nice == unset {
|
||||
// If an explicit nice score isn't set, we use the default.
|
||||
*nice, err = defaultNiceScore()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get default nice score: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// We drop effective caps prior to setting dumpable so that we limit the
|
||||
// impact of someone attempting to hijack the process (i.e. with a debugger)
|
||||
// to take advantage of the capabilities of the agent process. We encourage
|
||||
// users to set cap_net_admin on the agent binary for improved networking
|
||||
// performance and doing so results in the process having its SET_DUMPABLE
|
||||
// attribute disabled (meaning we cannot adjust the oom score).
|
||||
err = dropEffectiveCaps()
|
||||
if err != nil {
|
||||
printfStdErr("failed to drop effective caps: %v", err)
|
||||
}
|
||||
|
||||
// Set dumpable to 1 so that we can adjust the oom score. If the process
|
||||
// doesn't have capabilities or has an suid/sgid bit set, this is already
|
||||
// set.
|
||||
err = unix.Prctl(unix.PR_SET_DUMPABLE, 1, 0, 0, 0)
|
||||
if err != nil {
|
||||
printfStdErr("failed to set dumpable: %v", err)
|
||||
}
|
||||
|
||||
err = writeOOMScoreAdj(*oom)
|
||||
if err != nil {
|
||||
// We alert the user instead of failing the command since it can be difficult to debug
|
||||
// for a template admin otherwise. It's quite possible (and easy) to set an
|
||||
// inappriopriate value for oom_score_adj.
|
||||
printfStdErr("failed to adjust oom score to %d for cmd %+v: %v", *oom, execArgs(os.Args), err)
|
||||
}
|
||||
|
||||
// Set dumpable back to 0 just to be safe. It's not inherited for execve anyways.
|
||||
err = unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0)
|
||||
if err != nil {
|
||||
printfStdErr("failed to unset dumpable: %v", err)
|
||||
}
|
||||
|
||||
err = unix.Setpriority(unix.PRIO_PROCESS, 0, *nice)
|
||||
if err != nil {
|
||||
// We alert the user instead of failing the command since it can be difficult to debug
|
||||
// for a template admin otherwise. It's quite possible (and easy) to set an
|
||||
// inappriopriate value for niceness.
|
||||
printfStdErr("failed to adjust niceness to %d for cmd %+v: %v", *nice, args, err)
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(args[0])
|
||||
if err != nil {
|
||||
return xerrors.Errorf("look path: %w", err)
|
||||
}
|
||||
|
||||
// Remove environment variables specific to the agentexec command. This is
|
||||
// especially important for environments that are attempting to develop Coder in Coder.
|
||||
ei := usershell.SystemEnvInfo{}
|
||||
env := ei.Environ()
|
||||
env = slices.DeleteFunc(env, func(e string) bool {
|
||||
return strings.HasPrefix(e, EnvProcPrioMgmt) ||
|
||||
strings.HasPrefix(e, EnvProcOOMScore) ||
|
||||
strings.HasPrefix(e, EnvProcNiceScore)
|
||||
})
|
||||
|
||||
return syscall.Exec(path, args, env)
|
||||
}
|
||||
|
||||
func defaultNiceScore() (int, error) {
|
||||
score, err := unix.Getpriority(unix.PRIO_PROCESS, 0)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get nice score: %w", err)
|
||||
}
|
||||
// See https://linux.die.net/man/2/setpriority#Notes
|
||||
score = 20 - score
|
||||
|
||||
score += 5
|
||||
if score > 19 {
|
||||
return 19, nil
|
||||
}
|
||||
return score, nil
|
||||
}
|
||||
|
||||
func defaultOOMScore() (int, error) {
|
||||
score, err := oomScoreAdj()
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get oom score: %w", err)
|
||||
}
|
||||
|
||||
// If the agent has a negative oom_score_adj, we set the child to 0
|
||||
// so it's treated like every other process.
|
||||
if score < 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// If the agent is already almost at the maximum then set it to the max.
|
||||
if score >= 998 {
|
||||
return 1000, nil
|
||||
}
|
||||
|
||||
// If the agent oom_score_adj is >=0, we set the child to slightly
|
||||
// less than the maximum. If users want a different score they set it
|
||||
// directly.
|
||||
return 998, nil
|
||||
}
|
||||
|
||||
func oomScoreAdj() (int, error) {
|
||||
scoreStr, err := os.ReadFile("/proc/self/oom_score_adj")
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("read oom_score_adj: %w", err)
|
||||
}
|
||||
return strconv.Atoi(strings.TrimSpace(string(scoreStr)))
|
||||
}
|
||||
|
||||
func writeOOMScoreAdj(score int) error {
|
||||
return os.WriteFile(fmt.Sprintf("/proc/%d/oom_score_adj", os.Getpid()), []byte(fmt.Sprintf("%d", score)), 0o600)
|
||||
}
|
||||
|
||||
// execArgs returns the arguments to pass to syscall.Exec after the "--" delimiter.
|
||||
func execArgs(args []string) []string {
|
||||
for i, arg := range args {
|
||||
if arg == "--" {
|
||||
return args[i+1:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printfStdErr(format string, a ...any) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "coder-agent: %s\n", fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func dropEffectiveCaps() error {
|
||||
proc := cap.GetProc()
|
||||
err := proc.ClearFlag(cap.Effective)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("clear effective caps: %w", err)
|
||||
}
|
||||
err = proc.SetProc()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set proc: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package agentexec_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
//nolint:paralleltest // This test is sensitive to environment variables
|
||||
func TestCLI(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
cmd, path := cmd(ctx, t, 123, 12)
|
||||
err := cmd.Start()
|
||||
require.NoError(t, err)
|
||||
go cmd.Wait()
|
||||
|
||||
waitForSentinel(ctx, t, cmd, path)
|
||||
requireOOMScore(t, cmd.Process.Pid, 123)
|
||||
requireNiceScore(t, cmd.Process.Pid, 12)
|
||||
})
|
||||
|
||||
t.Run("FiltersEnv", func(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
cmd, path := cmd(ctx, t, 123, 12)
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=true", agentexec.EnvProcPrioMgmt))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=123", agentexec.EnvProcOOMScore))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=12", agentexec.EnvProcNiceScore))
|
||||
// Ensure unrelated environment variables are preserved.
|
||||
cmd.Env = append(cmd.Env, "CODER_TEST_ME_AGENTEXEC=true")
|
||||
err := cmd.Start()
|
||||
require.NoError(t, err)
|
||||
go cmd.Wait()
|
||||
waitForSentinel(ctx, t, cmd, path)
|
||||
|
||||
env := procEnv(t, cmd.Process.Pid)
|
||||
hasExecEnvs := slices.ContainsFunc(
|
||||
env,
|
||||
func(e string) bool {
|
||||
return strings.HasPrefix(e, agentexec.EnvProcPrioMgmt) ||
|
||||
strings.HasPrefix(e, agentexec.EnvProcOOMScore) ||
|
||||
strings.HasPrefix(e, agentexec.EnvProcNiceScore)
|
||||
})
|
||||
require.False(t, hasExecEnvs, "expected environment variables to be filtered")
|
||||
userEnv := slices.Contains(env, "CODER_TEST_ME_AGENTEXEC=true")
|
||||
require.True(t, userEnv, "expected user environment variables to be preserved")
|
||||
})
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
cmd, path := cmd(ctx, t, 0, 0)
|
||||
err := cmd.Start()
|
||||
require.NoError(t, err)
|
||||
go cmd.Wait()
|
||||
|
||||
waitForSentinel(ctx, t, cmd, path)
|
||||
|
||||
expectedNice := expectedNiceScore(t)
|
||||
expectedOOM := expectedOOMScore(t)
|
||||
requireOOMScore(t, cmd.Process.Pid, expectedOOM)
|
||||
requireNiceScore(t, cmd.Process.Pid, expectedNice)
|
||||
})
|
||||
|
||||
t.Run("Capabilities", func(t *testing.T) {
|
||||
testdir := filepath.Dir(TestBin)
|
||||
capDir := filepath.Join(testdir, "caps")
|
||||
err := os.Mkdir(capDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
bin := buildBinary(capDir)
|
||||
// Try to set capabilities on the binary. This should work fine in CI but
|
||||
// it's possible some developers may be working in an environment where they don't have the necessary permissions.
|
||||
err = setCaps(t, bin, "cap_net_admin")
|
||||
if os.Getenv("CI") != "" {
|
||||
require.NoError(t, err)
|
||||
} else if err != nil {
|
||||
t.Skipf("unable to set capabilities for test: %v", err)
|
||||
}
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
cmd, path := binCmd(ctx, t, bin, 123, 12)
|
||||
err = cmd.Start()
|
||||
require.NoError(t, err)
|
||||
go cmd.Wait()
|
||||
|
||||
waitForSentinel(ctx, t, cmd, path)
|
||||
// This is what we're really testing, a binary with added capabilities requires setting dumpable.
|
||||
requireOOMScore(t, cmd.Process.Pid, 123)
|
||||
requireNiceScore(t, cmd.Process.Pid, 12)
|
||||
})
|
||||
}
|
||||
|
||||
func requireNiceScore(t *testing.T, pid int, score int) {
|
||||
t.Helper()
|
||||
|
||||
nice, err := unix.Getpriority(unix.PRIO_PROCESS, pid)
|
||||
require.NoError(t, err)
|
||||
// See https://linux.die.net/man/2/setpriority#Notes
|
||||
require.Equal(t, score, 20-nice)
|
||||
}
|
||||
|
||||
func requireOOMScore(t *testing.T, pid int, expected int) {
|
||||
t.Helper()
|
||||
|
||||
actual, err := os.ReadFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid))
|
||||
require.NoError(t, err)
|
||||
score := strings.TrimSpace(string(actual))
|
||||
require.Equal(t, strconv.Itoa(expected), score)
|
||||
}
|
||||
|
||||
func waitForSentinel(ctx context.Context, t *testing.T, cmd *exec.Cmd, path string) {
|
||||
t.Helper()
|
||||
|
||||
ticker := time.NewTicker(testutil.IntervalFast)
|
||||
defer ticker.Stop()
|
||||
|
||||
// RequireEventually doesn't work well with require.NoError or similar require functions.
|
||||
for {
|
||||
err := cmd.Process.Signal(syscall.Signal(0))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = os.Stat(path)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-ctx.Done():
|
||||
require.NoError(t, ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func binCmd(ctx context.Context, t *testing.T, bin string, oom, nice int) (*exec.Cmd, string) {
|
||||
var (
|
||||
args = execArgs(oom, nice)
|
||||
dir = t.TempDir()
|
||||
file = filepath.Join(dir, "sentinel")
|
||||
)
|
||||
|
||||
args = append(args, "sh", "-c", fmt.Sprintf("touch %s && sleep 10m", file))
|
||||
//nolint:gosec
|
||||
cmd := exec.CommandContext(ctx, bin, args...)
|
||||
|
||||
// We set this so we can also easily kill the sleep process the shell spawns.
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &buf
|
||||
t.Cleanup(func() {
|
||||
// Print output of a command if the test fails.
|
||||
if t.Failed() {
|
||||
t.Logf("cmd %q output: %s", cmd.Args, buf.String())
|
||||
}
|
||||
if cmd.Process != nil {
|
||||
// We use -cmd.Process.Pid to kill the whole process group.
|
||||
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)
|
||||
}
|
||||
})
|
||||
return cmd, file
|
||||
}
|
||||
|
||||
func cmd(ctx context.Context, t *testing.T, oom, nice int) (*exec.Cmd, string) {
|
||||
return binCmd(ctx, t, TestBin, oom, nice)
|
||||
}
|
||||
|
||||
func expectedOOMScore(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
score, err := os.ReadFile(fmt.Sprintf("/proc/%d/oom_score_adj", os.Getpid()))
|
||||
require.NoError(t, err)
|
||||
|
||||
scoreInt, err := strconv.Atoi(strings.TrimSpace(string(score)))
|
||||
require.NoError(t, err)
|
||||
|
||||
if scoreInt < 0 {
|
||||
return 0
|
||||
}
|
||||
if scoreInt >= 998 {
|
||||
return 1000
|
||||
}
|
||||
return 998
|
||||
}
|
||||
|
||||
// procEnv returns the environment variables for a given process.
|
||||
func procEnv(t *testing.T, pid int) []string {
|
||||
t.Helper()
|
||||
|
||||
env, err := os.ReadFile(fmt.Sprintf("/proc/%d/environ", pid))
|
||||
require.NoError(t, err)
|
||||
return strings.Split(string(env), "\x00")
|
||||
}
|
||||
|
||||
func expectedNiceScore(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
score, err := unix.Getpriority(unix.PRIO_PROCESS, os.Getpid())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Priority is niceness + 20.
|
||||
score = 20 - score
|
||||
score += 5
|
||||
if score > 19 {
|
||||
return 19
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func execArgs(oom int, nice int) []string {
|
||||
execArgs := []string{"agent-exec"}
|
||||
if oom != 0 {
|
||||
execArgs = append(execArgs, fmt.Sprintf("--coder-oom=%d", oom))
|
||||
}
|
||||
if nice != 0 {
|
||||
execArgs = append(execArgs, fmt.Sprintf("--coder-nice=%d", nice))
|
||||
}
|
||||
execArgs = append(execArgs, "--")
|
||||
return execArgs
|
||||
}
|
||||
|
||||
func setCaps(t *testing.T, bin string, caps ...string) error {
|
||||
t.Helper()
|
||||
|
||||
setcap := fmt.Sprintf("sudo -n setcap %s=ep %s", strings.Join(caps, ", "), bin)
|
||||
out, err := exec.CommandContext(context.Background(), "sh", "-c", setcap).CombinedOutput()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("setcap %q (%s): %w", setcap, out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package agentexec
|
||||
|
||||
import "golang.org/x/xerrors"
|
||||
|
||||
func CLI() error {
|
||||
return xerrors.New("agent-exec is only supported on Linux")
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := agentexec.CLI()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package agentexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/pty"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvProcPrioMgmt is the environment variable that determines whether
|
||||
// we attempt to manage process CPU and OOM Killer priority.
|
||||
EnvProcPrioMgmt = "CODER_PROC_PRIO_MGMT"
|
||||
EnvProcOOMScore = "CODER_PROC_OOM_SCORE"
|
||||
EnvProcNiceScore = "CODER_PROC_NICE_SCORE"
|
||||
|
||||
// unset is set to an invalid value for nice and oom scores.
|
||||
unset = -2000
|
||||
)
|
||||
|
||||
var DefaultExecer Execer = execer{}
|
||||
|
||||
// Execer defines an abstraction for creating exec.Cmd variants. It's unfortunately
|
||||
// necessary because we need to be able to wrap child processes with "coder agent-exec"
|
||||
// for templates that expect the agent to manage process priority.
|
||||
type Execer interface {
|
||||
// CommandContext returns an exec.Cmd that calls "coder agent-exec" prior to exec'ing
|
||||
// the provided command if CODER_PROC_PRIO_MGMT is set, otherwise a normal exec.Cmd
|
||||
// is returned. All instances of exec.Cmd should flow through this function to ensure
|
||||
// proper resource constraints are applied to the child process.
|
||||
CommandContext(ctx context.Context, cmd string, args ...string) *exec.Cmd
|
||||
// PTYCommandContext returns an pty.Cmd that calls "coder agent-exec" prior to exec'ing
|
||||
// the provided command if CODER_PROC_PRIO_MGMT is set, otherwise a normal pty.Cmd
|
||||
// is returned. All instances of pty.Cmd should flow through this function to ensure
|
||||
// proper resource constraints are applied to the child process.
|
||||
PTYCommandContext(ctx context.Context, cmd string, args ...string) *pty.Cmd
|
||||
}
|
||||
|
||||
func NewExecer() (Execer, error) {
|
||||
_, enabled := os.LookupEnv(EnvProcPrioMgmt)
|
||||
if runtime.GOOS != "linux" || !enabled {
|
||||
return DefaultExecer, nil
|
||||
}
|
||||
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get executable: %w", err)
|
||||
}
|
||||
|
||||
bin, err := filepath.EvalSymlinks(executable)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("eval symlinks: %w", err)
|
||||
}
|
||||
|
||||
oomScore, ok := envValInt(EnvProcOOMScore)
|
||||
if !ok {
|
||||
oomScore = unset
|
||||
}
|
||||
|
||||
niceScore, ok := envValInt(EnvProcNiceScore)
|
||||
if !ok {
|
||||
niceScore = unset
|
||||
}
|
||||
|
||||
return priorityExecer{
|
||||
binPath: bin,
|
||||
oomScore: oomScore,
|
||||
niceScore: niceScore,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type execer struct{}
|
||||
|
||||
func (execer) CommandContext(ctx context.Context, cmd string, args ...string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (execer) PTYCommandContext(ctx context.Context, cmd string, args ...string) *pty.Cmd {
|
||||
return pty.CommandContext(ctx, cmd, args...)
|
||||
}
|
||||
|
||||
type priorityExecer struct {
|
||||
binPath string
|
||||
oomScore int
|
||||
niceScore int
|
||||
}
|
||||
|
||||
func (e priorityExecer) CommandContext(ctx context.Context, cmd string, args ...string) *exec.Cmd {
|
||||
cmd, args = e.agentExecCmd(cmd, args...)
|
||||
return exec.CommandContext(ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e priorityExecer) PTYCommandContext(ctx context.Context, cmd string, args ...string) *pty.Cmd {
|
||||
cmd, args = e.agentExecCmd(cmd, args...)
|
||||
return pty.CommandContext(ctx, cmd, args...)
|
||||
}
|
||||
|
||||
func (e priorityExecer) agentExecCmd(cmd string, args ...string) (string, []string) {
|
||||
execArgs := []string{"agent-exec"}
|
||||
if e.oomScore != unset {
|
||||
execArgs = append(execArgs, oomScoreArg(e.oomScore))
|
||||
}
|
||||
|
||||
if e.niceScore != unset {
|
||||
execArgs = append(execArgs, niceScoreArg(e.niceScore))
|
||||
}
|
||||
execArgs = append(execArgs, "--", cmd)
|
||||
execArgs = append(execArgs, args...)
|
||||
|
||||
return e.binPath, execArgs
|
||||
}
|
||||
|
||||
// envValInt searches for a key in a list of environment variables and parses it to an int.
|
||||
// If the key is not found or cannot be parsed, returns 0 and false.
|
||||
func envValInt(key string) (int, bool) {
|
||||
val, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return i, true
|
||||
}
|
||||
|
||||
// The following are flags used by the agent-exec command. We use flags instead of
|
||||
// environment variables to avoid having to deal with a caller overriding the
|
||||
// environment variables.
|
||||
const (
|
||||
niceFlag = "coder-nice"
|
||||
oomFlag = "coder-oom"
|
||||
)
|
||||
|
||||
func niceScoreArg(score int) string {
|
||||
return fmt.Sprintf("--%s=%d", niceFlag, score)
|
||||
}
|
||||
|
||||
func oomScoreArg(score int) string {
|
||||
return fmt.Sprintf("--%s=%d", oomFlag, score)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package agentexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExecer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd := DefaultExecer.CommandContext(context.Background(), "sh", "-c", "sleep")
|
||||
|
||||
path, err := exec.LookPath("sh")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, path, cmd.Path)
|
||||
require.Equal(t, []string{"sh", "-c", "sleep"}, cmd.Args)
|
||||
})
|
||||
|
||||
t.Run("Priority", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := priorityExecer{
|
||||
binPath: "/foo/bar/baz",
|
||||
oomScore: unset,
|
||||
niceScore: unset,
|
||||
}
|
||||
|
||||
cmd := e.CommandContext(context.Background(), "sh", "-c", "sleep")
|
||||
require.Equal(t, e.binPath, cmd.Path)
|
||||
require.Equal(t, []string{e.binPath, "agent-exec", "--", "sh", "-c", "sleep"}, cmd.Args)
|
||||
})
|
||||
|
||||
t.Run("Nice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := priorityExecer{
|
||||
binPath: "/foo/bar/baz",
|
||||
oomScore: unset,
|
||||
niceScore: 10,
|
||||
}
|
||||
|
||||
cmd := e.CommandContext(context.Background(), "sh", "-c", "sleep")
|
||||
require.Equal(t, e.binPath, cmd.Path)
|
||||
require.Equal(t, []string{e.binPath, "agent-exec", "--coder-nice=10", "--", "sh", "-c", "sleep"}, cmd.Args)
|
||||
})
|
||||
|
||||
t.Run("OOM", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := priorityExecer{
|
||||
binPath: "/foo/bar/baz",
|
||||
oomScore: 123,
|
||||
niceScore: unset,
|
||||
}
|
||||
|
||||
cmd := e.CommandContext(context.Background(), "sh", "-c", "sleep")
|
||||
require.Equal(t, e.binPath, cmd.Path)
|
||||
require.Equal(t, []string{e.binPath, "agent-exec", "--coder-oom=123", "--", "sh", "-c", "sleep"}, cmd.Args)
|
||||
})
|
||||
|
||||
t.Run("Both", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := priorityExecer{
|
||||
binPath: "/foo/bar/baz",
|
||||
oomScore: 432,
|
||||
niceScore: 14,
|
||||
}
|
||||
|
||||
cmd := e.CommandContext(context.Background(), "sh", "-c", "sleep")
|
||||
require.Equal(t, e.binPath, cmd.Path)
|
||||
require.Equal(t, []string{e.binPath, "agent-exec", "--coder-oom=432", "--coder-nice=14", "--", "sh", "-c", "sleep"}, cmd.Args)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package agentexec_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var TestBin string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := func() int {
|
||||
// We generate a unique directory per test invocation to avoid collisions between two
|
||||
// processes attempting to create the same temp file.
|
||||
dir := genDir()
|
||||
defer os.RemoveAll(dir)
|
||||
TestBin = buildBinary(dir)
|
||||
return m.Run()
|
||||
}()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func buildBinary(dir string) string {
|
||||
path := filepath.Join(dir, "agent-test")
|
||||
out, err := exec.Command("go", "build", "-o", path, "./cmdtest").CombinedOutput()
|
||||
mustf(err, "build binary: %s", out)
|
||||
return path
|
||||
}
|
||||
|
||||
func mustf(err error, msg string, args ...any) {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf(msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func genDir() string {
|
||||
dir, err := os.MkdirTemp(os.TempDir(), "agentexec")
|
||||
mustf(err, "create temp dir: %v", err)
|
||||
return dir
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Package agentproctest contains utility functions
|
||||
// for testing process management in the agent.
|
||||
package agentproctest
|
||||
|
||||
//go:generate mockgen -destination ./syscallermock.go -package agentproctest github.com/coder/coder/v2/agent/agentproc Syscaller
|
||||
@@ -0,0 +1,55 @@
|
||||
package agentproctest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentproc"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
)
|
||||
|
||||
func GenerateProcess(t *testing.T, fs afero.Fs, muts ...func(*agentproc.Process)) agentproc.Process {
|
||||
t.Helper()
|
||||
|
||||
pid, err := cryptorand.Intn(1<<31 - 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
arg1, err := cryptorand.String(5)
|
||||
require.NoError(t, err)
|
||||
|
||||
arg2, err := cryptorand.String(5)
|
||||
require.NoError(t, err)
|
||||
|
||||
arg3, err := cryptorand.String(5)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmdline := fmt.Sprintf("%s\x00%s\x00%s", arg1, arg2, arg3)
|
||||
|
||||
process := agentproc.Process{
|
||||
CmdLine: cmdline,
|
||||
PID: int32(pid),
|
||||
OOMScoreAdj: 0,
|
||||
}
|
||||
|
||||
for _, mut := range muts {
|
||||
mut(&process)
|
||||
}
|
||||
|
||||
process.Dir = fmt.Sprintf("%s/%d", "/proc", process.PID)
|
||||
|
||||
err = fs.MkdirAll(process.Dir, 0o555)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = afero.WriteFile(fs, fmt.Sprintf("%s/cmdline", process.Dir), []byte(process.CmdLine), 0o444)
|
||||
require.NoError(t, err)
|
||||
|
||||
score := strconv.Itoa(process.OOMScoreAdj)
|
||||
err = afero.WriteFile(fs, fmt.Sprintf("%s/oom_score_adj", process.Dir), []byte(score), 0o444)
|
||||
require.NoError(t, err)
|
||||
|
||||
return process
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/coder/coder/v2/agent/agentproc (interfaces: Syscaller)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination ./syscallermock.go -package agentproctest github.com/coder/coder/v2/agent/agentproc Syscaller
|
||||
//
|
||||
|
||||
// Package agentproctest is a generated GoMock package.
|
||||
package agentproctest
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
syscall "syscall"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockSyscaller is a mock of Syscaller interface.
|
||||
type MockSyscaller struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSyscallerMockRecorder
|
||||
}
|
||||
|
||||
// MockSyscallerMockRecorder is the mock recorder for MockSyscaller.
|
||||
type MockSyscallerMockRecorder struct {
|
||||
mock *MockSyscaller
|
||||
}
|
||||
|
||||
// NewMockSyscaller creates a new mock instance.
|
||||
func NewMockSyscaller(ctrl *gomock.Controller) *MockSyscaller {
|
||||
mock := &MockSyscaller{ctrl: ctrl}
|
||||
mock.recorder = &MockSyscallerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSyscaller) EXPECT() *MockSyscallerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetPriority mocks base method.
|
||||
func (m *MockSyscaller) GetPriority(arg0 int32) (int, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPriority", arg0)
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPriority indicates an expected call of GetPriority.
|
||||
func (mr *MockSyscallerMockRecorder) GetPriority(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPriority", reflect.TypeOf((*MockSyscaller)(nil).GetPriority), arg0)
|
||||
}
|
||||
|
||||
// Kill mocks base method.
|
||||
func (m *MockSyscaller) Kill(arg0 int32, arg1 syscall.Signal) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Kill", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Kill indicates an expected call of Kill.
|
||||
func (mr *MockSyscallerMockRecorder) Kill(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kill", reflect.TypeOf((*MockSyscaller)(nil).Kill), arg0, arg1)
|
||||
}
|
||||
|
||||
// SetPriority mocks base method.
|
||||
func (m *MockSyscaller) SetPriority(arg0 int32, arg1 int) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetPriority", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetPriority indicates an expected call of SetPriority.
|
||||
func (mr *MockSyscallerMockRecorder) SetPriority(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPriority", reflect.TypeOf((*MockSyscaller)(nil).SetPriority), arg0, arg1)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package agentproc contains logic for interfacing with local
|
||||
// processes running in the same context as the agent.
|
||||
package agentproc
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package agentproc
|
||||
|
||||
import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func (*Process) Niceness(Syscaller) (int, error) {
|
||||
return 0, errUnimplemented
|
||||
}
|
||||
|
||||
func (*Process) SetNiceness(Syscaller, int) error {
|
||||
return errUnimplemented
|
||||
}
|
||||
|
||||
func (*Process) Cmd() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func List(afero.Fs, Syscaller) ([]*Process, error) {
|
||||
return nil, errUnimplemented
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package agentproc_test
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentproc"
|
||||
"github.com/coder/coder/v2/agent/agentproc/agentproctest"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skipf("skipping non-linux environment")
|
||||
}
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
fs = afero.NewMemMapFs()
|
||||
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
|
||||
expectedProcs = make(map[int32]agentproc.Process)
|
||||
)
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
proc := agentproctest.GenerateProcess(t, fs)
|
||||
expectedProcs[proc.PID] = proc
|
||||
|
||||
sc.EXPECT().
|
||||
Kill(proc.PID, syscall.Signal(0)).
|
||||
Return(nil)
|
||||
}
|
||||
|
||||
actualProcs, err := agentproc.List(fs, sc)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actualProcs, len(expectedProcs))
|
||||
for _, proc := range actualProcs {
|
||||
expected, ok := expectedProcs[proc.PID]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expected.PID, proc.PID)
|
||||
require.Equal(t, expected.CmdLine, proc.CmdLine)
|
||||
require.Equal(t, expected.Dir, proc.Dir)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FinishedProcess", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
fs = afero.NewMemMapFs()
|
||||
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
|
||||
expectedProcs = make(map[int32]agentproc.Process)
|
||||
)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
proc := agentproctest.GenerateProcess(t, fs)
|
||||
expectedProcs[proc.PID] = proc
|
||||
|
||||
sc.EXPECT().
|
||||
Kill(proc.PID, syscall.Signal(0)).
|
||||
Return(nil)
|
||||
}
|
||||
|
||||
// Create a process that's already finished. We're not adding
|
||||
// it to the map because it should be skipped over.
|
||||
proc := agentproctest.GenerateProcess(t, fs)
|
||||
sc.EXPECT().
|
||||
Kill(proc.PID, syscall.Signal(0)).
|
||||
Return(xerrors.New("os: process already finished"))
|
||||
|
||||
actualProcs, err := agentproc.List(fs, sc)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actualProcs, len(expectedProcs))
|
||||
for _, proc := range actualProcs {
|
||||
expected, ok := expectedProcs[proc.PID]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expected.PID, proc.PID)
|
||||
require.Equal(t, expected.CmdLine, proc.CmdLine)
|
||||
require.Equal(t, expected.Dir, proc.Dir)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoSuchProcess", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
fs = afero.NewMemMapFs()
|
||||
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
|
||||
expectedProcs = make(map[int32]agentproc.Process)
|
||||
)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
proc := agentproctest.GenerateProcess(t, fs)
|
||||
expectedProcs[proc.PID] = proc
|
||||
|
||||
sc.EXPECT().
|
||||
Kill(proc.PID, syscall.Signal(0)).
|
||||
Return(nil)
|
||||
}
|
||||
|
||||
// Create a process that doesn't exist. We're not adding
|
||||
// it to the map because it should be skipped over.
|
||||
proc := agentproctest.GenerateProcess(t, fs)
|
||||
sc.EXPECT().
|
||||
Kill(proc.PID, syscall.Signal(0)).
|
||||
Return(syscall.ESRCH)
|
||||
|
||||
actualProcs, err := agentproc.List(fs, sc)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, actualProcs, len(expectedProcs))
|
||||
for _, proc := range actualProcs {
|
||||
expected, ok := expectedProcs[proc.PID]
|
||||
require.True(t, ok)
|
||||
require.Equal(t, expected.PID, proc.PID)
|
||||
require.Equal(t, expected.CmdLine, proc.CmdLine)
|
||||
require.Equal(t, expected.Dir, proc.Dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// These tests are not very interesting but they provide some modicum of
|
||||
// confidence.
|
||||
func TestProcess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skipf("skipping non-linux environment")
|
||||
}
|
||||
|
||||
t.Run("SetNiceness", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
|
||||
proc = &agentproc.Process{
|
||||
PID: 32,
|
||||
}
|
||||
score = 20
|
||||
)
|
||||
|
||||
sc.EXPECT().SetPriority(proc.PID, score).Return(nil)
|
||||
err := proc.SetNiceness(sc, score)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Cmd", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
proc = &agentproc.Process{
|
||||
CmdLine: "helloworld\x00--arg1\x00--arg2",
|
||||
}
|
||||
expectedName = "helloworld --arg1 --arg2"
|
||||
)
|
||||
|
||||
require.Equal(t, expectedName, proc.Cmd())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package agentproc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func List(fs afero.Fs, syscaller Syscaller) ([]*Process, error) {
|
||||
d, err := fs.Open(defaultProcDir)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("open dir %q: %w", defaultProcDir, err)
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
entries, err := d.Readdirnames(0)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("readdirnames: %w", err)
|
||||
}
|
||||
|
||||
processes := make([]*Process, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
pid, err := strconv.ParseInt(entry, 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check that the process still exists.
|
||||
exists, err := isProcessExist(syscaller, int32(pid))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("check process exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
cmdline, err := afero.ReadFile(fs, filepath.Join(defaultProcDir, entry, "cmdline"))
|
||||
if err != nil {
|
||||
var errNo syscall.Errno
|
||||
if xerrors.As(err, &errNo) && errNo == syscall.EPERM {
|
||||
continue
|
||||
}
|
||||
return nil, xerrors.Errorf("read cmdline: %w", err)
|
||||
}
|
||||
|
||||
oomScore, err := afero.ReadFile(fs, filepath.Join(defaultProcDir, entry, "oom_score_adj"))
|
||||
if err != nil {
|
||||
if xerrors.Is(err, os.ErrPermission) {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, xerrors.Errorf("read oom_score_adj: %w", err)
|
||||
}
|
||||
|
||||
oom, err := strconv.Atoi(strings.TrimSpace(string(oomScore)))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("convert oom score: %w", err)
|
||||
}
|
||||
|
||||
processes = append(processes, &Process{
|
||||
PID: int32(pid),
|
||||
CmdLine: string(cmdline),
|
||||
Dir: filepath.Join(defaultProcDir, entry),
|
||||
OOMScoreAdj: oom,
|
||||
})
|
||||
}
|
||||
|
||||
return processes, nil
|
||||
}
|
||||
|
||||
func isProcessExist(syscaller Syscaller, pid int32) (bool, error) {
|
||||
err := syscaller.Kill(pid, syscall.Signal(0))
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if err.Error() == "os: process already finished" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var errno syscall.Errno
|
||||
if !errors.As(err, &errno) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch errno {
|
||||
case syscall.ESRCH:
|
||||
return false, nil
|
||||
case syscall.EPERM:
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, xerrors.Errorf("kill: %w", err)
|
||||
}
|
||||
|
||||
func (p *Process) Niceness(sc Syscaller) (int, error) {
|
||||
nice, err := sc.GetPriority(p.PID)
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get priority for %q: %w", p.CmdLine, err)
|
||||
}
|
||||
return nice, nil
|
||||
}
|
||||
|
||||
func (p *Process) SetNiceness(sc Syscaller, score int) error {
|
||||
err := sc.SetPriority(p.PID, score)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set priority for %q: %w", p.CmdLine, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Process) Cmd() string {
|
||||
return strings.Join(p.cmdLine(), " ")
|
||||
}
|
||||
|
||||
func (p *Process) cmdLine() []string {
|
||||
return strings.Split(p.CmdLine, "\x00")
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package agentproc
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type Syscaller interface {
|
||||
SetPriority(pid int32, priority int) error
|
||||
GetPriority(pid int32) (int, error)
|
||||
Kill(pid int32, sig syscall.Signal) error
|
||||
}
|
||||
|
||||
// nolint: unused // used on some but no all platforms
|
||||
const defaultProcDir = "/proc"
|
||||
|
||||
type Process struct {
|
||||
Dir string
|
||||
CmdLine string
|
||||
PID int32
|
||||
OOMScoreAdj int
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package agentproc
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func NewSyscaller() Syscaller {
|
||||
return nopSyscaller{}
|
||||
}
|
||||
|
||||
var errUnimplemented = xerrors.New("unimplemented")
|
||||
|
||||
type nopSyscaller struct{}
|
||||
|
||||
func (nopSyscaller) SetPriority(int32, int) error {
|
||||
return errUnimplemented
|
||||
}
|
||||
|
||||
func (nopSyscaller) GetPriority(int32) (int, error) {
|
||||
return 0, errUnimplemented
|
||||
}
|
||||
|
||||
func (nopSyscaller) Kill(int32, syscall.Signal) error {
|
||||
return errUnimplemented
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package agentproc
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func NewSyscaller() Syscaller {
|
||||
return UnixSyscaller{}
|
||||
}
|
||||
|
||||
type UnixSyscaller struct{}
|
||||
|
||||
func (UnixSyscaller) SetPriority(pid int32, nice int) error {
|
||||
err := unix.Setpriority(unix.PRIO_PROCESS, int(pid), nice)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set priority: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (UnixSyscaller) GetPriority(pid int32) (int, error) {
|
||||
nice, err := unix.Getpriority(0, int(pid))
|
||||
if err != nil {
|
||||
return 0, xerrors.Errorf("get priority: %w", err)
|
||||
}
|
||||
return nice, nil
|
||||
}
|
||||
|
||||
func (UnixSyscaller) Kill(pid int32, sig syscall.Signal) error {
|
||||
err := syscall.Kill(int(pid), sig)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("kill: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package agentrsa
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// GenerateDeterministicKey generates an RSA private key deterministically based on the provided seed.
|
||||
// This function uses a deterministic random source to generate the primes p and q, ensuring that the
|
||||
// same seed will always produce the same private key. The generated key is 2048 bits in size.
|
||||
//
|
||||
// Reference: https://pkg.go.dev/crypto/rsa#GenerateKey
|
||||
func GenerateDeterministicKey(seed int64) *rsa.PrivateKey {
|
||||
// Since the standard lib purposefully does not generate
|
||||
// deterministic rsa keys, we need to do it ourselves.
|
||||
|
||||
// Create deterministic random source
|
||||
// nolint: gosec
|
||||
deterministicRand := rand.New(rand.NewSource(seed))
|
||||
|
||||
// Use fixed values for p and q based on the seed
|
||||
p := big.NewInt(0)
|
||||
q := big.NewInt(0)
|
||||
e := big.NewInt(65537) // Standard RSA public exponent
|
||||
|
||||
for {
|
||||
// Generate deterministic primes using the seeded random
|
||||
// Each prime should be ~1024 bits to get a 2048-bit key
|
||||
for {
|
||||
p.SetBit(p, 1024, 1) // Ensure it's large enough
|
||||
for i := range 1024 {
|
||||
if deterministicRand.Int63()%2 == 1 {
|
||||
p.SetBit(p, i, 1)
|
||||
} else {
|
||||
p.SetBit(p, i, 0)
|
||||
}
|
||||
}
|
||||
p1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
if p.ProbablyPrime(20) && new(big.Int).GCD(nil, nil, e, p1).Cmp(big.NewInt(1)) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
q.SetBit(q, 1024, 1) // Ensure it's large enough
|
||||
for i := range 1024 {
|
||||
if deterministicRand.Int63()%2 == 1 {
|
||||
q.SetBit(q, i, 1)
|
||||
} else {
|
||||
q.SetBit(q, i, 0)
|
||||
}
|
||||
}
|
||||
q1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
if q.ProbablyPrime(20) && p.Cmp(q) != 0 && new(big.Int).GCD(nil, nil, e, q1).Cmp(big.NewInt(1)) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate phi = (p-1) * (q-1)
|
||||
p1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
q1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
phi := new(big.Int).Mul(p1, q1)
|
||||
|
||||
// Calculate private exponent d
|
||||
d := new(big.Int).ModInverse(e, phi)
|
||||
if d != nil {
|
||||
// Calculate n = p * q
|
||||
n := new(big.Int).Mul(p, q)
|
||||
|
||||
// Create the private key
|
||||
privateKey := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: n,
|
||||
E: int(e.Int64()),
|
||||
},
|
||||
D: d,
|
||||
Primes: []*big.Int{p, q},
|
||||
}
|
||||
|
||||
// Compute precomputed values
|
||||
privateKey.Precompute()
|
||||
|
||||
return privateKey
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package agentrsa_test
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"math/rand/v2"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentrsa"
|
||||
)
|
||||
|
||||
func TestGenerateDeterministicKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key1 := agentrsa.GenerateDeterministicKey(1234)
|
||||
key2 := agentrsa.GenerateDeterministicKey(1234)
|
||||
|
||||
assert.Equal(t, key1, key2)
|
||||
assert.EqualExportedValues(t, key1, key2)
|
||||
}
|
||||
|
||||
var result *rsa.PrivateKey
|
||||
|
||||
func BenchmarkGenerateDeterministicKey(b *testing.B) {
|
||||
var r *rsa.PrivateKey
|
||||
|
||||
for range b.N {
|
||||
// always record the result of DeterministicPrivateKey to prevent
|
||||
// the compiler eliminating the function call.
|
||||
// #nosec G404 - Using math/rand is acceptable for benchmarking deterministic keys
|
||||
r = agentrsa.GenerateDeterministicKey(rand.Int64())
|
||||
}
|
||||
|
||||
// always store the result to a package level variable
|
||||
// so the compiler cannot eliminate the Benchmark itself.
|
||||
result = r
|
||||
}
|
||||
|
||||
func FuzzGenerateDeterministicKey(f *testing.F) {
|
||||
testcases := []int64{0, 1234, 1010101010}
|
||||
for _, tc := range testcases {
|
||||
f.Add(tc) // Use f.Add to provide a seed corpus
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, seed int64) {
|
||||
key1 := agentrsa.GenerateDeterministicKey(seed)
|
||||
key2 := agentrsa.GenerateDeterministicKey(seed)
|
||||
assert.Equal(t, key1, key2)
|
||||
assert.EqualExportedValues(t, key1, key2)
|
||||
})
|
||||
}
|
||||
@@ -19,13 +19,10 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentssh"
|
||||
"github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
)
|
||||
@@ -78,36 +75,18 @@ func New(opts Options) *Runner {
|
||||
}
|
||||
}
|
||||
|
||||
type ScriptCompletedFunc func(context.Context, *proto.WorkspaceAgentScriptCompletedRequest) (*proto.WorkspaceAgentScriptCompletedResponse, error)
|
||||
|
||||
type runnerScript struct {
|
||||
runOnPostStart bool
|
||||
codersdk.WorkspaceAgentScript
|
||||
}
|
||||
|
||||
func toRunnerScript(scripts ...codersdk.WorkspaceAgentScript) []runnerScript {
|
||||
var rs []runnerScript
|
||||
for _, s := range scripts {
|
||||
rs = append(rs, runnerScript{
|
||||
WorkspaceAgentScript: s,
|
||||
})
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
type Runner struct {
|
||||
Options
|
||||
|
||||
cronCtx context.Context
|
||||
cronCtxCancel context.CancelFunc
|
||||
cmdCloseWait sync.WaitGroup
|
||||
closed chan struct{}
|
||||
closeMutex sync.Mutex
|
||||
cron *cron.Cron
|
||||
initialized atomic.Bool
|
||||
scripts []runnerScript
|
||||
dataDir string
|
||||
scriptCompleted ScriptCompletedFunc
|
||||
cronCtx context.Context
|
||||
cronCtxCancel context.CancelFunc
|
||||
cmdCloseWait sync.WaitGroup
|
||||
closed chan struct{}
|
||||
closeMutex sync.Mutex
|
||||
cron *cron.Cron
|
||||
initialized atomic.Bool
|
||||
scripts []codersdk.WorkspaceAgentScript
|
||||
dataDir string
|
||||
|
||||
// scriptsExecuted includes all scripts executed by the workspace agent. Agents
|
||||
// execute startup scripts, and scripts on a cron schedule. Both will increment
|
||||
@@ -134,35 +113,15 @@ func (r *Runner) RegisterMetrics(reg prometheus.Registerer) {
|
||||
reg.MustRegister(r.scriptsExecuted)
|
||||
}
|
||||
|
||||
// InitOption describes an option for the runner initialization.
|
||||
type InitOption func(*Runner)
|
||||
|
||||
// WithPostStartScripts adds scripts that should be run after the workspace
|
||||
// start scripts but before the workspace is marked as started.
|
||||
func WithPostStartScripts(scripts ...codersdk.WorkspaceAgentScript) InitOption {
|
||||
return func(r *Runner) {
|
||||
for _, s := range scripts {
|
||||
r.scripts = append(r.scripts, runnerScript{
|
||||
runOnPostStart: true,
|
||||
WorkspaceAgentScript: s,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the runner with the provided scripts.
|
||||
// It also schedules any scripts that have a schedule.
|
||||
// This function must be called before Execute.
|
||||
func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript, scriptCompleted ScriptCompletedFunc, opts ...InitOption) error {
|
||||
func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript) error {
|
||||
if r.initialized.Load() {
|
||||
return xerrors.New("init: already initialized")
|
||||
}
|
||||
r.initialized.Store(true)
|
||||
r.scripts = toRunnerScript(scripts...)
|
||||
r.scriptCompleted = scriptCompleted
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
r.scripts = scripts
|
||||
r.Logger.Info(r.cronCtx, "initializing agent scripts", slog.F("script_count", len(scripts)), slog.F("log_dir", r.LogDir))
|
||||
|
||||
err := r.Filesystem.MkdirAll(r.ScriptBinDir(), 0o700)
|
||||
@@ -170,13 +129,13 @@ func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript, scriptCompleted S
|
||||
return xerrors.Errorf("create script bin dir: %w", err)
|
||||
}
|
||||
|
||||
for _, script := range r.scripts {
|
||||
for _, script := range scripts {
|
||||
if script.Cron == "" {
|
||||
continue
|
||||
}
|
||||
script := script
|
||||
_, err := r.cron.AddFunc(script.Cron, func() {
|
||||
err := r.trackRun(r.cronCtx, script.WorkspaceAgentScript, ExecuteCronScripts)
|
||||
err := r.trackRun(r.cronCtx, script)
|
||||
if err != nil {
|
||||
r.Logger.Warn(context.Background(), "run agent script on schedule", slog.Error(err))
|
||||
}
|
||||
@@ -213,35 +172,22 @@ func (r *Runner) StartCron() {
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteOption describes what scripts we want to execute.
|
||||
type ExecuteOption int
|
||||
|
||||
// ExecuteOption enums.
|
||||
const (
|
||||
ExecuteAllScripts ExecuteOption = iota
|
||||
ExecuteStartScripts
|
||||
ExecutePostStartScripts
|
||||
ExecuteStopScripts
|
||||
ExecuteCronScripts
|
||||
)
|
||||
|
||||
// Execute runs a set of scripts according to a filter.
|
||||
func (r *Runner) Execute(ctx context.Context, option ExecuteOption) error {
|
||||
func (r *Runner) Execute(ctx context.Context, filter func(script codersdk.WorkspaceAgentScript) bool) error {
|
||||
if filter == nil {
|
||||
// Execute em' all!
|
||||
filter = func(script codersdk.WorkspaceAgentScript) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
var eg errgroup.Group
|
||||
for _, script := range r.scripts {
|
||||
runScript := (option == ExecuteStartScripts && script.RunOnStart) ||
|
||||
(option == ExecuteStopScripts && script.RunOnStop) ||
|
||||
(option == ExecutePostStartScripts && script.runOnPostStart) ||
|
||||
(option == ExecuteCronScripts && script.Cron != "") ||
|
||||
option == ExecuteAllScripts
|
||||
|
||||
if !runScript {
|
||||
if !filter(script) {
|
||||
continue
|
||||
}
|
||||
|
||||
script := script
|
||||
eg.Go(func() error {
|
||||
err := r.trackRun(ctx, script.WorkspaceAgentScript, option)
|
||||
err := r.trackRun(ctx, script)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("run agent script %q: %w", script.LogSourceID, err)
|
||||
}
|
||||
@@ -252,8 +198,8 @@ func (r *Runner) Execute(ctx context.Context, option ExecuteOption) error {
|
||||
}
|
||||
|
||||
// trackRun wraps "run" with metrics.
|
||||
func (r *Runner) trackRun(ctx context.Context, script codersdk.WorkspaceAgentScript, option ExecuteOption) error {
|
||||
err := r.run(ctx, script, option)
|
||||
func (r *Runner) trackRun(ctx context.Context, script codersdk.WorkspaceAgentScript) error {
|
||||
err := r.run(ctx, script)
|
||||
if err != nil {
|
||||
r.scriptsExecuted.WithLabelValues("false").Add(1)
|
||||
} else {
|
||||
@@ -266,7 +212,7 @@ func (r *Runner) trackRun(ctx context.Context, script codersdk.WorkspaceAgentScr
|
||||
// If the timeout is exceeded, the process is sent an interrupt signal.
|
||||
// If the process does not exit after a few seconds, it is forcefully killed.
|
||||
// This function immediately returns after a timeout, and does not wait for the process to exit.
|
||||
func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript, option ExecuteOption) error {
|
||||
func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript) error {
|
||||
logPath := script.LogPath
|
||||
if logPath == "" {
|
||||
logPath = fmt.Sprintf("coder-script-%s.log", script.LogSourceID)
|
||||
@@ -319,14 +265,14 @@ func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript,
|
||||
cmdCtx, ctxCancel = context.WithTimeout(ctx, script.Timeout)
|
||||
defer ctxCancel()
|
||||
}
|
||||
cmdPty, err := r.SSHServer.CreateCommand(cmdCtx, script.Script, nil, nil)
|
||||
cmdPty, err := r.SSHServer.CreateCommand(cmdCtx, script.Script, nil)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("%s script: create command: %w", logPath, err)
|
||||
}
|
||||
cmd = cmdPty.AsExec()
|
||||
cmd.SysProcAttr = cmdSysProcAttr()
|
||||
cmd.WaitDelay = 10 * time.Second
|
||||
cmd.Cancel = cmdCancel(ctx, logger, cmd)
|
||||
cmd.Cancel = cmdCancel(cmd)
|
||||
|
||||
// Expose env vars that can be used in the script for storing data
|
||||
// and binaries. In the future, we may want to expose more env vars
|
||||
@@ -353,9 +299,9 @@ func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript,
|
||||
cmd.Stdout = io.MultiWriter(fileWriter, infoW)
|
||||
cmd.Stderr = io.MultiWriter(fileWriter, errW)
|
||||
|
||||
start := dbtime.Now()
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
end := dbtime.Now()
|
||||
end := time.Now()
|
||||
execTime := end.Sub(start)
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
@@ -368,60 +314,6 @@ func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript,
|
||||
} else {
|
||||
logger.Info(ctx, fmt.Sprintf("%s script completed", logPath), slog.F("execution_time", execTime), slog.F("exit_code", exitCode))
|
||||
}
|
||||
|
||||
if r.scriptCompleted == nil {
|
||||
logger.Debug(ctx, "r.scriptCompleted unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
// We want to check this outside of the goroutine to avoid a race condition
|
||||
timedOut := errors.Is(err, ErrTimeout)
|
||||
pipesLeftOpen := errors.Is(err, ErrOutputPipesOpen)
|
||||
|
||||
err = r.trackCommandGoroutine(func() {
|
||||
var stage proto.Timing_Stage
|
||||
switch option {
|
||||
case ExecuteStartScripts:
|
||||
stage = proto.Timing_START
|
||||
case ExecuteStopScripts:
|
||||
stage = proto.Timing_STOP
|
||||
case ExecuteCronScripts:
|
||||
stage = proto.Timing_CRON
|
||||
}
|
||||
|
||||
var status proto.Timing_Status
|
||||
switch {
|
||||
case timedOut:
|
||||
status = proto.Timing_TIMED_OUT
|
||||
case pipesLeftOpen:
|
||||
status = proto.Timing_PIPES_LEFT_OPEN
|
||||
case exitCode != 0:
|
||||
status = proto.Timing_EXIT_FAILURE
|
||||
default:
|
||||
status = proto.Timing_OK
|
||||
}
|
||||
|
||||
reportTimeout := 30 * time.Second
|
||||
reportCtx, cancel := context.WithTimeout(context.Background(), reportTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := r.scriptCompleted(reportCtx, &proto.WorkspaceAgentScriptCompletedRequest{
|
||||
Timing: &proto.Timing{
|
||||
ScriptId: script.ID[:],
|
||||
Start: timestamppb.New(start),
|
||||
End: timestamppb.New(end),
|
||||
ExitCode: int32(exitCode),
|
||||
Stage: stage,
|
||||
Status: status,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("reporting script completed: %s", err.Error()))
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, fmt.Sprintf("reporting script completed: track command goroutine: %s", err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Start()
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
package agentscripts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"cdr.dev/slog"
|
||||
)
|
||||
|
||||
func cmdSysProcAttr() *syscall.SysProcAttr {
|
||||
@@ -16,9 +13,8 @@ func cmdSysProcAttr() *syscall.SysProcAttr {
|
||||
}
|
||||
}
|
||||
|
||||
func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error {
|
||||
func cmdCancel(cmd *exec.Cmd) func() error {
|
||||
return func() error {
|
||||
logger.Debug(ctx, "cmdCancel: sending SIGHUP to process and children", slog.F("pid", cmd.Process.Pid))
|
||||
return syscall.Kill(-cmd.Process.Pid, syscall.SIGHUP)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -16,17 +14,16 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent/agentscripts"
|
||||
"github.com/coder/coder/v2/agent/agentssh"
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestExecuteBasic(t *testing.T) {
|
||||
@@ -37,13 +34,14 @@ func TestExecuteBasic(t *testing.T) {
|
||||
return fLogger
|
||||
})
|
||||
defer runner.Close()
|
||||
aAPI := agenttest.NewFakeAgentAPI(t, testutil.Logger(t), nil, nil)
|
||||
err := runner.Init([]codersdk.WorkspaceAgentScript{{
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo hello",
|
||||
}}, aAPI.ScriptCompleted)
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, runner.Execute(context.Background(), agentscripts.ExecuteAllScripts))
|
||||
require.NoError(t, runner.Execute(context.Background(), func(script codersdk.WorkspaceAgentScript) bool {
|
||||
return true
|
||||
}))
|
||||
log := testutil.RequireRecvCtx(ctx, t, fLogger.logs)
|
||||
require.Equal(t, "hello", log.Output)
|
||||
}
|
||||
@@ -63,17 +61,18 @@ func TestEnv(t *testing.T) {
|
||||
cmd.exe /c echo %CODER_SCRIPT_BIN_DIR%
|
||||
`
|
||||
}
|
||||
aAPI := agenttest.NewFakeAgentAPI(t, testutil.Logger(t), nil, nil)
|
||||
err := runner.Init([]codersdk.WorkspaceAgentScript{{
|
||||
LogSourceID: id,
|
||||
Script: script,
|
||||
}}, aAPI.ScriptCompleted)
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
done := testutil.Go(t, func() {
|
||||
err := runner.Execute(ctx, agentscripts.ExecuteAllScripts)
|
||||
err := runner.Execute(ctx, func(script codersdk.WorkspaceAgentScript) bool {
|
||||
return true
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
defer func() {
|
||||
@@ -104,44 +103,13 @@ func TestTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
runner := setup(t, nil)
|
||||
defer runner.Close()
|
||||
aAPI := agenttest.NewFakeAgentAPI(t, testutil.Logger(t), nil, nil)
|
||||
err := runner.Init([]codersdk.WorkspaceAgentScript{{
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "sleep infinity",
|
||||
Timeout: time.Millisecond,
|
||||
}}, aAPI.ScriptCompleted)
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.ErrorIs(t, runner.Execute(context.Background(), agentscripts.ExecuteAllScripts), agentscripts.ErrTimeout)
|
||||
}
|
||||
|
||||
func TestScriptReportsTiming(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
fLogger := newFakeScriptLogger()
|
||||
runner := setup(t, func(uuid2 uuid.UUID) agentscripts.ScriptLogger {
|
||||
return fLogger
|
||||
})
|
||||
|
||||
aAPI := agenttest.NewFakeAgentAPI(t, testutil.Logger(t), nil, nil)
|
||||
err := runner.Init([]codersdk.WorkspaceAgentScript{{
|
||||
DisplayName: "say-hello",
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo hello",
|
||||
}}, aAPI.ScriptCompleted)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, runner.Execute(ctx, agentscripts.ExecuteAllScripts))
|
||||
runner.Close()
|
||||
|
||||
log := testutil.RequireRecvCtx(ctx, t, fLogger.logs)
|
||||
require.Equal(t, "hello", log.Output)
|
||||
|
||||
timings := aAPI.GetTimings()
|
||||
require.Equal(t, 1, len(timings))
|
||||
|
||||
timing := timings[0]
|
||||
require.Equal(t, int32(0), timing.ExitCode)
|
||||
require.GreaterOrEqual(t, timing.End.AsTime(), timing.Start.AsTime())
|
||||
require.ErrorIs(t, runner.Execute(context.Background(), nil), agentscripts.ErrTimeout)
|
||||
}
|
||||
|
||||
// TestCronClose exists because cron.Run() can happen after cron.Close().
|
||||
@@ -153,167 +121,17 @@ func TestCronClose(t *testing.T) {
|
||||
require.NoError(t, runner.Close(), "close runner")
|
||||
}
|
||||
|
||||
func TestExecuteOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
startScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo start",
|
||||
RunOnStart: true,
|
||||
}
|
||||
stopScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo stop",
|
||||
RunOnStop: true,
|
||||
}
|
||||
postStartScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo poststart",
|
||||
}
|
||||
regularScript := codersdk.WorkspaceAgentScript{
|
||||
ID: uuid.New(),
|
||||
LogSourceID: uuid.New(),
|
||||
Script: "echo regular",
|
||||
}
|
||||
|
||||
scripts := []codersdk.WorkspaceAgentScript{
|
||||
startScript,
|
||||
stopScript,
|
||||
regularScript,
|
||||
}
|
||||
allScripts := append(slices.Clone(scripts), postStartScript)
|
||||
|
||||
scriptByID := func(t *testing.T, id uuid.UUID) codersdk.WorkspaceAgentScript {
|
||||
for _, script := range allScripts {
|
||||
if script.ID == id {
|
||||
return script
|
||||
}
|
||||
}
|
||||
t.Fatal("script not found")
|
||||
return codersdk.WorkspaceAgentScript{}
|
||||
}
|
||||
|
||||
wantOutput := map[uuid.UUID]string{
|
||||
startScript.ID: "start",
|
||||
stopScript.ID: "stop",
|
||||
postStartScript.ID: "poststart",
|
||||
regularScript.ID: "regular",
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
option agentscripts.ExecuteOption
|
||||
wantRun []uuid.UUID
|
||||
}{
|
||||
{
|
||||
name: "ExecuteAllScripts",
|
||||
option: agentscripts.ExecuteAllScripts,
|
||||
wantRun: []uuid.UUID{startScript.ID, stopScript.ID, regularScript.ID, postStartScript.ID},
|
||||
},
|
||||
{
|
||||
name: "ExecuteStartScripts",
|
||||
option: agentscripts.ExecuteStartScripts,
|
||||
wantRun: []uuid.UUID{startScript.ID},
|
||||
},
|
||||
{
|
||||
name: "ExecutePostStartScripts",
|
||||
option: agentscripts.ExecutePostStartScripts,
|
||||
wantRun: []uuid.UUID{postStartScript.ID},
|
||||
},
|
||||
{
|
||||
name: "ExecuteStopScripts",
|
||||
option: agentscripts.ExecuteStopScripts,
|
||||
wantRun: []uuid.UUID{stopScript.ID},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
executedScripts := make(map[uuid.UUID]bool)
|
||||
fLogger := &executeOptionTestLogger{
|
||||
tb: t,
|
||||
executedScripts: executedScripts,
|
||||
wantOutput: wantOutput,
|
||||
}
|
||||
|
||||
runner := setup(t, func(uuid.UUID) agentscripts.ScriptLogger {
|
||||
return fLogger
|
||||
})
|
||||
defer runner.Close()
|
||||
|
||||
aAPI := agenttest.NewFakeAgentAPI(t, testutil.Logger(t), nil, nil)
|
||||
err := runner.Init(
|
||||
scripts,
|
||||
aAPI.ScriptCompleted,
|
||||
agentscripts.WithPostStartScripts(postStartScript),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = runner.Execute(ctx, tc.option)
|
||||
require.NoError(t, err)
|
||||
|
||||
gotRun := map[uuid.UUID]bool{}
|
||||
for _, id := range tc.wantRun {
|
||||
gotRun[id] = true
|
||||
require.True(t, executedScripts[id],
|
||||
"script %s should have run when using filter %s", scriptByID(t, id).Script, tc.name)
|
||||
}
|
||||
|
||||
for _, script := range allScripts {
|
||||
if _, ok := gotRun[script.ID]; ok {
|
||||
continue
|
||||
}
|
||||
require.False(t, executedScripts[script.ID],
|
||||
"script %s should not have run when using filter %s", script.Script, tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type executeOptionTestLogger struct {
|
||||
tb testing.TB
|
||||
executedScripts map[uuid.UUID]bool
|
||||
wantOutput map[uuid.UUID]string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (l *executeOptionTestLogger) Send(_ context.Context, logs ...agentsdk.Log) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
for _, log := range logs {
|
||||
l.tb.Log(log.Output)
|
||||
for id, output := range l.wantOutput {
|
||||
if log.Output == output {
|
||||
l.executedScripts[id] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*executeOptionTestLogger) Flush(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setup(t *testing.T, getScriptLogger func(logSourceID uuid.UUID) agentscripts.ScriptLogger) *agentscripts.Runner {
|
||||
t.Helper()
|
||||
if getScriptLogger == nil {
|
||||
// noop
|
||||
getScriptLogger = func(uuid.UUID) agentscripts.ScriptLogger {
|
||||
getScriptLogger = func(uuid uuid.UUID) agentscripts.ScriptLogger {
|
||||
return noopScriptLogger{}
|
||||
}
|
||||
}
|
||||
fs := afero.NewMemMapFs()
|
||||
logger := testutil.Logger(t)
|
||||
s, err := agentssh.NewServer(context.Background(), logger, prometheus.NewRegistry(), fs, agentexec.DefaultExecer, nil)
|
||||
logger := slogtest.Make(t, nil)
|
||||
s, err := agentssh.NewServer(context.Background(), logger, prometheus.NewRegistry(), fs, nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = s.Close()
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
package agentscripts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"cdr.dev/slog"
|
||||
)
|
||||
|
||||
func cmdSysProcAttr() *syscall.SysProcAttr {
|
||||
return &syscall.SysProcAttr{}
|
||||
}
|
||||
|
||||
func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error {
|
||||
func cmdCancel(cmd *exec.Cmd) func() error {
|
||||
return func() error {
|
||||
logger.Debug(ctx, "cmdCancel: sending interrupt to process", slog.F("pid", cmd.Process.Pid))
|
||||
return cmd.Process.Signal(os.Interrupt)
|
||||
}
|
||||
}
|
||||
|
||||
+94
-293
@@ -3,6 +3,8 @@ package agentssh
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,7 +14,6 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -29,9 +30,6 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentrsa"
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
@@ -43,6 +41,14 @@ const (
|
||||
// unlikely to shadow other exit codes, which are typically 1, 2, 3, etc.
|
||||
MagicSessionErrorCode = 229
|
||||
|
||||
// MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection.
|
||||
// This is stripped from any commands being executed, and is counted towards connection stats.
|
||||
MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE"
|
||||
// MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself.
|
||||
MagicSessionTypeVSCode = "vscode"
|
||||
// MagicSessionTypeJetBrains is set in the SSH config by the JetBrains
|
||||
// extension to identify itself.
|
||||
MagicSessionTypeJetBrains = "jetbrains"
|
||||
// MagicProcessCmdlineJetBrains is a string in a process's command line that
|
||||
// uniquely identifies it as JetBrains software.
|
||||
MagicProcessCmdlineJetBrains = "idea.vendor.name=JetBrains"
|
||||
@@ -53,42 +59,9 @@ const (
|
||||
BlockedFileTransferErrorMessage = "File transfer has been disabled."
|
||||
)
|
||||
|
||||
// MagicSessionType is a type that represents the type of session that is being
|
||||
// established.
|
||||
type MagicSessionType string
|
||||
|
||||
const (
|
||||
// MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection.
|
||||
// This is stripped from any commands being executed, and is counted towards connection stats.
|
||||
MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE"
|
||||
// ContainerEnvironmentVariable is used to specify the target container for an SSH connection.
|
||||
// This is stripped from any commands being executed.
|
||||
// Only available if CODER_AGENT_DEVCONTAINERS_ENABLE=true.
|
||||
ContainerEnvironmentVariable = "CODER_CONTAINER"
|
||||
// ContainerUserEnvironmentVariable is used to specify the container user for
|
||||
// an SSH connection.
|
||||
// Only available if CODER_AGENT_DEVCONTAINERS_ENABLE=true.
|
||||
ContainerUserEnvironmentVariable = "CODER_CONTAINER_USER"
|
||||
)
|
||||
|
||||
// MagicSessionType enums.
|
||||
const (
|
||||
// MagicSessionTypeUnknown means the session type could not be determined.
|
||||
MagicSessionTypeUnknown MagicSessionType = "unknown"
|
||||
// MagicSessionTypeSSH is the default session type.
|
||||
MagicSessionTypeSSH MagicSessionType = "ssh"
|
||||
// MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself.
|
||||
MagicSessionTypeVSCode MagicSessionType = "vscode"
|
||||
// MagicSessionTypeJetBrains is set in the SSH config by the JetBrains
|
||||
// extension to identify itself.
|
||||
MagicSessionTypeJetBrains MagicSessionType = "jetbrains"
|
||||
)
|
||||
|
||||
// BlockedFileTransferCommands contains a list of restricted file transfer commands.
|
||||
var BlockedFileTransferCommands = []string{"nc", "rsync", "scp", "sftp"}
|
||||
|
||||
type reportConnectionFunc func(id uuid.UUID, sessionType MagicSessionType, ip string) (disconnected func(code int, reason string))
|
||||
|
||||
// Config sets configuration parameters for the agent SSH server.
|
||||
type Config struct {
|
||||
// MaxTimeout sets the absolute connection timeout, none if empty. If set to
|
||||
@@ -106,16 +79,11 @@ type Config struct {
|
||||
// where users will land when they connect via SSH. Default is the home
|
||||
// directory of the user.
|
||||
WorkingDirectory func() string
|
||||
// X11DisplayOffset is the offset to add to the X11 display number.
|
||||
// Default is 10.
|
||||
X11DisplayOffset *int
|
||||
// X11SocketDir is the directory where X11 sockets are created. Default is
|
||||
// /tmp/.X11-unix.
|
||||
X11SocketDir string
|
||||
// BlockFileTransfer restricts use of file transfer applications.
|
||||
BlockFileTransfer bool
|
||||
// ReportConnection.
|
||||
ReportConnection reportConnectionFunc
|
||||
// Experimental: allow connecting to running containers if
|
||||
// CODER_AGENT_DEVCONTAINERS_ENABLE=true.
|
||||
ExperimentalDevContainersEnabled bool
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@@ -129,7 +97,6 @@ type Server struct {
|
||||
// a lock on mu but protected by closing.
|
||||
wg sync.WaitGroup
|
||||
|
||||
Execer agentexec.Execer
|
||||
logger slog.Logger
|
||||
srv *ssh.Server
|
||||
|
||||
@@ -142,13 +109,23 @@ type Server struct {
|
||||
metrics *sshServerMetrics
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prometheus.Registry, fs afero.Fs, execer agentexec.Execer, config *Config) (*Server, error) {
|
||||
func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prometheus.Registry, fs afero.Fs, config *Config) (*Server, error) {
|
||||
// Clients' should ignore the host key when connecting.
|
||||
// The agent needs to authenticate with coderd to SSH,
|
||||
// so SSH authentication doesn't improve security.
|
||||
randomHostKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
randomSigner, err := gossh.NewSignerFromKey(randomHostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
config = &Config{}
|
||||
}
|
||||
if config.X11DisplayOffset == nil {
|
||||
offset := X11DefaultDisplayOffset
|
||||
config.X11DisplayOffset = &offset
|
||||
if config.X11SocketDir == "" {
|
||||
config.X11SocketDir = filepath.Join(os.TempDir(), ".X11-unix")
|
||||
}
|
||||
if config.UpdateEnv == nil {
|
||||
config.UpdateEnv = func(current []string) ([]string, error) { return current, nil }
|
||||
@@ -168,16 +145,12 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
return home
|
||||
}
|
||||
}
|
||||
if config.ReportConnection == nil {
|
||||
config.ReportConnection = func(uuid.UUID, MagicSessionType, string) func(int, string) { return func(int, string) {} }
|
||||
}
|
||||
|
||||
forwardHandler := &ssh.ForwardedTCPHandler{}
|
||||
unixForwardHandler := newForwardedUnixHandler(logger)
|
||||
|
||||
metrics := newSSHServerMetrics(prometheusRegistry)
|
||||
s := &Server{
|
||||
Execer: execer,
|
||||
listeners: make(map[net.Listener]struct{}),
|
||||
fs: fs,
|
||||
conns: make(map[net.Conn]struct{}),
|
||||
@@ -193,7 +166,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"direct-tcpip": func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
||||
// Wrapper is designed to find and track JetBrains Gateway connections.
|
||||
wrapped := NewJetbrainsChannelWatcher(ctx, s.logger, s.config.ReportConnection, newChan, &s.connCountJetBrains)
|
||||
wrapped := NewJetbrainsChannelWatcher(ctx, s.logger, newChan, &s.connCountJetBrains)
|
||||
ssh.DirectTCPIPHandler(srv, conn, wrapped, ctx)
|
||||
},
|
||||
"direct-streamlocal@openssh.com": directStreamLocalHandler,
|
||||
@@ -212,10 +185,8 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
slog.F("local_addr", conn.LocalAddr()),
|
||||
slog.Error(err))
|
||||
},
|
||||
Handler: s.sessionHandler,
|
||||
// HostSigners are intentionally empty, as the host key will
|
||||
// be set before we start listening.
|
||||
HostSigners: []ssh.Signer{},
|
||||
Handler: s.sessionHandler,
|
||||
HostSigners: []ssh.Signer{randomSigner},
|
||||
LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
// Allow local port forwarding all!
|
||||
s.logger.Debug(ctx, "local port forward",
|
||||
@@ -223,7 +194,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
slog.F("destination_port", destinationPort))
|
||||
return true
|
||||
},
|
||||
PtyCallback: func(_ ssh.Context, _ ssh.Pty) bool {
|
||||
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool {
|
||||
return true
|
||||
},
|
||||
ReversePortForwardingCallback: func(ctx ssh.Context, bindHost string, bindPort uint32) bool {
|
||||
@@ -240,7 +211,7 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
|
||||
"cancel-streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
|
||||
},
|
||||
X11Callback: s.x11Callback,
|
||||
ServerConfigCallback: func(_ ssh.Context) *gossh.ServerConfig {
|
||||
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
|
||||
return &gossh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
}
|
||||
@@ -280,135 +251,35 @@ func (s *Server) ConnStats() ConnStats {
|
||||
}
|
||||
}
|
||||
|
||||
func extractMagicSessionType(env []string) (magicType MagicSessionType, rawType string, filteredEnv []string) {
|
||||
for _, kv := range env {
|
||||
if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) {
|
||||
continue
|
||||
}
|
||||
|
||||
rawType = strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"=")
|
||||
// Keep going, we'll use the last instance of the env.
|
||||
}
|
||||
|
||||
// Always force lowercase checking to be case-insensitive.
|
||||
switch MagicSessionType(strings.ToLower(rawType)) {
|
||||
case MagicSessionTypeVSCode:
|
||||
magicType = MagicSessionTypeVSCode
|
||||
case MagicSessionTypeJetBrains:
|
||||
magicType = MagicSessionTypeJetBrains
|
||||
case "", MagicSessionTypeSSH:
|
||||
magicType = MagicSessionTypeSSH
|
||||
default:
|
||||
magicType = MagicSessionTypeUnknown
|
||||
}
|
||||
|
||||
return magicType, rawType, slices.DeleteFunc(env, func(kv string) bool {
|
||||
return strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable+"=")
|
||||
})
|
||||
}
|
||||
|
||||
// sessionCloseTracker is a wrapper around Session that tracks the exit code.
|
||||
type sessionCloseTracker struct {
|
||||
ssh.Session
|
||||
exitOnce sync.Once
|
||||
code atomic.Int64
|
||||
}
|
||||
|
||||
var _ ssh.Session = &sessionCloseTracker{}
|
||||
|
||||
func (s *sessionCloseTracker) track(code int) {
|
||||
s.exitOnce.Do(func() {
|
||||
s.code.Store(int64(code))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sessionCloseTracker) exitCode() int {
|
||||
return int(s.code.Load())
|
||||
}
|
||||
|
||||
func (s *sessionCloseTracker) Exit(code int) error {
|
||||
s.track(code)
|
||||
return s.Session.Exit(code)
|
||||
}
|
||||
|
||||
func (s *sessionCloseTracker) Close() error {
|
||||
s.track(1)
|
||||
return s.Session.Close()
|
||||
}
|
||||
|
||||
func extractContainerInfo(env []string) (container, containerUser string, filteredEnv []string) {
|
||||
for _, kv := range env {
|
||||
if strings.HasPrefix(kv, ContainerEnvironmentVariable+"=") {
|
||||
container = strings.TrimPrefix(kv, ContainerEnvironmentVariable+"=")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(kv, ContainerUserEnvironmentVariable+"=") {
|
||||
containerUser = strings.TrimPrefix(kv, ContainerUserEnvironmentVariable+"=")
|
||||
}
|
||||
}
|
||||
|
||||
return container, containerUser, slices.DeleteFunc(env, func(kv string) bool {
|
||||
return strings.HasPrefix(kv, ContainerEnvironmentVariable+"=") || strings.HasPrefix(kv, ContainerUserEnvironmentVariable+"=")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) sessionHandler(session ssh.Session) {
|
||||
ctx := session.Context()
|
||||
id := uuid.New()
|
||||
logger := s.logger.With(
|
||||
slog.F("remote_addr", session.RemoteAddr()),
|
||||
slog.F("local_addr", session.LocalAddr()),
|
||||
// Assigning a random uuid for each session is useful for tracking
|
||||
// logs for the same ssh session.
|
||||
slog.F("id", id.String()),
|
||||
slog.F("id", uuid.NewString()),
|
||||
)
|
||||
logger.Info(ctx, "handling ssh session")
|
||||
|
||||
env := session.Environ()
|
||||
magicType, magicTypeRaw, env := extractMagicSessionType(env)
|
||||
|
||||
if !s.trackSession(session, true) {
|
||||
reason := "unable to accept new session, server is closing"
|
||||
// Report connection attempt even if we couldn't accept it.
|
||||
disconnected := s.config.ReportConnection(id, magicType, session.RemoteAddr().String())
|
||||
defer disconnected(1, reason)
|
||||
|
||||
logger.Info(ctx, reason)
|
||||
// See (*Server).Close() for why we call Close instead of Exit.
|
||||
_ = session.Close()
|
||||
logger.Info(ctx, "unable to accept new session, server is closing")
|
||||
return
|
||||
}
|
||||
defer s.trackSession(session, false)
|
||||
|
||||
reportSession := true
|
||||
|
||||
switch magicType {
|
||||
case MagicSessionTypeVSCode:
|
||||
s.connCountVSCode.Add(1)
|
||||
defer s.connCountVSCode.Add(-1)
|
||||
case MagicSessionTypeJetBrains:
|
||||
// Do nothing here because JetBrains launches hundreds of ssh sessions.
|
||||
// We instead track JetBrains in the single persistent tcp forwarding channel.
|
||||
reportSession = false
|
||||
case MagicSessionTypeSSH:
|
||||
s.connCountSSHSession.Add(1)
|
||||
defer s.connCountSSHSession.Add(-1)
|
||||
case MagicSessionTypeUnknown:
|
||||
logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("raw_type", magicTypeRaw))
|
||||
}
|
||||
|
||||
closeCause := func(string) {}
|
||||
if reportSession {
|
||||
var reason string
|
||||
closeCause = func(r string) { reason = r }
|
||||
|
||||
scr := &sessionCloseTracker{Session: session}
|
||||
session = scr
|
||||
|
||||
disconnected := s.config.ReportConnection(id, magicType, session.RemoteAddr().String())
|
||||
defer func() {
|
||||
disconnected(scr.exitCode(), reason)
|
||||
}()
|
||||
extraEnv := make([]string, 0)
|
||||
x11, hasX11 := session.X11()
|
||||
if hasX11 {
|
||||
handled := s.x11Handler(session.Context(), x11)
|
||||
if !handled {
|
||||
_ = session.Exit(1)
|
||||
logger.Error(ctx, "x11 handler failed")
|
||||
return
|
||||
}
|
||||
extraEnv = append(extraEnv, fmt.Sprintf("DISPLAY=:%d.0", x11.ScreenNumber))
|
||||
}
|
||||
|
||||
if s.fileTransferBlocked(session) {
|
||||
@@ -419,52 +290,22 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||
errorMessage := fmt.Sprintf("\x02%s\n", BlockedFileTransferErrorMessage)
|
||||
_, _ = session.Write([]byte(errorMessage))
|
||||
}
|
||||
closeCause("file transfer blocked")
|
||||
_ = session.Exit(BlockedFileTransferErrorCode)
|
||||
return
|
||||
}
|
||||
|
||||
container, containerUser, env := extractContainerInfo(env)
|
||||
if container != "" {
|
||||
s.logger.Debug(ctx, "container info",
|
||||
slog.F("container", container),
|
||||
slog.F("container_user", containerUser),
|
||||
)
|
||||
}
|
||||
|
||||
switch ss := session.Subsystem(); ss {
|
||||
case "":
|
||||
case "sftp":
|
||||
if s.config.ExperimentalDevContainersEnabled && container != "" {
|
||||
closeCause("sftp not yet supported with containers")
|
||||
_ = session.Exit(1)
|
||||
return
|
||||
}
|
||||
err := s.sftpHandler(logger, session)
|
||||
if err != nil {
|
||||
closeCause(err.Error())
|
||||
}
|
||||
s.sftpHandler(logger, session)
|
||||
return
|
||||
default:
|
||||
logger.Warn(ctx, "unsupported subsystem", slog.F("subsystem", ss))
|
||||
closeCause(fmt.Sprintf("unsupported subsystem: %s", ss))
|
||||
_ = session.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
x11, hasX11 := session.X11()
|
||||
if hasX11 {
|
||||
display, handled := s.x11Handler(session.Context(), x11)
|
||||
if !handled {
|
||||
logger.Error(ctx, "x11 handler failed")
|
||||
closeCause("x11 handler failed")
|
||||
_ = session.Exit(1)
|
||||
return
|
||||
}
|
||||
env = append(env, fmt.Sprintf("DISPLAY=localhost:%d.%d", display, x11.ScreenNumber))
|
||||
}
|
||||
|
||||
err := s.sessionStart(logger, session, env, magicType, container, containerUser)
|
||||
err := s.sessionStart(logger, session, extraEnv)
|
||||
var exitError *exec.ExitError
|
||||
if xerrors.As(err, &exitError) {
|
||||
code := exitError.ExitCode()
|
||||
@@ -485,8 +326,6 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||
slog.F("exit_code", code),
|
||||
)
|
||||
|
||||
closeCause(fmt.Sprintf("process exited with error status: %d", exitError.ExitCode()))
|
||||
|
||||
// TODO(mafredri): For signal exit, there's also an "exit-signal"
|
||||
// request (session.Exit sends "exit-status"), however, since it's
|
||||
// not implemented on the session interface and not used by
|
||||
@@ -498,7 +337,6 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||
logger.Warn(ctx, "ssh session failed", slog.Error(err))
|
||||
// This exit code is designed to be unlikely to be confused for a legit exit code
|
||||
// from the process.
|
||||
closeCause(err.Error())
|
||||
_ = session.Exit(MagicSessionErrorCode)
|
||||
return
|
||||
}
|
||||
@@ -537,27 +375,42 @@ func (s *Server) fileTransferBlocked(session ssh.Session) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []string, magicType MagicSessionType, container, containerUser string) (retErr error) {
|
||||
func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, extraEnv []string) (retErr error) {
|
||||
ctx := session.Context()
|
||||
env := append(session.Environ(), extraEnv...)
|
||||
var magicType string
|
||||
for index, kv := range env {
|
||||
if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) {
|
||||
continue
|
||||
}
|
||||
magicType = strings.ToLower(strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"="))
|
||||
env = append(env[:index], env[index+1:]...)
|
||||
}
|
||||
|
||||
// Always force lowercase checking to be case-insensitive.
|
||||
switch magicType {
|
||||
case MagicSessionTypeVSCode:
|
||||
s.connCountVSCode.Add(1)
|
||||
defer s.connCountVSCode.Add(-1)
|
||||
case MagicSessionTypeJetBrains:
|
||||
// Do nothing here because JetBrains launches hundreds of ssh sessions.
|
||||
// We instead track JetBrains in the single persistent tcp forwarding channel.
|
||||
case "":
|
||||
s.connCountSSHSession.Add(1)
|
||||
defer s.connCountSSHSession.Add(-1)
|
||||
default:
|
||||
logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("type", magicType))
|
||||
}
|
||||
|
||||
magicTypeLabel := magicTypeMetricLabel(magicType)
|
||||
sshPty, windowSize, isPty := session.Pty()
|
||||
ptyLabel := "no"
|
||||
if isPty {
|
||||
ptyLabel = "yes"
|
||||
}
|
||||
|
||||
var ei usershell.EnvInfoer
|
||||
var err error
|
||||
if s.config.ExperimentalDevContainersEnabled && container != "" {
|
||||
ei, err = agentcontainers.EnvInfo(ctx, s.Execer, container, containerUser)
|
||||
if err != nil {
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "container_env_info").Add(1)
|
||||
return err
|
||||
}
|
||||
}
|
||||
cmd, err := s.CreateCommand(ctx, session.RawCommand(), env, ei)
|
||||
cmd, err := s.CreateCommand(ctx, session.RawCommand(), env)
|
||||
if err != nil {
|
||||
ptyLabel := "no"
|
||||
if isPty {
|
||||
ptyLabel = "yes"
|
||||
}
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "create_command").Add(1)
|
||||
return err
|
||||
}
|
||||
@@ -565,6 +418,11 @@ func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []str
|
||||
if ssh.AgentRequested(session) {
|
||||
l, err := ssh.NewAgentListener()
|
||||
if err != nil {
|
||||
ptyLabel := "no"
|
||||
if isPty {
|
||||
ptyLabel = "yes"
|
||||
}
|
||||
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "listener").Add(1)
|
||||
return xerrors.Errorf("new agent listener: %w", err)
|
||||
}
|
||||
@@ -611,7 +469,7 @@ func (s *Server) startNonPTYSession(logger slog.Logger, session ssh.Session, mag
|
||||
}()
|
||||
go func() {
|
||||
for sig := range sigs {
|
||||
handleSignal(logger, sig, cmd.Process, s.metrics, magicTypeLabel)
|
||||
s.handleSignal(logger, sig, cmd.Process, magicTypeLabel)
|
||||
}
|
||||
}()
|
||||
return cmd.Wait()
|
||||
@@ -696,13 +554,12 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
|
||||
sigs = nil
|
||||
continue
|
||||
}
|
||||
handleSignal(logger, sig, process, s.metrics, magicTypeLabel)
|
||||
s.handleSignal(logger, sig, process, magicTypeLabel)
|
||||
case win, ok := <-windowSize:
|
||||
if !ok {
|
||||
windowSize = nil
|
||||
continue
|
||||
}
|
||||
// #nosec G115 - Safe conversions for terminal dimensions which are expected to be within uint16 range
|
||||
resizeErr := ptty.Resize(uint16(win.Height), uint16(win.Width))
|
||||
// If the pty is closed, then command has exited, no need to log.
|
||||
if resizeErr != nil && !errors.Is(resizeErr, pty.ErrClosed) {
|
||||
@@ -751,7 +608,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSignal(logger slog.Logger, ssig ssh.Signal, signaler interface{ Signal(os.Signal) error }, metrics *sshServerMetrics, magicTypeLabel string) {
|
||||
func (s *Server) handleSignal(logger slog.Logger, ssig ssh.Signal, signaler interface{ Signal(os.Signal) error }, magicTypeLabel string) {
|
||||
ctx := context.Background()
|
||||
sig := osSignalFrom(ssig)
|
||||
logger = logger.With(slog.F("ssh_signal", ssig), slog.F("signal", sig.String()))
|
||||
@@ -759,11 +616,11 @@ func handleSignal(logger slog.Logger, ssig ssh.Signal, signaler interface{ Signa
|
||||
err := signaler.Signal(sig)
|
||||
if err != nil {
|
||||
logger.Warn(ctx, "signaling the process failed", slog.Error(err))
|
||||
metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "signal").Add(1)
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "signal").Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) error {
|
||||
func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) {
|
||||
s.metrics.sftpConnectionsTotal.Add(1)
|
||||
|
||||
ctx := session.Context()
|
||||
@@ -787,7 +644,7 @@ func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) error {
|
||||
server, err := sftp.NewServer(session, opts...)
|
||||
if err != nil {
|
||||
logger.Debug(ctx, "initialize sftp server", slog.Error(err))
|
||||
return xerrors.Errorf("initialize sftp server: %w", err)
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
@@ -802,32 +659,24 @@ func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) error {
|
||||
// code but `scp` on macOS does (when using the default
|
||||
// SFTP backend).
|
||||
_ = session.Exit(0)
|
||||
return nil
|
||||
return
|
||||
}
|
||||
logger.Warn(ctx, "sftp server closed with error", slog.Error(err))
|
||||
s.metrics.sftpServerErrors.Add(1)
|
||||
_ = session.Exit(1)
|
||||
return xerrors.Errorf("sftp server closed with error: %w", err)
|
||||
}
|
||||
|
||||
// CreateCommand processes raw command input with OpenSSH-like behavior.
|
||||
// If the script provided is empty, it will default to the users shell.
|
||||
// This injects environment variables specified by the user at launch too.
|
||||
// The final argument is an interface that allows the caller to provide
|
||||
// alternative implementations for the dependencies of CreateCommand.
|
||||
// This is useful when creating a command to be run in a separate environment
|
||||
// (for example, a Docker container). Pass in nil to use the default.
|
||||
func (s *Server) CreateCommand(ctx context.Context, script string, env []string, ei usershell.EnvInfoer) (*pty.Cmd, error) {
|
||||
if ei == nil {
|
||||
ei = &usershell.SystemEnvInfo{}
|
||||
}
|
||||
currentUser, err := ei.User()
|
||||
func (s *Server) CreateCommand(ctx context.Context, script string, env []string) (*pty.Cmd, error) {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get current user: %w", err)
|
||||
}
|
||||
username := currentUser.Username
|
||||
|
||||
shell, err := ei.Shell(username)
|
||||
shell, err := usershell.Get(username)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get user shell: %w", err)
|
||||
}
|
||||
@@ -875,18 +724,7 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string,
|
||||
}
|
||||
}
|
||||
|
||||
// Modify command prior to execution. This will usually be a no-op, but not
|
||||
// always. For example, to run a command in a Docker container, we need to
|
||||
// modify the command to be `docker exec -it <container> <command>`.
|
||||
modifiedName, modifiedArgs := ei.ModifyCommand(name, args...)
|
||||
// Log if the command was modified.
|
||||
if modifiedName != name && slices.Compare(modifiedArgs, args) != 0 {
|
||||
s.logger.Debug(ctx, "modified command",
|
||||
slog.F("before", append([]string{name}, args...)),
|
||||
slog.F("after", append([]string{modifiedName}, modifiedArgs...)),
|
||||
)
|
||||
}
|
||||
cmd := s.Execer.PTYCommandContext(ctx, modifiedName, modifiedArgs...)
|
||||
cmd := pty.CommandContext(ctx, name, args...)
|
||||
cmd.Dir = s.config.WorkingDirectory()
|
||||
|
||||
// If the metadata directory doesn't exist, we run the command
|
||||
@@ -894,17 +732,14 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string,
|
||||
_, err = os.Stat(cmd.Dir)
|
||||
if cmd.Dir == "" || err != nil {
|
||||
// Default to user home if a directory is not set.
|
||||
homedir, err := ei.HomeDir()
|
||||
homedir, err := userHomeDir()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get home dir: %w", err)
|
||||
}
|
||||
cmd.Dir = homedir
|
||||
}
|
||||
cmd.Env = append(ei.Environ(), env...)
|
||||
// Set login variables (see `man login`).
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", username))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("LOGNAME=%s", username))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("SHELL=%s", shell))
|
||||
|
||||
// Set SSH connection environment variables (these are also set by OpenSSH
|
||||
// and thus expected to be present by SSH clients). Since the agent does
|
||||
@@ -923,13 +758,7 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string,
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// Serve starts the server to handle incoming connections on the provided listener.
|
||||
// It returns an error if no host keys are set or if there is an issue accepting connections.
|
||||
func (s *Server) Serve(l net.Listener) (retErr error) {
|
||||
if len(s.srv.HostSigners) == 0 {
|
||||
return xerrors.New("no host keys set")
|
||||
}
|
||||
|
||||
s.logger.Info(context.Background(), "started serving listener", slog.F("listen_addr", l.Addr()))
|
||||
defer func() {
|
||||
s.logger.Info(context.Background(), "stopped serving listener",
|
||||
@@ -1184,31 +1013,3 @@ func userHomeDir() (string, error) {
|
||||
}
|
||||
return u.HomeDir, nil
|
||||
}
|
||||
|
||||
// UpdateHostSigner updates the host signer with a new key generated from the provided seed.
|
||||
// If an existing host key exists with the same algorithm, it is overwritten
|
||||
func (s *Server) UpdateHostSigner(seed int64) error {
|
||||
key, err := CoderSigner(seed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.srv.AddHostKey(key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CoderSigner generates a deterministic SSH signer based on the provided seed.
|
||||
// It uses RSA with a key size of 2048 bits.
|
||||
func CoderSigner(seed int64) (gossh.Signer, error) {
|
||||
// Clients should ignore the host key when connecting.
|
||||
// The agent needs to authenticate with coderd to SSH,
|
||||
// so SSH authentication doesn't improve security.
|
||||
coderHostKey := agentrsa.GenerateDeterministicKey(seed)
|
||||
|
||||
coderSigner, err := gossh.NewSignerFromKey(coderHostKey)
|
||||
return coderSigner, err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user