Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d37eb42e7 | |||
| e7033b34dc | |||
| 01c6266e3e | |||
| f9011dcba2 | |||
| ae1be27ba6 | |||
| c4a01a42ce | |||
| c0aeb2fc2e | |||
| 908d236a19 | |||
| f519db88fb | |||
| e996e8b7e8 | |||
| da60671b33 | |||
| 963a1404c0 | |||
| 002110228c |
-124
@@ -1,124 +0,0 @@
|
||||
# Cursor Rules
|
||||
|
||||
This project is called "Coder" - an application for managing remote development environments.
|
||||
|
||||
Coder provides a platform for creating, managing, and using remote development environments (also known as Cloud Development Environments or CDEs). It leverages Terraform to define and provision these environments, which are referred to as "workspaces" within the project. The system is designed to be extensible, secure, and provide developers with a seamless remote development experience.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
The heart of Coder is a control plane that orchestrates the creation and management of workspaces. This control plane interacts with separate Provisioner processes over gRPC to handle workspace builds. The Provisioners consume workspace definitions and use Terraform to create the actual infrastructure.
|
||||
|
||||
The CLI package serves dual purposes - it can be used to launch the control plane itself and also provides client functionality for users to interact with an existing control plane instance. All user-facing frontend code is developed in TypeScript using React and lives in the `site/` directory.
|
||||
|
||||
The database layer uses PostgreSQL with SQLC for generating type-safe database code. Database migrations are carefully managed to ensure both forward and backward compatibility through paired `.up.sql` and `.down.sql` files.
|
||||
|
||||
## API Design
|
||||
|
||||
Coder's API architecture combines REST and gRPC approaches. The REST API is defined in `coderd/coderd.go` and uses Chi for HTTP routing. This provides the primary interface for the frontend and external integrations.
|
||||
|
||||
Internal communication with Provisioners occurs over gRPC, with service definitions maintained in `.proto` files. This separation allows for efficient binary communication with the components responsible for infrastructure management while providing a standard REST interface for human-facing applications.
|
||||
|
||||
## Network Architecture
|
||||
|
||||
Coder implements a secure networking layer based on Tailscale's Wireguard implementation. The `tailnet` package provides connectivity between workspace agents and clients through DERP (Designated Encrypted Relay for Packets) servers when direct connections aren't possible. This creates a secure overlay network allowing access to workspaces regardless of network topology, firewalls, or NAT configurations.
|
||||
|
||||
### Tailnet and DERP System
|
||||
|
||||
The networking system has three key components:
|
||||
|
||||
1. **Tailnet**: An overlay network implemented in the `tailnet` package that provides secure, end-to-end encrypted connections between clients, the Coder server, and workspace agents.
|
||||
|
||||
2. **DERP Servers**: These relay traffic when direct connections aren't possible. Coder provides several options:
|
||||
- A built-in DERP server that runs on the Coder control plane
|
||||
- Integration with Tailscale's global DERP infrastructure
|
||||
- Support for custom DERP servers for lower latency or offline deployments
|
||||
|
||||
3. **Direct Connections**: When possible, the system establishes peer-to-peer connections between clients and workspaces using STUN for NAT traversal. This requires both endpoints to send UDP traffic on ephemeral ports.
|
||||
|
||||
### Workspace Proxies
|
||||
|
||||
Workspace proxies (in the Enterprise edition) provide regional relay points for browser-based connections, reducing latency for geo-distributed teams. Key characteristics:
|
||||
|
||||
- Deployed as independent servers that authenticate with the Coder control plane
|
||||
- Relay connections for SSH, workspace apps, port forwarding, and web terminals
|
||||
- Do not make direct database connections
|
||||
- Managed through the `coder wsproxy` commands
|
||||
- Implemented primarily in the `enterprise/wsproxy/` package
|
||||
|
||||
## Agent System
|
||||
|
||||
The workspace agent runs within each provisioned workspace and provides core functionality including:
|
||||
|
||||
- SSH access to workspaces via the `agentssh` package
|
||||
- Port forwarding
|
||||
- Terminal connectivity via the `pty` package for pseudo-terminal support
|
||||
- Application serving
|
||||
- Healthcheck monitoring
|
||||
- Resource usage reporting
|
||||
|
||||
Agents communicate with the control plane using the tailnet system and authenticate using secure tokens.
|
||||
|
||||
## Workspace Applications
|
||||
|
||||
Workspace applications (or "apps") provide browser-based access to services running within workspaces. The system supports:
|
||||
|
||||
- HTTP(S) and WebSocket connections
|
||||
- Path-based or subdomain-based access URLs
|
||||
- Health checks to monitor application availability
|
||||
- Different sharing levels (owner-only, authenticated users, or public)
|
||||
- Custom icons and display settings
|
||||
|
||||
The implementation is primarily in the `coderd/workspaceapps/` directory with components for URL generation, proxying connections, and managing application state.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The project structure separates frontend and backend concerns. React components and pages are organized in the `site/src/` directory, with Jest used for testing. The backend is primarily written in Go, with a strong emphasis on error handling patterns and test coverage.
|
||||
|
||||
Database interactions are carefully managed through migrations in `coderd/database/migrations/` and queries in `coderd/database/queries/`. All new queries require proper database authorization (dbauthz) implementation to ensure that only users with appropriate permissions can access specific resources.
|
||||
|
||||
## Authorization System
|
||||
|
||||
The database authorization (dbauthz) system enforces fine-grained access control across all database operations. It uses role-based access control (RBAC) to validate user permissions before executing database operations. The `dbauthz` package wraps the database store and performs authorization checks before returning data. All database operations must pass through this layer to ensure security.
|
||||
|
||||
## Testing Framework
|
||||
|
||||
The codebase has a comprehensive testing approach with several key components:
|
||||
|
||||
1. **Parallel Testing**: All tests must use `t.Parallel()` to run concurrently, which improves test suite performance and helps identify race conditions.
|
||||
|
||||
2. **coderdtest Package**: This package in `coderd/coderdtest/` provides utilities for creating test instances of the Coder server, setting up test users and workspaces, and mocking external components.
|
||||
|
||||
3. **Integration Tests**: Tests often span multiple components to verify system behavior, such as template creation, workspace provisioning, and agent connectivity.
|
||||
|
||||
4. **Enterprise Testing**: Enterprise features have dedicated test utilities in the `coderdenttest` package.
|
||||
|
||||
## Open Source and Enterprise Components
|
||||
|
||||
The repository contains both open source and enterprise components:
|
||||
|
||||
- Enterprise code lives primarily in the `enterprise/` directory
|
||||
- Enterprise features focus on governance, scalability (high availability), and advanced deployment options like workspace proxies
|
||||
- The boundary between open source and enterprise is managed through a licensing system
|
||||
- The same core codebase supports both editions, with enterprise features conditionally enabled
|
||||
|
||||
## Development Philosophy
|
||||
|
||||
Coder emphasizes clear error handling, with specific patterns required:
|
||||
|
||||
- Concise error messages that avoid phrases like "failed to"
|
||||
- Wrapping errors with `%w` to maintain error chains
|
||||
- Using sentinel errors with the "err" prefix (e.g., `errNotFound`)
|
||||
|
||||
All tests should run in parallel using `t.Parallel()` to ensure efficient testing and expose potential race conditions. The codebase is rigorously linted with golangci-lint to maintain consistent code quality.
|
||||
|
||||
Git contributions follow a standard format with commit messages structured as `type: <message>`, where type is one of `feat`, `fix`, or `chore`.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Development can be initiated using `scripts/develop.sh` to start the application after making changes. Database schema updates should be performed through the migration system using `create_migration.sh <name>` to generate migration files, with each `.up.sql` migration paired with a corresponding `.down.sql` that properly reverts all changes.
|
||||
|
||||
If the development database gets into a bad state, it can be completely reset by removing the PostgreSQL data directory with `rm -rf .coderv2/postgres`. This will destroy all data in the development database, requiring you to recreate any test users, templates, or workspaces after restarting the application.
|
||||
|
||||
Code generation for the database layer uses `coderd/database/generate.sh`, and developers should refer to `sqlc.yaml` for the appropriate style and patterns to follow when creating new queries or tables.
|
||||
|
||||
The focus should always be on maintaining security through proper database authorization, clean error handling, and comprehensive test coverage to ensure the platform remains robust and reliable.
|
||||
@@ -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
-5
@@ -7,11 +7,7 @@ trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
|
||||
[*.{yaml,yml,tf,tfvars,nix}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.proto]
|
||||
[*.{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
-7
@@ -1,20 +1,15 @@
|
||||
# Generated files
|
||||
agent/agentcontainers/acmock/acmock.go linguist-generated=true
|
||||
agent/agentcontainers/dcspec/dcspec_gen.go linguist-generated=true
|
||||
agent/agentcontainers/testdata/devcontainercli/*/*.log 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
|
||||
provisionerd/proto/*.go linguist-generated=true
|
||||
provisionerd/proto/version.go linguist-generated=false
|
||||
provisionersdk/proto/*.go linguist-generated=true
|
||||
*.tfplan.json linguist-generated=true
|
||||
*.tfstate.json linguist-generated=true
|
||||
*.tfstate.dot linguist-generated=true
|
||||
*.tfplan.dot linguist-generated=true
|
||||
site/e2e/provisionerGenerated.ts linguist-generated=true
|
||||
site/src/api/typesGenerated.ts linguist-generated=true
|
||||
site/src/pages/SetupPage/countries.tsx linguist-generated=true
|
||||
|
||||
@@ -1,29 +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"
|
||||
- pattern: "docs.github.com"
|
||||
- pattern: "claude.ai"
|
||||
- pattern: "splunk.com"
|
||||
aliveStatusCodes:
|
||||
- 200
|
||||
@@ -1,79 +0,0 @@
|
||||
name: "🐞 Bug"
|
||||
description: "File a bug report."
|
||||
title: "bug: "
|
||||
labels: ["needs-triage"]
|
||||
type: "Bug"
|
||||
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"
|
||||
@@ -1,57 +0,0 @@
|
||||
name: "Setup Go Paths"
|
||||
description: Overrides Go paths like GOCACHE and GOMODCACHE to use temporary directories.
|
||||
outputs:
|
||||
gocache:
|
||||
description: "Value of GOCACHE"
|
||||
value: ${{ steps.paths.outputs.gocache }}
|
||||
gomodcache:
|
||||
description: "Value of GOMODCACHE"
|
||||
value: ${{ steps.paths.outputs.gomodcache }}
|
||||
gopath:
|
||||
description: "Value of GOPATH"
|
||||
value: ${{ steps.paths.outputs.gopath }}
|
||||
gotmp:
|
||||
description: "Value of GOTMPDIR"
|
||||
value: ${{ steps.paths.outputs.gotmp }}
|
||||
cached-dirs:
|
||||
description: "Go directories that should be cached between CI runs"
|
||||
value: ${{ steps.paths.outputs.cached-dirs }}
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Override Go paths
|
||||
id: paths
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const path = require('path');
|
||||
|
||||
// RUNNER_TEMP should be backed by a RAM disk on Windows if
|
||||
// coder/setup-ramdisk-action was used
|
||||
const runnerTemp = process.env.RUNNER_TEMP;
|
||||
const gocacheDir = path.join(runnerTemp, 'go-cache');
|
||||
const gomodcacheDir = path.join(runnerTemp, 'go-mod-cache');
|
||||
const gopathDir = path.join(runnerTemp, 'go-path');
|
||||
const gotmpDir = path.join(runnerTemp, 'go-tmp');
|
||||
|
||||
core.exportVariable('GOCACHE', gocacheDir);
|
||||
core.exportVariable('GOMODCACHE', gomodcacheDir);
|
||||
core.exportVariable('GOPATH', gopathDir);
|
||||
core.exportVariable('GOTMPDIR', gotmpDir);
|
||||
|
||||
core.setOutput('gocache', gocacheDir);
|
||||
core.setOutput('gomodcache', gomodcacheDir);
|
||||
core.setOutput('gopath', gopathDir);
|
||||
core.setOutput('gotmp', gotmpDir);
|
||||
|
||||
const cachedDirs = `${gocacheDir}\n${gomodcacheDir}`;
|
||||
core.setOutput('cached-dirs', cachedDirs);
|
||||
|
||||
- name: Create directories
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
mkdir -p "$GOCACHE"
|
||||
mkdir -p "$GOMODCACHE"
|
||||
mkdir -p "$GOPATH"
|
||||
mkdir -p "$GOTMPDIR"
|
||||
@@ -1,14 +0,0 @@
|
||||
name: "Setup Go tools"
|
||||
description: |
|
||||
Set up tools for `make gen`, `offlinedocs` and Schmoder CI.
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: go install tools
|
||||
shell: bash
|
||||
run: |
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
|
||||
go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34
|
||||
go install golang.org/x/tools/cmd/goimports@v0.31.0
|
||||
go install github.com/mikefarah/yq/v4@v4.44.3
|
||||
go install go.uber.org/mock/mockgen@v0.5.0
|
||||
@@ -4,29 +4,18 @@ description: |
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.24.6"
|
||||
use-preinstalled-go:
|
||||
description: "Whether to use preinstalled Go."
|
||||
default: "false"
|
||||
use-cache:
|
||||
description: "Whether to use the cache."
|
||||
default: "true"
|
||||
default: "1.21.5"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
|
||||
uses: buildjet/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ inputs.use-preinstalled-go == 'false' && inputs.version || '' }}
|
||||
cache: ${{ inputs.use-cache }}
|
||||
go-version: ${{ inputs.version }}
|
||||
|
||||
- name: Install gotestsum
|
||||
shell: bash
|
||||
run: go install gotest.tools/gotestsum@0d9599e513d70e5792bb9334869f82f6e8b53d4d # main as of 2025-05-15
|
||||
|
||||
- name: Install mtimehash
|
||||
shell: bash
|
||||
run: go install github.com/slsyy/mtimehash/cmd/mtimehash@a6b5da4ed2c4a40e7b805534b004e9fde7b53ce0 # v1.0.0
|
||||
run: go install gotest.tools/gotestsum@latest
|
||||
|
||||
# It isn't necessary that we ever do this, but it helps
|
||||
# separate the "setup" from the "run" times.
|
||||
|
||||
@@ -11,16 +11,16 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
node-version: 20.16.0
|
||||
version: 8
|
||||
- name: Setup Node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 18.19.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.12.2
|
||||
terraform_version: 1.5.7
|
||||
terraform_wrapper: false
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: "Download Test Cache"
|
||||
description: |
|
||||
Downloads the test cache and outputs today's cache key.
|
||||
A PR job can use a cache if it was created by its base branch, its current
|
||||
branch, or the default branch.
|
||||
https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
|
||||
outputs:
|
||||
cache-key:
|
||||
description: "Today's cache key"
|
||||
value: ${{ steps.vars.outputs.cache-key }}
|
||||
inputs:
|
||||
key-prefix:
|
||||
description: "Prefix for the cache key"
|
||||
required: true
|
||||
cache-path:
|
||||
description: "Path to the cache directory"
|
||||
required: true
|
||||
# This path is defined in testutil/cache.go
|
||||
default: "~/.cache/coderv2-test"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Get date values and cache key
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
export YEAR_MONTH=$(date +'%Y-%m')
|
||||
export PREV_YEAR_MONTH=$(date -d 'last month' +'%Y-%m')
|
||||
export DAY=$(date +'%d')
|
||||
echo "year-month=$YEAR_MONTH" >> $GITHUB_OUTPUT
|
||||
echo "prev-year-month=$PREV_YEAR_MONTH" >> $GITHUB_OUTPUT
|
||||
echo "cache-key=${{ inputs.key-prefix }}-${YEAR_MONTH}-${DAY}" >> $GITHUB_OUTPUT
|
||||
|
||||
# TODO: As a cost optimization, we could remove caches that are older than
|
||||
# a day or two. By default, depot keeps caches for 14 days, which isn't
|
||||
# necessary for the test cache.
|
||||
# https://depot.dev/docs/github-actions/overview#cache-retention-policy
|
||||
- name: Download test cache
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ${{ inputs.cache-path }}
|
||||
key: ${{ steps.vars.outputs.cache-key }}
|
||||
# > If there are multiple partial matches for a restore key, the action returns the most recently created cache.
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#matching-a-cache-key
|
||||
# The second restore key allows non-main branches to use the cache from the previous month.
|
||||
# This prevents PRs from rebuilding the cache on the first day of the month.
|
||||
# It also makes sure that once a month, the cache is fully reset.
|
||||
restore-keys: |
|
||||
${{ inputs.key-prefix }}-${{ steps.vars.outputs.year-month }}-
|
||||
${{ github.ref != 'refs/heads/main' && format('{0}-{1}-', inputs.key-prefix, steps.vars.outputs.prev-year-month) || '' }}
|
||||
@@ -1,20 +0,0 @@
|
||||
name: "Upload Test Cache"
|
||||
description: Uploads the test cache. Only works on the main branch.
|
||||
inputs:
|
||||
cache-key:
|
||||
description: "Cache key"
|
||||
required: true
|
||||
cache-path:
|
||||
description: "Path to the cache directory"
|
||||
required: true
|
||||
# This path is defined in testutil/cache.go
|
||||
default: "~/.cache/coderv2-test"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Upload test cache
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ${{ inputs.cache-path }}
|
||||
key: ${{ inputs.cache-key }}
|
||||
@@ -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"
|
||||
@@ -10,8 +9,6 @@ runs:
|
||||
steps:
|
||||
- shell: bash
|
||||
run: |
|
||||
set -e
|
||||
|
||||
owner=${{ github.repository_owner }}
|
||||
echo "owner: $owner"
|
||||
if [[ $owner != "coder" ]]; then
|
||||
@@ -23,45 +20,8 @@ runs:
|
||||
echo "No API key provided, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BINARY_VERSION="v2.48.0"
|
||||
BINARY_HASH_WINDOWS="b7bebb8212403fddb1563bae84ce5e69a70dac11e35eb07a00c9ef7ac9ed65ea"
|
||||
BINARY_HASH_MACOS="e87c808638fddb21a87a5c4584b68ba802965eb0a593d43959c81f67246bd9eb"
|
||||
BINARY_HASH_LINUX="5e700c465728fff8313e77c2d5ba1ce19a736168735137e1ddc7c6346ed48208"
|
||||
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
||||
if [[ "${{ runner.os }}" == "Windows" ]]; then
|
||||
BINARY_PATH="${TMP_DIR}/datadog-ci.exe"
|
||||
BINARY_URL="https://github.com/DataDog/datadog-ci/releases/download/${BINARY_VERSION}/datadog-ci_win-x64"
|
||||
elif [[ "${{ runner.os }}" == "macOS" ]]; then
|
||||
BINARY_PATH="${TMP_DIR}/datadog-ci"
|
||||
BINARY_URL="https://github.com/DataDog/datadog-ci/releases/download/${BINARY_VERSION}/datadog-ci_darwin-arm64"
|
||||
elif [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
BINARY_PATH="${TMP_DIR}/datadog-ci"
|
||||
BINARY_URL="https://github.com/DataDog/datadog-ci/releases/download/${BINARY_VERSION}/datadog-ci_linux-x64"
|
||||
else
|
||||
echo "Unsupported OS: ${{ runner.os }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Downloading DataDog CI binary version ${BINARY_VERSION} for ${{ runner.os }}..."
|
||||
curl -sSL "$BINARY_URL" -o "$BINARY_PATH"
|
||||
|
||||
if [[ "${{ runner.os }}" == "Windows" ]]; then
|
||||
echo "$BINARY_HASH_WINDOWS $BINARY_PATH" | sha256sum --check
|
||||
elif [[ "${{ runner.os }}" == "macOS" ]]; then
|
||||
echo "$BINARY_HASH_MACOS $BINARY_PATH" | shasum -a 256 --check
|
||||
elif [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
echo "$BINARY_HASH_LINUX $BINARY_PATH" | sha256sum --check
|
||||
fi
|
||||
|
||||
# Make binary executable (not needed for Windows)
|
||||
if [[ "${{ runner.os }}" != "Windows" ]]; then
|
||||
chmod +x "$BINARY_PATH"
|
||||
fi
|
||||
|
||||
"$BINARY_PATH" junit upload --service coder ./gotests.xml \
|
||||
npm install -g @datadog/datadog-ci@2.21.0
|
||||
datadog-ci junit upload --service coder ./gotests.xml \
|
||||
--tags os:${{runner.os}} --tags runner_name:${{runner.name}}
|
||||
env:
|
||||
DATADOG_API_KEY: ${{ inputs.api-key }}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
enabled: true
|
||||
preservePullRequestTitle: true
|
||||
@@ -0,0 +1,43 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false
|
||||
notify:
|
||||
after_n_builds: 5
|
||||
|
||||
comment: false
|
||||
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
coverage:
|
||||
range: 50..75
|
||||
round: down
|
||||
precision: 2
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
informational: yes
|
||||
project:
|
||||
default:
|
||||
target: 65%
|
||||
informational: true
|
||||
|
||||
ignore:
|
||||
# This is generated code.
|
||||
- coderd/database/models.go
|
||||
- coderd/database/queries.sql.go
|
||||
- coderd/database/databasefake
|
||||
# These are generated or don't require tests.
|
||||
- cmd
|
||||
- coderd/tunnel
|
||||
- coderd/database/dump
|
||||
- coderd/database/postgres
|
||||
- peerbroker/proto
|
||||
- provisionerd/proto
|
||||
- provisionersdk/proto
|
||||
- scripts
|
||||
- site/.storybook
|
||||
- rules.go
|
||||
# Packages used for writing tests.
|
||||
- cli/clitest
|
||||
- coderd/coderdtest
|
||||
- pty/ptytest
|
||||
+58
-54
@@ -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:
|
||||
@@ -24,10 +39,6 @@ updates:
|
||||
prefix: "chore"
|
||||
labels: []
|
||||
open-pull-requests-limit: 15
|
||||
groups:
|
||||
x:
|
||||
patterns:
|
||||
- "golang.org/x/*"
|
||||
ignore:
|
||||
# Ignore patch updates for all dependencies
|
||||
- dependency-name: "*"
|
||||
@@ -36,14 +47,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"
|
||||
@@ -57,12 +61,7 @@ updates:
|
||||
- dependency-name: "terraform"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directories:
|
||||
- "/site"
|
||||
- "/offlinedocs"
|
||||
- "/scripts"
|
||||
- "/scripts/apidocgen"
|
||||
|
||||
directory: "/site/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
time: "06:00"
|
||||
@@ -72,53 +71,58 @@ updates:
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
labels: []
|
||||
groups:
|
||||
xterm:
|
||||
patterns:
|
||||
- "@xterm*"
|
||||
mui:
|
||||
patterns:
|
||||
- "@mui*"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
emotion:
|
||||
patterns:
|
||||
- "@emotion*"
|
||||
exclude-patterns:
|
||||
- "jest-runner-eslint"
|
||||
jest:
|
||||
patterns:
|
||||
- "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
|
||||
open-pull-requests-limit: 15
|
||||
groups:
|
||||
site:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "terraform"
|
||||
directories:
|
||||
- "dogfood/*/"
|
||||
- "examples/templates/*/"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/offlinedocs/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "monthly"
|
||||
time: "06:00"
|
||||
timezone: "America/Chicago"
|
||||
reviewers:
|
||||
- "coder/ts"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
groups:
|
||||
coder:
|
||||
patterns:
|
||||
- "registry.coder.com/coder/*/coder"
|
||||
labels: []
|
||||
ignore:
|
||||
# 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
|
||||
groups:
|
||||
offlinedocs:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
# Update dogfood.
|
||||
- package-ecosystem: "terraform"
|
||||
directory: "/dogfood/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
time: "06:00"
|
||||
timezone: "America/Chicago"
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
labels: []
|
||||
ignore:
|
||||
# We likely want to update this ourselves.
|
||||
- dependency-name: "coder/coder"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
app = "jnb-coder"
|
||||
primary_region = "jnb"
|
||||
|
||||
[experimental]
|
||||
entrypoint = ["/bin/sh", "-c", "CODER_DERP_SERVER_RELAY_URL=\"http://[${FLY_PRIVATE_IP}]:3000\" /opt/coder wsproxy server"]
|
||||
auto_rollback = true
|
||||
|
||||
[build]
|
||||
image = "ghcr.io/coder/coder-preview:main"
|
||||
|
||||
[env]
|
||||
CODER_ACCESS_URL = "https://jnb.fly.dev.coder.com"
|
||||
CODER_HTTP_ADDRESS = "0.0.0.0:3000"
|
||||
CODER_PRIMARY_ACCESS_URL = "https://dev.coder.com"
|
||||
CODER_WILDCARD_ACCESS_URL = "*--apps.jnb.fly.dev.coder.com"
|
||||
CODER_VERBOSE = "true"
|
||||
|
||||
[http_service]
|
||||
internal_port = 3000
|
||||
force_https = true
|
||||
auto_stop_machines = true
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
|
||||
# Ref: https://fly.io/docs/reference/configuration/#http_service-concurrency
|
||||
[http_service.concurrency]
|
||||
type = "requests"
|
||||
soft_limit = 50
|
||||
hard_limit = 100
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = "shared"
|
||||
cpus = 2
|
||||
memory_mb = 512
|
||||
@@ -22,12 +22,6 @@ primary_region = "cdg"
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
|
||||
# Ref: https://fly.io/docs/reference/configuration/#http_service-concurrency
|
||||
[http_service.concurrency]
|
||||
type = "requests"
|
||||
soft_limit = 50
|
||||
hard_limit = 100
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = "shared"
|
||||
cpus = 2
|
||||
|
||||
@@ -22,12 +22,6 @@ primary_region = "gru"
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
|
||||
# Ref: https://fly.io/docs/reference/configuration/#http_service-concurrency
|
||||
[http_service.concurrency]
|
||||
type = "requests"
|
||||
soft_limit = 50
|
||||
hard_limit = 100
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = "shared"
|
||||
cpus = 2
|
||||
|
||||
@@ -22,12 +22,6 @@ primary_region = "syd"
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
|
||||
# Ref: https://fly.io/docs/reference/configuration/#http_service-concurrency
|
||||
[http_service.concurrency]
|
||||
type = "requests"
|
||||
soft_limit = 50
|
||||
hard_limit = 100
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = "shared"
|
||||
cpus = 2
|
||||
|
||||
@@ -86,12 +86,12 @@ provider "kubernetes" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
startup_script = <<-EOT
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
startup_script_timeout = 180
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
|
||||
# install and start code-server
|
||||
@@ -176,21 +176,21 @@ resource "coder_app" "code-server" {
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-pvc"
|
||||
"app.kubernetes.io/instance" = "coder-pvc-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/instance" = "coder-pvc-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
//Coder-specific labels.
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
"com.coder.user.id" = data.coder_workspace.me.owner_id
|
||||
"com.coder.user.username" = data.coder_workspace.me.owner
|
||||
}
|
||||
annotations = {
|
||||
"com.coder.user.email" = data.coder_workspace_owner.me.email
|
||||
"com.coder.user.email" = data.coder_workspace.me.owner_email
|
||||
}
|
||||
}
|
||||
wait_until_bound = false
|
||||
@@ -211,20 +211,20 @@ resource "kubernetes_deployment" "main" {
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
name = "coder-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-workspace"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${lower(data.coder_workspace.me.owner)}-${lower(data.coder_workspace.me.name)}"
|
||||
"app.kubernetes.io/part-of" = "coder"
|
||||
"com.coder.resource" = "true"
|
||||
"com.coder.workspace.id" = data.coder_workspace.me.id
|
||||
"com.coder.workspace.name" = data.coder_workspace.me.name
|
||||
"com.coder.user.id" = data.coder_workspace_owner.me.id
|
||||
"com.coder.user.username" = data.coder_workspace_owner.me.name
|
||||
"com.coder.user.id" = data.coder_workspace.me.owner_id
|
||||
"com.coder.user.username" = data.coder_workspace.me.owner
|
||||
}
|
||||
annotations = {
|
||||
"com.coder.user.email" = data.coder_workspace_owner.me.email
|
||||
"com.coder.user.email" = data.coder_workspace.me.owner_email
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+180
-974
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.3.1
|
||||
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"
|
||||
@@ -42,17 +47,15 @@ jobs:
|
||||
# branch should not be protected
|
||||
branch: "main"
|
||||
# Some users have signed a corporate CLA with Coder so are exempt from signing our community one.
|
||||
allowlist: "coryb,aaronlehmann,dependabot*,blink-so*"
|
||||
allowlist: "coryb,aaronlehmann,dependabot*"
|
||||
|
||||
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@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.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 }}
|
||||
@@ -8,11 +8,6 @@ on:
|
||||
- scripts/Dockerfile.base
|
||||
- scripts/Dockerfile
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- scripts/Dockerfile.base
|
||||
- .github/workflows/docker-base.yaml
|
||||
|
||||
schedule:
|
||||
# Run every week at 09:43 on Monday, Wednesday and Friday. We build this
|
||||
# frequently to ensure that packages are up-to-date.
|
||||
@@ -22,6 +17,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 +28,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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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,25 +45,23 @@ 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@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.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' }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/coder/coder-base:latest
|
||||
|
||||
- name: Verify that images are pushed properly
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
# retry 10 times with a 5 second delay as the images may not be
|
||||
# available immediately
|
||||
|
||||
@@ -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@666c9d29007687c52e3c7aa2aac6c0ffcadeadc3 # 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,52 +17,16 @@ 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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Nix
|
||||
uses: nixbuild/nix-quick-install-action@63ca48f939ee3b8d835f4126562537df0fee5b91 # v32
|
||||
with:
|
||||
# Pinning to 2.28 here, as Nix gets a "error: [json.exception.type_error.302] type must be array, but is string"
|
||||
# on version 2.29 and above.
|
||||
nix_version: "2.28.4"
|
||||
|
||||
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
|
||||
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@dde14ac574a8b9b1cedc59a1cf312788af43d8d8 # v8.2.1
|
||||
uses: tj-actions/branch-names@v8
|
||||
|
||||
- name: "Branch name to Docker tag name"
|
||||
id: docker-tag-name
|
||||
@@ -73,81 +37,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@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
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@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10
|
||||
with:
|
||||
workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- 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'
|
||||
@@ -159,18 +100,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" --variable jfrog_url=${{ secrets.JFROG_URL }}
|
||||
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,23 @@
|
||||
{
|
||||
"ignorePatterns": [
|
||||
{
|
||||
"pattern": "://localhost"
|
||||
},
|
||||
{
|
||||
"pattern": "://.*.?example\\.com"
|
||||
},
|
||||
{
|
||||
"pattern": "developer.github.com"
|
||||
},
|
||||
{
|
||||
"pattern": "docs.github.com"
|
||||
},
|
||||
{
|
||||
"pattern": "support.google.com"
|
||||
},
|
||||
{
|
||||
"pattern": "tailscale.com"
|
||||
}
|
||||
],
|
||||
"aliveStatusCodes": [200, 0]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# The nightly-gauntlet runs tests that are either too flaky or too slow to block
|
||||
# every PR.
|
||||
name: nightly-gauntlet
|
||||
on:
|
||||
schedule:
|
||||
# Every day at midnight
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
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: "buildjet-8vcpu-ubuntu-2204"
|
||||
# This runner costs 0.016 USD per minute,
|
||||
# so 0.016 * 240 = 3.84 USD per run.
|
||||
timeout-minutes: 240
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: ./.github/actions/setup-tf
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
# -race is likeliest to catch flaky tests
|
||||
# due to correctness detection and its performance
|
||||
# impact.
|
||||
gotestsum --junitfile="gotests.xml" -- -timeout=240m -count=10 -race ./...
|
||||
|
||||
- name: Upload test results to DataDog
|
||||
uses: ./.github/actions/upload-datadog
|
||||
if: always()
|
||||
with:
|
||||
api-key: ${{ secrets.DATADOG_API_KEY }}
|
||||
|
||||
go-timing:
|
||||
# We run these tests with p=1 so we don't need a lot of compute.
|
||||
runs-on: "buildjet-2vcpu-ubuntu-2204"
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Assign author
|
||||
uses: toshimaru/auto-author-assign@16f0022cf3d7970c106d8d1105f75a1165edb516 # v2.1.1
|
||||
uses: toshimaru/auto-author-assign@v2.1.0
|
||||
|
||||
@@ -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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -110,8 +101,8 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG_BASE64 }}" | base64 --decode > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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 }}
|
||||
@@ -205,11 +189,8 @@ jobs:
|
||||
needs: get_info
|
||||
# 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.
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
# 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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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
|
||||
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.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'
|
||||
@@ -420,16 +389,16 @@ jobs:
|
||||
curl -fsSL "$URL" -o "${DEST}"
|
||||
chmod +x "${DEST}"
|
||||
"${DEST}" version
|
||||
sudo mv "${DEST}" /usr/local/bin/coder
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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
|
||||
+48
-488
@@ -1,16 +1,11 @@
|
||||
# GitHub release workflow.
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_channel:
|
||||
type: choice
|
||||
description: Release channel
|
||||
options:
|
||||
- mainline
|
||||
- stable
|
||||
release_notes:
|
||||
description: Release notes for the publishing the release. This is required to create a release.
|
||||
dry_run:
|
||||
description: Perform a dry-run release (devel). Note that ref must be an annotated tag when run without dry-run.
|
||||
type: boolean
|
||||
@@ -18,7 +13,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 }}
|
||||
|
||||
@@ -28,118 +28,19 @@ env:
|
||||
# https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/
|
||||
CODER_RELEASE: ${{ !inputs.dry_run }}
|
||||
CODER_DRY_RUN: ${{ inputs.dry_run }}
|
||||
CODER_RELEASE_CHANNEL: ${{ inputs.release_channel }}
|
||||
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.1.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
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
env:
|
||||
# Necessary for Docker manifest
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -161,45 +62,21 @@ jobs:
|
||||
echo "CODER_FORCE_VERSION=$version" >> $GITHUB_ENV
|
||||
echo "$version"
|
||||
|
||||
# Verify that all expectations for a release are met.
|
||||
- name: Verify release input
|
||||
if: ${{ !inputs.dry_run }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${GITHUB_REF}" != "refs/tags/v"* ]]; then
|
||||
echo "Ref must be a semver tag when creating a release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2.10.2 -> release/2.10
|
||||
version="$(./scripts/version.sh)"
|
||||
release_branch=release/${version%.*}
|
||||
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
|
||||
if [[ -z "${branch_contains_tag}" ]]; then
|
||||
echo "Ref tag must exist in a branch named ${release_branch} when creating a release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${CODER_RELEASE_NOTES}" ]]; then
|
||||
echo "Release notes are required to create a release, did you use scripts/release.sh?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Release inputs verified:"
|
||||
echo
|
||||
echo "- Ref: ${GITHUB_REF}"
|
||||
echo "- Version: ${version}"
|
||||
echo "- Release channel: ${CODER_RELEASE_CHANNEL}"
|
||||
echo "- Release branch: ${release_branch}"
|
||||
echo "- Release notes: true"
|
||||
|
||||
- name: Create release notes file
|
||||
- name: Create release notes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# We always have to set this since there might be commits on
|
||||
# main that didn't have a PR.
|
||||
CODER_IGNORE_MISSING_COMMIT_METADATA: "1"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ref=HEAD
|
||||
old_version="$(git describe --abbrev=0 "$ref^1")"
|
||||
version="v$(./scripts/version.sh)"
|
||||
|
||||
# Generate notes.
|
||||
release_notes_file="$(mktemp -t release_notes.XXXXXX)"
|
||||
echo "$CODER_RELEASE_NOTES" > "$release_notes_file"
|
||||
./scripts/release/generate_release_notes.sh --check-for-changelog --old-version "$old_version" --new-version "$version" --ref "$ref" >> "$release_notes_file"
|
||||
echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> $GITHUB_ENV
|
||||
|
||||
- name: Show release notes
|
||||
@@ -208,7 +85,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 }}
|
||||
@@ -220,16 +97,6 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
# Necessary for signing Windows binaries.
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
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 +117,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
|
||||
@@ -269,44 +130,6 @@ jobs:
|
||||
AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
|
||||
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
|
||||
|
||||
- name: Setup Windows EV Signing Certificate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
touch /tmp/ev_cert.pem
|
||||
chmod 600 /tmp/ev_cert.pem
|
||||
echo "$EV_SIGNING_CERT" > /tmp/ev_cert.pem
|
||||
wget https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O /tmp/jsign-6.0.jar
|
||||
env:
|
||||
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
|
||||
|
||||
- name: Test migrations from current ref to main
|
||||
run: |
|
||||
POSTGRES_VERSION=13 make test-migrations
|
||||
|
||||
# Setup GCloud for signing Windows binaries.
|
||||
- name: Authenticate to Google Cloud
|
||||
id: gcloud_auth
|
||||
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10
|
||||
with:
|
||||
workload_identity_provider: ${{ vars.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ vars.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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
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
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -321,29 +144,16 @@ jobs:
|
||||
build/coder_helm_"$version".tgz \
|
||||
build/provisioner_helm_"$version".tgz
|
||||
env:
|
||||
CODER_SIGN_WINDOWS: "1"
|
||||
CODER_SIGN_DARWIN: "1"
|
||||
CODER_SIGN_GPG: "1"
|
||||
CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}
|
||||
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 }}
|
||||
AC_APIKEY_ID: ${{ secrets.AC_APIKEY_ID }}
|
||||
AC_APIKEY_FILE: /tmp/apple_apikey.p8
|
||||
EV_KEY: ${{ secrets.EV_KEY }}
|
||||
EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }}
|
||||
EV_TSA_URL: ${{ secrets.EV_TSA_URL }}
|
||||
EV_CERTIFICATE_PATH: /tmp/ev_cert.pem
|
||||
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
|
||||
JSIGN_PATH: /tmp/jsign-6.0.jar
|
||||
|
||||
- name: Delete Apple Developer certificate and API key
|
||||
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
|
||||
|
||||
- name: Delete Windows EV Signing Cert
|
||||
run: rm /tmp/ev_cert.pem
|
||||
|
||||
- name: Determine base image tag
|
||||
id: image-base-tag
|
||||
run: |
|
||||
@@ -361,19 +171,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@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.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
|
||||
@@ -381,7 +189,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
|
||||
@@ -410,55 +217,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@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc # v2.4.0
|
||||
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 -j 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
|
||||
@@ -466,166 +232,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
|
||||
make -j push/build/coder_"$version"_linux.tag
|
||||
|
||||
# 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: SBOM Generation and Attestation
|
||||
if: ${{ !inputs.dry_run }}
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: "1"
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
# Generate SBOM for multi-arch image with version in filename
|
||||
echo "Generating SBOM for multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}"
|
||||
syft "${{ steps.build_docker.outputs.multiarch_image }}" -o spdx-json > coder_${{ steps.version.outputs.version }}_sbom.spdx.json
|
||||
|
||||
# Attest SBOM to multi-arch image
|
||||
echo "Attesting SBOM to multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}"
|
||||
cosign clean --force=true "${{ steps.build_docker.outputs.multiarch_image }}"
|
||||
cosign attest --type spdxjson \
|
||||
--predicate coder_${{ steps.version.outputs.version }}_sbom.spdx.json \
|
||||
--yes \
|
||||
"${{ steps.build_docker.outputs.multiarch_image }}"
|
||||
|
||||
# If latest tag was created, also attest it
|
||||
if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then
|
||||
latest_tag="$(./scripts/image_tag.sh --version latest)"
|
||||
echo "Generating SBOM for latest image: ${latest_tag}"
|
||||
syft "${latest_tag}" -o spdx-json > coder_latest_sbom.spdx.json
|
||||
|
||||
echo "Attesting SBOM to latest image: ${latest_tag}"
|
||||
cosign clean --force=true "${latest_tag}"
|
||||
cosign attest --type spdxjson \
|
||||
--predicate coder_latest_sbom.spdx.json \
|
||||
--yes \
|
||||
"${latest_tag}"
|
||||
fi
|
||||
|
||||
- name: GitHub Attestation for Docker image
|
||||
id: attest_main
|
||||
if: ${{ !inputs.dry_run }}
|
||||
continue-on-error: true
|
||||
uses: actions/attest@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc # v2.4.0
|
||||
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@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc # v2.4.0
|
||||
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)"
|
||||
@@ -634,76 +256,38 @@ jobs:
|
||||
- name: ls build
|
||||
run: ls -lh build
|
||||
|
||||
- name: Publish Coder CLI binaries and detached signatures to GCS
|
||||
if: ${{ !inputs.dry_run && github.ref == 'refs/heads/main' && github.repository_owner == 'coder'}}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
version="$(./scripts/version.sh)"
|
||||
|
||||
# Source array of slim binaries
|
||||
declare -A binaries
|
||||
binaries["coder-darwin-amd64"]="coder-slim_${version}_darwin_amd64"
|
||||
binaries["coder-darwin-arm64"]="coder-slim_${version}_darwin_arm64"
|
||||
binaries["coder-linux-amd64"]="coder-slim_${version}_linux_amd64"
|
||||
binaries["coder-linux-arm64"]="coder-slim_${version}_linux_arm64"
|
||||
binaries["coder-linux-armv7"]="coder-slim_${version}_linux_armv7"
|
||||
binaries["coder-windows-amd64.exe"]="coder-slim_${version}_windows_amd64.exe"
|
||||
binaries["coder-windows-arm64.exe"]="coder-slim_${version}_windows_arm64.exe"
|
||||
|
||||
for cli_name in "${!binaries[@]}"; do
|
||||
slim_binary="${binaries[$cli_name]}"
|
||||
detached_signature="${slim_binary}.asc"
|
||||
gcloud storage cp "./build/${slim_binary}" "gs://releases.coder.com/coder-cli/${version}/${cli_name}"
|
||||
gcloud storage cp "./build/${detached_signature}" "gs://releases.coder.com/coder-cli/${version}/${cli_name}.asc"
|
||||
done
|
||||
|
||||
- name: Publish release
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
publish_args=()
|
||||
if [[ $CODER_RELEASE_CHANNEL == "stable" ]]; then
|
||||
publish_args+=(--stable)
|
||||
fi
|
||||
if [[ $CODER_DRY_RUN == *t* ]]; then
|
||||
publish_args+=(--dry-run)
|
||||
fi
|
||||
declare -p publish_args
|
||||
|
||||
# Build the list of files to publish
|
||||
files=(
|
||||
./build/*_installer.exe
|
||||
./build/*.zip
|
||||
./build/*.tar.gz
|
||||
./build/*.tgz
|
||||
./build/*.apk
|
||||
./build/*.deb
|
||||
./build/*.rpm
|
||||
./coder_${{ steps.version.outputs.version }}_sbom.spdx.json
|
||||
)
|
||||
|
||||
# Only include the latest SBOM file if it was created
|
||||
if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then
|
||||
files+=(./coder_latest_sbom.spdx.json)
|
||||
fi
|
||||
|
||||
./scripts/release/publish.sh \
|
||||
"${publish_args[@]}" \
|
||||
--release-notes-file "$CODER_RELEASE_NOTES_FILE" \
|
||||
"${files[@]}"
|
||||
./build/*_installer.exe \
|
||||
./build/*.zip \
|
||||
./build/*.tar.gz \
|
||||
./build/*.tgz \
|
||||
./build/*.apk \
|
||||
./build/*.deb \
|
||||
./build/*.rpm
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
|
||||
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 }}
|
||||
@@ -722,7 +306,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: |
|
||||
@@ -733,25 +317,16 @@ jobs:
|
||||
./build/*.apk
|
||||
./build/*.deb
|
||||
./build/*.rpm
|
||||
./coder_${{ steps.version.outputs.version }}_sbom.spdx.json
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload latest sbom artifact to actions (if dry-run)
|
||||
if: inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: latest-sbom-artifact
|
||||
path: ./coder_latest_sbom.spdx.json
|
||||
retention-days: 7
|
||||
|
||||
- name: Send repository-dispatch event
|
||||
- name: Start Packer builds
|
||||
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
|
||||
event-type: coder-release
|
||||
client-payload: '{"coder_version": "${{ steps.version.outputs.version }}", "release_channel": "${{ inputs.release_channel }}"}'
|
||||
client-payload: '{"coder_version": "${{ steps.version.outputs.version }}"}'
|
||||
|
||||
publish-homebrew:
|
||||
name: Publish to Homebrew tap
|
||||
@@ -762,11 +337,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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Update homebrew
|
||||
env:
|
||||
# Variables used by the `gh` command
|
||||
@@ -838,18 +408,13 @@ jobs:
|
||||
if: ${{ !inputs.dry_run }}
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Sync fork
|
||||
run: gh repo sync cdrci/winget-pkgs -b master
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.WINGET_GH_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -928,13 +493,8 @@ jobs:
|
||||
needs: release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
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@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -3,6 +3,7 @@ name: "security"
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -22,33 +23,26 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
permissions:
|
||||
security-events: write
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go, javascript
|
||||
|
||||
- name: Setup Go
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
languages: go, javascript
|
||||
|
||||
# Workaround to prevent CodeQL from building the dashboard.
|
||||
- name: Remove Makefile
|
||||
run: |
|
||||
rm Makefile
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
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' }}
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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,19 @@ 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 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: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@76071ef0d7ec797419534a183b498b4d6366cf37
|
||||
uses: aquasecurity/trivy-action@d43c1f16c00cfd3978dde6c07f4bbcf9eb6993ca
|
||||
with:
|
||||
image-ref: ${{ steps.build.outputs.image }}
|
||||
format: sarif
|
||||
@@ -150,13 +130,13 @@ jobs:
|
||||
severity: "CRITICAL,HIGH"
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
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
|
||||
|
||||
@@ -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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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,35 +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
|
||||
if: >-
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@coder')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '@coder'))
|
||||
environment: dev.coder.com
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Start Coder workspace
|
||||
uses: coder/start-workspace-action@35a4608cefc7e8cc56573cae7c3b85304575cb72
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github-username: >-
|
||||
${{
|
||||
(github.event_name == 'issue_comment' && github.event.comment.user.login) ||
|
||||
(github.event_name == 'issues' && github.event.issue.user.login)
|
||||
}}
|
||||
coder-url: ${{ secrets.CODER_URL }}
|
||||
coder-token: ${{ secrets.CODER_TOKEN }}
|
||||
template-name: ${{ secrets.CODER_TEMPLATE_NAME }}
|
||||
parameters: |-
|
||||
AI 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
|
||||
@@ -1,6 +1,3 @@
|
||||
[default]
|
||||
extend-ignore-identifiers-re = ["gho_.*"]
|
||||
|
||||
[default.extend-identifiers]
|
||||
alog = "alog"
|
||||
Jetbrains = "JetBrains"
|
||||
@@ -17,17 +14,7 @@ darcula = "darcula"
|
||||
Hashi = "Hashi"
|
||||
trialer = "trialer"
|
||||
encrypter = "encrypter"
|
||||
# as in helsinki
|
||||
hel = "hel"
|
||||
# this is used as proto node
|
||||
pn = "pn"
|
||||
# typos doesn't like the EDE in TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
|
||||
EDE = "EDE"
|
||||
# HELO is an SMTP command
|
||||
HELO = "HELO"
|
||||
LKE = "LKE"
|
||||
byt = "byt"
|
||||
typ = "typ"
|
||||
hel = "hel" # as in helsinki
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
@@ -39,13 +26,10 @@ 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/**",
|
||||
"agent/agentcontainers/testdata/devcontainercli/**"
|
||||
]
|
||||
|
||||
@@ -4,42 +4,27 @@ on:
|
||||
schedule:
|
||||
- cron: "0 9 * * 1"
|
||||
workflow_dispatch: # allows to run manually for testing
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
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@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
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@e2ccef58c4b9eb89cd71ee23a8629744bba75aa6 # v1.3.5
|
||||
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: "file"
|
||||
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'
|
||||
if: failure()
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' -d '{"msg":"Broken links found in the documentation. Please check the logs at ${{ env.LOGS_URL }}"}' ${{ secrets.DOCS_LINK_SLACK_WEBHOOK }}
|
||||
echo "Sent Slack notification"
|
||||
|
||||
+1
-19
@@ -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/
|
||||
@@ -50,15 +46,12 @@ site/stats/
|
||||
*.tfplan
|
||||
*.lock.hcl
|
||||
.terraform/
|
||||
!coderd/testdata/parameters/modules/.terraform/
|
||||
!provisioner/terraform/testdata/modules-source-caching/.terraform/
|
||||
|
||||
**/.coderv2/*
|
||||
**/__debug_bin
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
*.test
|
||||
|
||||
# Loadtesting
|
||||
@@ -75,14 +68,3 @@ result
|
||||
|
||||
# Filebrowser.db
|
||||
**/filebrowser.db
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
|
||||
# Zed
|
||||
.zed_server
|
||||
|
||||
# dlv debug binaries for go tests
|
||||
__debug_bin*
|
||||
|
||||
**/.claude/settings.local.json
|
||||
|
||||
+32
-21
@@ -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
|
||||
@@ -164,30 +190,12 @@ linters-settings:
|
||||
- name: unnecessary-stmt
|
||||
- name: unreachable-code
|
||||
- name: unused-parameter
|
||||
exclude: "**/*_test.go"
|
||||
- name: unused-receiver
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
- name: waitgroup-by-value
|
||||
|
||||
# irrelevant as of Go v1.22: https://go.dev/blog/loopvar-preview
|
||||
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
|
||||
@@ -199,15 +207,17 @@ issues:
|
||||
- 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
|
||||
@@ -223,6 +233,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,94 @@
|
||||
# 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
|
||||
# .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
+223
-62
@@ -1,64 +1,225 @@
|
||||
{
|
||||
"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",
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
||||
}
|
||||
"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",
|
||||
"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,
|
||||
"**Generated.ts": 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"
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# Coder Development Guidelines
|
||||
|
||||
Read [cursor rules](.cursorrules).
|
||||
|
||||
## Build/Test/Lint Commands
|
||||
|
||||
### Main Commands
|
||||
|
||||
- `make build` or `make build-fat` - Build all "fat" binaries (includes "server" functionality)
|
||||
- `make build-slim` - Build "slim" binaries
|
||||
- `make test` - Run Go tests
|
||||
- `make test RUN=TestFunctionName` or `go test -v ./path/to/package -run TestFunctionName` - Test single
|
||||
- `make test-postgres` - Run tests with Postgres database
|
||||
- `make test-race` - Run tests with Go race detector
|
||||
- `make test-e2e` - Run end-to-end tests
|
||||
- `make lint` - Run all linters
|
||||
- `make fmt` - Format all code
|
||||
- `make gen` - Generates mocks, database queries and other auto-generated files
|
||||
|
||||
### Frontend Commands (site directory)
|
||||
|
||||
- `pnpm build` - Build frontend
|
||||
- `pnpm dev` - Run development server
|
||||
- `pnpm check` - Run code checks
|
||||
- `pnpm format` - Format frontend code
|
||||
- `pnpm lint` - Lint frontend code
|
||||
- `pnpm test` - Run frontend tests
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Go
|
||||
|
||||
- Follow [Effective Go](https://go.dev/doc/effective_go) and [Go's Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
- Use `gofumpt` for formatting
|
||||
- Create packages when used during implementation
|
||||
- Validate abstractions against implementations
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Use descriptive error messages
|
||||
- Wrap errors with context
|
||||
- Propagate errors appropriately
|
||||
- Use proper error types
|
||||
- (`xerrors.Errorf("failed to X: %w", err)`)
|
||||
|
||||
### Naming
|
||||
|
||||
- Use clear, descriptive names
|
||||
- Abbreviate only when obvious
|
||||
- Follow Go and TypeScript naming conventions
|
||||
|
||||
### Comments
|
||||
|
||||
- Document exported functions, types, and non-obvious logic
|
||||
- Follow JSDoc format for TypeScript
|
||||
- Use godoc format for Go code
|
||||
|
||||
## Commit Style
|
||||
|
||||
- Follow [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||
- Format: `type(scope): message`
|
||||
- Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
||||
- Keep message titles concise (~70 characters)
|
||||
- Use imperative, present tense in commit titles
|
||||
|
||||
## Database queries
|
||||
|
||||
- MUST DO! Any changes to database - adding queries, modifying queries should be done in the `coderd\database\queries\*.sql` files. Use `make gen` to generate necessary changes after.
|
||||
- MUST DO! Queries are grouped in files relating to context - e.g. `prebuilds.sql`, `users.sql`, `provisionerjobs.sql`.
|
||||
- After making changes to any `coderd\database\queries\*.sql` files you must run `make gen` to generate respective ORM changes.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **coderd**: Main API service connecting workspaces, provisioners, and users
|
||||
- **provisionerd**: Execution context for infrastructure-modifying providers
|
||||
- **Agents**: Services in remote workspaces providing features like SSH and port forwarding
|
||||
- **Workspaces**: Cloud resources defined by Terraform
|
||||
|
||||
## Sub-modules
|
||||
|
||||
### Template System
|
||||
|
||||
- Templates define infrastructure for workspaces using Terraform
|
||||
- Environment variables pass context between Coder and templates
|
||||
- Official modules extend development environments
|
||||
|
||||
### RBAC System
|
||||
|
||||
- Permissions defined at site, organization, and user levels
|
||||
- Object-Action model protects resources
|
||||
- Built-in roles: owner, member, auditor, templateAdmin
|
||||
- Permission format: `<sign>?<level>.<object>.<id>.<action>`
|
||||
|
||||
### Database
|
||||
|
||||
- PostgreSQL 13+ recommended for production
|
||||
- Migrations managed with `migrate`
|
||||
- Database authorization through `dbauthz` package
|
||||
|
||||
## Frontend
|
||||
|
||||
The frontend is contained in the site folder.
|
||||
|
||||
For building Frontend refer to [this document](docs/about/contributing/frontend.md)
|
||||
@@ -1,8 +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
|
||||
provisionerd/proto/ @spikecurtis @johnstcn
|
||||
provisionersdk/proto/ @spikecurtis @johnstcn
|
||||
@@ -1,2 +0,0 @@
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
[https://coder.com/docs/about/contributing/CODE_OF_CONDUCT](https://coder.com/docs/about/contributing/CODE_OF_CONDUCT)
|
||||
@@ -1,2 +0,0 @@
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
[https://coder.com/docs/CONTRIBUTING](https://coder.com/docs/CONTRIBUTING)
|
||||
@@ -37,9 +37,6 @@ GOARCH := $(shell go env GOARCH)
|
||||
GOOS_BIN_EXT := $(if $(filter windows, $(GOOS)),.exe,)
|
||||
VERSION := $(shell ./scripts/version.sh)
|
||||
|
||||
POSTGRES_VERSION ?= 17
|
||||
POSTGRES_IMAGE ?= us-docker.pkg.dev/coder-v2-images-public/public/postgres:$(POSTGRES_VERSION)
|
||||
|
||||
# Use the highest ZSTD compression level in CI.
|
||||
ifdef CI
|
||||
ZSTDFLAGS := -22 --ultra
|
||||
@@ -56,22 +53,9 @@ 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')
|
||||
|
||||
# Ensure we don't use the user's git configs which might cause side-effects
|
||||
GIT_FLAGS = GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
|
||||
|
||||
# All ${OS}_${ARCH} combos we build for. Windows binaries have the .exe suffix.
|
||||
OS_ARCHES := \
|
||||
linux_amd64 linux_arm64 linux_armv7 \
|
||||
@@ -91,12 +75,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)
|
||||
@@ -128,7 +108,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
|
||||
|
||||
@@ -220,8 +200,7 @@ endef
|
||||
# calling this manually.
|
||||
$(CODER_ALL_BINARIES): go.mod go.sum \
|
||||
$(GO_SRC_FILES) \
|
||||
$(shell find ./examples/templates) \
|
||||
site/static/error.html
|
||||
$(shell find ./examples/templates)
|
||||
|
||||
$(get-mode-os-arch-ext)
|
||||
if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then
|
||||
@@ -252,32 +231,8 @@ $(CODER_ALL_BINARIES): go.mod go.sum \
|
||||
fi
|
||||
|
||||
cp "$@" "./site/out/bin/coder-$$os-$$arch$$dot_ext"
|
||||
|
||||
if [[ "$${CODER_SIGN_GPG:-0}" == "1" ]]; then
|
||||
cp "$@.asc" "./site/out/bin/coder-$$os-$$arch$$dot_ext.asc"
|
||||
fi
|
||||
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}
|
||||
@@ -404,40 +359,13 @@ $(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/
|
||||
# prevents this directory from getting to big, and causing "too much data" errors
|
||||
rm -rf out/assets/
|
||||
site/out/index.html: site/package.json $(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
|
||||
cd site
|
||||
../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
|
||||
|
||||
@@ -452,52 +380,32 @@ install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
|
||||
cp "$<" "$$output_file"
|
||||
.PHONY: install
|
||||
|
||||
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/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/prettier:
|
||||
echo "--- prettier"
|
||||
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
|
||||
|
||||
fmt/biome: site/node_modules/.installed
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/biome$(RESET)"
|
||||
cd site/
|
||||
# Avoid writing files in CI to reduce file write activity
|
||||
ifdef CI
|
||||
pnpm run format:check
|
||||
else
|
||||
pnpm run format
|
||||
pnpm run format:write
|
||||
endif
|
||||
.PHONY: fmt/biome
|
||||
.PHONY: fmt/prettier
|
||||
|
||||
fmt/terraform: $(wildcard *.tf)
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/terraform$(RESET)"
|
||||
terraform fmt -recursive
|
||||
.PHONY: fmt/terraform
|
||||
|
||||
fmt/shfmt: $(SHELL_SRC_FILES)
|
||||
echo "$(GREEN)==>$(RESET) $(BOLD)fmt/shfmt$(RESET)"
|
||||
echo "--- shfmt"
|
||||
# Only do diff check in CI, errors on diff.
|
||||
ifdef CI
|
||||
shfmt -d $(SHELL_SRC_FILES)
|
||||
@@ -506,34 +414,25 @@ 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/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)
|
||||
go run github.com/golangci/golangci-lint/cmd/golangci-lint@v$$linter_ver run
|
||||
linter_ver=$(shell egrep -o 'GOLANGCI_LINT_VERSION=\S+' dogfood/Dockerfile | cut -d '=' -f 2)
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$$linter_ver
|
||||
golangci-lint run
|
||||
.PHONY: lint/go
|
||||
|
||||
lint/examples:
|
||||
go run ./scripts/examplegen/main.go -lint
|
||||
.PHONY: lint/examples
|
||||
|
||||
# Use shfmt to determine the shell files, takes editorconfig into consideration.
|
||||
lint/shellcheck: $(SHELL_SRC_FILES)
|
||||
echo "--- shellcheck"
|
||||
@@ -541,18 +440,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 \
|
||||
@@ -560,55 +454,33 @@ 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 \
|
||||
docs/admin/prometheus.md \
|
||||
docs/cli.md \
|
||||
docs/admin/audit-logs.md \
|
||||
coderd/apidoc/swagger.json \
|
||||
docs/manifest.json \
|
||||
provisioner/terraform/testdata/version \
|
||||
.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
|
||||
|
||||
# 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:
|
||||
@@ -617,29 +489,26 @@ 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 \
|
||||
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
|
||||
@@ -648,7 +517,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
|
||||
|
||||
@@ -656,42 +525,18 @@ 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 \
|
||||
@@ -725,177 +570,153 @@ 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/ > $@
|
||||
pnpm run format:write:only "$@"
|
||||
|
||||
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 "$@"
|
||||
pnpm run format:write:only "$@"
|
||||
|
||||
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/main.go coderd/rbac/object.go
|
||||
go run scripts/rbacgen/main.go ./coderd/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 "$@"
|
||||
|
||||
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/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 "$@"
|
||||
pnpm run format:write:only ./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 "$@"
|
||||
pnpm run format:write:only ./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 "$@"
|
||||
pnpm run format:write:only ./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 "$@"
|
||||
pnpm run format:write:only ./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
|
||||
echo 'Running "make gen/golden-files"' >&2
|
||||
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)
|
||||
TZ=UTC go test ./cli -run="Test(CommandHelp|ServerYAML|ErrorExamples|.*Golden)" -update
|
||||
go test ./cli -run="Test(CommandHelp|ServerYAML)" -update
|
||||
touch "$@"
|
||||
|
||||
enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard enterprise/cli/*_test.go)
|
||||
TZ=UTC go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
|
||||
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
|
||||
touch "$@"
|
||||
|
||||
tailnet/testdata/.gen-golden: $(wildcard tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard tailnet/*_test.go)
|
||||
TZ=UTC go test ./tailnet -run="TestDebugTemplate" -update
|
||||
go test ./tailnet -run="TestDebugTemplate" -update
|
||||
touch "$@"
|
||||
|
||||
enterprise/tailnet/testdata/.gen-golden: $(wildcard enterprise/tailnet/testdata/*.golden.html) $(GO_SRC_FILES) $(wildcard enterprise/tailnet/*_test.go)
|
||||
TZ=UTC go test ./enterprise/tailnet -run="TestDebugTemplate" -update
|
||||
go test ./enterprise/tailnet -run="TestDebugTemplate" -update
|
||||
touch "$@"
|
||||
|
||||
helm/coder/tests/testdata/.gen-golden: $(wildcard helm/coder/tests/testdata/*.yaml) $(wildcard helm/coder/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/coder/tests/*_test.go)
|
||||
TZ=UTC go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update
|
||||
go test ./helm/coder/tests -run=TestUpdateGoldenFiles -update
|
||||
touch "$@"
|
||||
|
||||
helm/provisioner/tests/testdata/.gen-golden: $(wildcard helm/provisioner/tests/testdata/*.yaml) $(wildcard helm/provisioner/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/provisioner/tests/*_test.go)
|
||||
TZ=UTC go test ./helm/provisioner/tests -run=TestUpdateGoldenFiles -update
|
||||
go test ./helm/provisioner/tests -run=TestUpdateGoldenFiles -update
|
||||
touch "$@"
|
||||
|
||||
coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard coderd/*_test.go)
|
||||
TZ=UTC 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)
|
||||
TZ=UTC go test ./coderd/notifications -run="Test.*Golden$$" -update
|
||||
go test ./coderd -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
|
||||
provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard provisioner/terraform/*_test.go)
|
||||
TZ=UTC go test ./provisioner/terraform -run="Test.*Golden$$" -update
|
||||
go test ./provisioner/terraform -run="Test.*Golden$$" -update
|
||||
touch "$@"
|
||||
|
||||
provisioner/terraform/testdata/version:
|
||||
if [[ "$(shell cat provisioner/terraform/testdata/version.txt)" != "$(shell terraform version -json | jq -r '.terraform_version')" ]]; then
|
||||
./provisioner/terraform/testdata/generate.sh
|
||||
fi
|
||||
.PHONY: provisioner/terraform/testdata/version
|
||||
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 "$@"
|
||||
|
||||
# Set the retry flags if TEST_RETRIES is set
|
||||
ifdef TEST_RETRIES
|
||||
GOTESTSUM_RETRY_FLAGS := --rerun-fails=$(TEST_RETRIES)
|
||||
else
|
||||
GOTESTSUM_RETRY_FLAGS :=
|
||||
endif
|
||||
# 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 $(GOTESTSUM_RETRY_FLAGS) --packages="./..." -- -v -short -count=1 $(if $(RUN),-run $(RUN))
|
||||
gotestsum --format standard-quiet -- -v -short -count=1 ./...
|
||||
.PHONY: test
|
||||
|
||||
test-cli:
|
||||
$(GIT_FLAGS) gotestsum --format standard-quiet $(GOTESTSUM_RETRY_FLAGS) --packages="./cli/..." -- -v -short -count=1
|
||||
.PHONY: test-cli
|
||||
|
||||
# sqlc-cloud-is-setup will fail if no SQLc auth token is set. Use this as a
|
||||
# dependency for any sqlc-cloud related targets.
|
||||
sqlc-cloud-is-setup:
|
||||
@@ -929,54 +750,17 @@ 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 \
|
||||
DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \
|
||||
--junitfile="gotests.xml" \
|
||||
--jsonfile="gotests.json" \
|
||||
$(GOTESTSUM_RETRY_FLAGS) \
|
||||
--packages="./..." -- \
|
||||
-timeout=20m \
|
||||
-failfast \
|
||||
-count=1
|
||||
.PHONY: test-postgres
|
||||
|
||||
test-migrations: test-postgres-docker
|
||||
echo "--- test migrations"
|
||||
set -euo pipefail
|
||||
COMMIT_FROM=$(shell git log -1 --format='%h' HEAD)
|
||||
echo "COMMIT_FROM=$${COMMIT_FROM}"
|
||||
COMMIT_TO=$(shell git log -1 --format='%h' origin/main)
|
||||
echo "COMMIT_TO=$${COMMIT_TO}"
|
||||
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 ${POSTGRES_IMAGE} || {
|
||||
retries=2
|
||||
for try in $(seq 1 ${retries}); do
|
||||
echo "Failed to pull image, retrying (${try}/${retries})..."
|
||||
sleep 1
|
||||
if docker pull ${POSTGRES_IMAGE}; 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 rm -f test-postgres-docker || true
|
||||
docker run \
|
||||
--env POSTGRES_PASSWORD=postgres \
|
||||
--env POSTGRES_USER=postgres \
|
||||
@@ -984,14 +768,13 @@ test-postgres-docker:
|
||||
--env PGDATA=/tmp \
|
||||
--tmpfs /tmp \
|
||||
--publish 5432:5432 \
|
||||
--name test-postgres-docker-${POSTGRES_VERSION} \
|
||||
--name test-postgres-docker \
|
||||
--restart no \
|
||||
--detach \
|
||||
--memory 16GB \
|
||||
${POSTGRES_IMAGE} \
|
||||
-c shared_buffers=2GB \
|
||||
gcr.io/coder-dev-1/postgres:13 \
|
||||
-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 \
|
||||
@@ -1006,42 +789,12 @@ 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 ./...
|
||||
gotestsum --junitfile="gotests.xml" -- -race -count=1 ./...
|
||||
.PHONY: test-race
|
||||
|
||||
test-tailnet-integration:
|
||||
env \
|
||||
CODER_TAILNET_TESTS=true \
|
||||
CODER_MAGICSOCK_DEBUG_LOGGING=true \
|
||||
TS_DEBUG_NETCHECK=true \
|
||||
GOTRACEBACK=single \
|
||||
go test \
|
||||
-exec "sudo -E" \
|
||||
-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
|
||||
# instead. Keeping it here for convenience.
|
||||
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
|
||||
|
||||
@@ -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,28 +11,27 @@
|
||||
</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/v2/latest/enterprise)
|
||||
|
||||
[](https://discord.gg/coder)
|
||||
[](https://codecov.io/gh/coder/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)
|
||||
[](https://goreportcard.com/report/github.com/coder/coder)
|
||||
[](./LICENSE)
|
||||
|
||||
</div>
|
||||
|
||||
[Coder](https://coder.com) enables organizations to set up development environments in their public or private cloud infrastructure. Cloud development environments are defined with Terraform, connected through a secure high-speed Wireguard® tunnel, and automatically shut down when not used to save on costs. Coder gives engineering teams the flexibility to use the cloud for workloads most beneficial to them.
|
||||
[Coder](https://coder.com) enables organizations to set up development environments in their public or private cloud infrastructure. Cloud development environments are defined with Terraform, connected through a secure high-speed Wireguard® tunnel, and are automatically shut down when not in use to save on costs. Coder gives engineering teams the flexibility to use the cloud for workloads that are most beneficial to them.
|
||||
|
||||
- Define cloud development environments in Terraform
|
||||
- EC2 VMs, Kubernetes Pods, Docker Containers, etc.
|
||||
@@ -41,22 +39,22 @@
|
||||
- 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
|
||||
|
||||
# Start the Coder server (caches data in ~/.cache/coder)
|
||||
coder server
|
||||
|
||||
# Navigate to http://localhost:3000 to create your initial user,
|
||||
# create a Docker template and provision a workspace
|
||||
# Navigate to http://localhost:3000 to create your initial user
|
||||
# Create a Docker template, and provision a workspace
|
||||
```
|
||||
|
||||
## Install
|
||||
@@ -66,15 +64,15 @@ 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
|
||||
```
|
||||
|
||||
You can run the install script with `--dry-run` to see the commands that will be used to install without executing them. Run the install script with `--help` for additional flags.
|
||||
You can run the install script with `--dry-run` to see the commands that will be used to install without executing them. You can modify the installation process by including flags. Run the install script with `--help` for reference.
|
||||
|
||||
> See [install](https://coder.com/docs/install) for additional methods.
|
||||
> See [install](https://coder.com/docs/v2/latest/install) for additional methods.
|
||||
|
||||
Once installed, you can start a production deployment with a single command:
|
||||
Once installed, you can start a production deployment<sup>1</sup> with a single command:
|
||||
|
||||
```shell
|
||||
# Automatically sets up an external access URL on *.try.coder.app
|
||||
@@ -84,50 +82,44 @@ coder server
|
||||
coder server --postgres-url <url> --access-url <url>
|
||||
```
|
||||
|
||||
Use `coder --help` to get a list of flags and environment variables. Use our [install guides](https://coder.com/docs/install) for a complete walkthrough.
|
||||
> <sup>1</sup> For production deployments, set up an external PostgreSQL instance for reliability.
|
||||
|
||||
Use `coder --help` to get a list of flags and environment variables. Use our [install guides](https://coder.com/docs/v2/latest/install) for a full walkthrough.
|
||||
|
||||
## Documentation
|
||||
|
||||
Browse our docs [here](https://coder.com/docs) or visit a specific section below:
|
||||
Browse our docs [here](https://coder.com/docs/v2) or visit a specific section below:
|
||||
|
||||
- [**Templates**](https://coder.com/docs/templates): Templates are written in Terraform and describe the infrastructure for workspaces
|
||||
- [**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
|
||||
- [**Templates**](https://coder.com/docs/v2/latest/templates): Templates are written in Terraform and describe the infrastructure for workspaces
|
||||
- [**Workspaces**](https://coder.com/docs/v2/latest/workspaces): Workspaces contain the IDEs, dependencies, and configuration information needed for software development
|
||||
- [**IDEs**](https://coder.com/docs/v2/latest/ides): Connect your existing editor to a workspace
|
||||
- [**Administration**](https://coder.com/docs/v2/latest/admin): Learn how to operate Coder
|
||||
- [**Enterprise**](https://coder.com/docs/v2/latest/enterprise): Learn about our paid features built for large teams
|
||||
|
||||
## Support
|
||||
## Community and Support
|
||||
|
||||
Feel free to [open an issue](https://github.com/coder/coder/issues/new) if you have questions, run into bugs, or have a feature request.
|
||||
|
||||
[Join our Discord](https://discord.gg/coder) to provide feedback on in-progress features and chat with the community using Coder!
|
||||
[Join our Discord](https://discord.gg/coder) or [Slack](https://cdr.co/join-community) to provide feedback on in-progress features, and chat with the community using Coder!
|
||||
|
||||
## Integrations
|
||||
## Contributing
|
||||
|
||||
We are always working on new integrations. Please feel free to open an issue and ask for an integration. Contributions are welcome in any official or community repositories.
|
||||
Contributions are welcome! Read the [contributing docs](https://coder.com/docs/v2/latest/CONTRIBUTING) to get started.
|
||||
|
||||
Find our list of contributors [here](https://github.com/coder/coder/graphs/contributors).
|
||||
|
||||
## Related
|
||||
|
||||
We are always working on new integrations. Feel free to open an issue to request an integration. Contributions are welcome in any official or community repositories.
|
||||
|
||||
### Official
|
||||
|
||||
- [**VS Code Extension**](https://marketplace.visualstudio.com/items?itemName=coder.coder-remote): Open any Coder workspace in VS Code with a single click
|
||||
- [**JetBrains Toolbox Plugin**](https://plugins.jetbrains.com/plugin/26968-coder): Open any Coder workspace from JetBrains Toolbox with a single click
|
||||
- [**JetBrains Gateway Plugin**](https://plugins.jetbrains.com/plugin/19620-coder): Open any Coder workspace in JetBrains Gateway with a single click
|
||||
- [**Dev Container Builder**](https://github.com/coder/envbuilder): Build development environments using `devcontainer.json` on Docker, Kubernetes, and OpenShift
|
||||
- [**Coder Registry**](https://registry.coder.com): Build and 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
|
||||
- [**JetBrains Gateway Extension**](https://plugins.jetbrains.com/plugin/19620-coder): Open any Coder workspace in JetBrains Gateway with a single click
|
||||
- [**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
|
||||
|
||||
- [**Provision Coder with Terraform**](https://github.com/ElliotG/coder-oss-tf): Provision Coder on Google GKE, Azure AKS, AWS EKS, DigitalOcean DOKS, IBMCloud K8s, OVHCloud K8s, and Scaleway K8s Kapsule with Terraform
|
||||
- [**Coder Template GitHub Action**](https://github.com/marketplace/actions/update-coder-template): A GitHub Action that updates Coder templates
|
||||
|
||||
## Contributing
|
||||
|
||||
We are always happy to see new contributors to Coder. If you are new to the Coder codebase, we have
|
||||
[a guide on how to get started](https://coder.com/docs/CONTRIBUTING). We'd love to see your
|
||||
contributions!
|
||||
|
||||
## Hiring
|
||||
|
||||
Apply [here](https://jobs.ashbyhq.com/coder?utm_source=github&utm_medium=readme&utm_campaign=unknown) if you're interested in joining our team.
|
||||
- [**Coder GitHub Action**](https://github.com/marketplace/actions/update-coder-template): A GitHub Action that updates Coder templates
|
||||
- [**Various Templates**](./examples/templates/community-templates.md): Hetzner Cloud, Docker in Docker, and other templates the community has built.
|
||||
|
||||
+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:
|
||||
|
||||
+792
-1406
File diff suppressed because it is too large
Load Diff
+405
-1289
File diff suppressed because it is too large
Load Diff
@@ -1,190 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: .. (interfaces: ContainerCLI,DevcontainerCLI)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination ./acmock.go -package acmock .. ContainerCLI,DevcontainerCLI
|
||||
//
|
||||
|
||||
// Package acmock is a generated GoMock package.
|
||||
package acmock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
agentcontainers "github.com/coder/coder/v2/agent/agentcontainers"
|
||||
codersdk "github.com/coder/coder/v2/codersdk"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockContainerCLI is a mock of ContainerCLI interface.
|
||||
type MockContainerCLI struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockContainerCLIMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockContainerCLIMockRecorder is the mock recorder for MockContainerCLI.
|
||||
type MockContainerCLIMockRecorder struct {
|
||||
mock *MockContainerCLI
|
||||
}
|
||||
|
||||
// NewMockContainerCLI creates a new mock instance.
|
||||
func NewMockContainerCLI(ctrl *gomock.Controller) *MockContainerCLI {
|
||||
mock := &MockContainerCLI{ctrl: ctrl}
|
||||
mock.recorder = &MockContainerCLIMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockContainerCLI) EXPECT() *MockContainerCLIMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Copy mocks base method.
|
||||
func (m *MockContainerCLI) Copy(ctx context.Context, containerName, src, dst string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Copy", ctx, containerName, src, dst)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Copy indicates an expected call of Copy.
|
||||
func (mr *MockContainerCLIMockRecorder) Copy(ctx, containerName, src, dst any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockContainerCLI)(nil).Copy), ctx, containerName, src, dst)
|
||||
}
|
||||
|
||||
// DetectArchitecture mocks base method.
|
||||
func (m *MockContainerCLI) DetectArchitecture(ctx context.Context, containerName string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DetectArchitecture", ctx, containerName)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DetectArchitecture indicates an expected call of DetectArchitecture.
|
||||
func (mr *MockContainerCLIMockRecorder) DetectArchitecture(ctx, containerName any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetectArchitecture", reflect.TypeOf((*MockContainerCLI)(nil).DetectArchitecture), ctx, containerName)
|
||||
}
|
||||
|
||||
// ExecAs mocks base method.
|
||||
func (m *MockContainerCLI) ExecAs(ctx context.Context, containerName, user string, args ...string) ([]byte, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, containerName, user}
|
||||
for _, a := range args {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "ExecAs", varargs...)
|
||||
ret0, _ := ret[0].([]byte)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ExecAs indicates an expected call of ExecAs.
|
||||
func (mr *MockContainerCLIMockRecorder) ExecAs(ctx, containerName, user any, args ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, containerName, user}, args...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecAs", reflect.TypeOf((*MockContainerCLI)(nil).ExecAs), varargs...)
|
||||
}
|
||||
|
||||
// List mocks base method.
|
||||
func (m *MockContainerCLI) 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 *MockContainerCLIMockRecorder) List(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockContainerCLI)(nil).List), ctx)
|
||||
}
|
||||
|
||||
// MockDevcontainerCLI is a mock of DevcontainerCLI interface.
|
||||
type MockDevcontainerCLI struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockDevcontainerCLIMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockDevcontainerCLIMockRecorder is the mock recorder for MockDevcontainerCLI.
|
||||
type MockDevcontainerCLIMockRecorder struct {
|
||||
mock *MockDevcontainerCLI
|
||||
}
|
||||
|
||||
// NewMockDevcontainerCLI creates a new mock instance.
|
||||
func NewMockDevcontainerCLI(ctrl *gomock.Controller) *MockDevcontainerCLI {
|
||||
mock := &MockDevcontainerCLI{ctrl: ctrl}
|
||||
mock.recorder = &MockDevcontainerCLIMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockDevcontainerCLI) EXPECT() *MockDevcontainerCLIMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Exec mocks base method.
|
||||
func (m *MockDevcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath, cmd string, cmdArgs []string, opts ...agentcontainers.DevcontainerCLIExecOptions) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, workspaceFolder, configPath, cmd, cmdArgs}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Exec", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Exec indicates an expected call of Exec.
|
||||
func (mr *MockDevcontainerCLIMockRecorder) Exec(ctx, workspaceFolder, configPath, cmd, cmdArgs any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, workspaceFolder, configPath, cmd, cmdArgs}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockDevcontainerCLI)(nil).Exec), varargs...)
|
||||
}
|
||||
|
||||
// ReadConfig mocks base method.
|
||||
func (m *MockDevcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, env []string, opts ...agentcontainers.DevcontainerCLIReadConfigOptions) (agentcontainers.DevcontainerConfig, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, workspaceFolder, configPath, env}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "ReadConfig", varargs...)
|
||||
ret0, _ := ret[0].(agentcontainers.DevcontainerConfig)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadConfig indicates an expected call of ReadConfig.
|
||||
func (mr *MockDevcontainerCLIMockRecorder) ReadConfig(ctx, workspaceFolder, configPath, env any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, workspaceFolder, configPath, env}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockDevcontainerCLI)(nil).ReadConfig), varargs...)
|
||||
}
|
||||
|
||||
// Up mocks base method.
|
||||
func (m *MockDevcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath string, opts ...agentcontainers.DevcontainerCLIUpOptions) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []any{ctx, workspaceFolder, configPath}
|
||||
for _, a := range opts {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Up", varargs...)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Up indicates an expected call of Up.
|
||||
func (mr *MockDevcontainerCLIMockRecorder) Up(ctx, workspaceFolder, configPath any, opts ...any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]any{ctx, workspaceFolder, configPath}, opts...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Up", reflect.TypeOf((*MockDevcontainerCLI)(nil).Up), varargs...)
|
||||
}
|
||||
@@ -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 .. ContainerCLI,DevcontainerCLI
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,358 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/coder/coder/v2/provisioner"
|
||||
)
|
||||
|
||||
func TestSafeAgentName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
folderName string
|
||||
expected string
|
||||
fallback bool
|
||||
}{
|
||||
// Basic valid names
|
||||
{
|
||||
folderName: "simple",
|
||||
expected: "simple",
|
||||
},
|
||||
{
|
||||
folderName: "with-hyphens",
|
||||
expected: "with-hyphens",
|
||||
},
|
||||
{
|
||||
folderName: "123numbers",
|
||||
expected: "123numbers",
|
||||
},
|
||||
{
|
||||
folderName: "mixed123",
|
||||
expected: "mixed123",
|
||||
},
|
||||
|
||||
// Names that need transformation
|
||||
{
|
||||
folderName: "With_Underscores",
|
||||
expected: "with-underscores",
|
||||
},
|
||||
{
|
||||
folderName: "With Spaces",
|
||||
expected: "with-spaces",
|
||||
},
|
||||
{
|
||||
folderName: "UPPERCASE",
|
||||
expected: "uppercase",
|
||||
},
|
||||
{
|
||||
folderName: "Mixed_Case-Name",
|
||||
expected: "mixed-case-name",
|
||||
},
|
||||
|
||||
// Names with special characters that get replaced
|
||||
{
|
||||
folderName: "special@#$chars",
|
||||
expected: "special-chars",
|
||||
},
|
||||
{
|
||||
folderName: "dots.and.more",
|
||||
expected: "dots-and-more",
|
||||
},
|
||||
{
|
||||
folderName: "multiple___underscores",
|
||||
expected: "multiple-underscores",
|
||||
},
|
||||
{
|
||||
folderName: "multiple---hyphens",
|
||||
expected: "multiple-hyphens",
|
||||
},
|
||||
|
||||
// Edge cases with leading/trailing special chars
|
||||
{
|
||||
folderName: "-leading-hyphen",
|
||||
expected: "leading-hyphen",
|
||||
},
|
||||
{
|
||||
folderName: "trailing-hyphen-",
|
||||
expected: "trailing-hyphen",
|
||||
},
|
||||
{
|
||||
folderName: "_leading_underscore",
|
||||
expected: "leading-underscore",
|
||||
},
|
||||
{
|
||||
folderName: "trailing_underscore_",
|
||||
expected: "trailing-underscore",
|
||||
},
|
||||
{
|
||||
folderName: "---multiple-leading",
|
||||
expected: "multiple-leading",
|
||||
},
|
||||
{
|
||||
folderName: "trailing-multiple---",
|
||||
expected: "trailing-multiple",
|
||||
},
|
||||
|
||||
// Complex transformation cases
|
||||
{
|
||||
folderName: "___very---complex@@@name___",
|
||||
expected: "very-complex-name",
|
||||
},
|
||||
{
|
||||
folderName: "my.project-folder_v2",
|
||||
expected: "my-project-folder-v2",
|
||||
},
|
||||
|
||||
// Empty and fallback cases - now correctly uses friendlyName fallback
|
||||
{
|
||||
folderName: "",
|
||||
expected: "friendly-fallback",
|
||||
fallback: true,
|
||||
},
|
||||
{
|
||||
folderName: "---",
|
||||
expected: "friendly-fallback",
|
||||
fallback: true,
|
||||
},
|
||||
{
|
||||
folderName: "___",
|
||||
expected: "friendly-fallback",
|
||||
fallback: true,
|
||||
},
|
||||
{
|
||||
folderName: "@#$",
|
||||
expected: "friendly-fallback",
|
||||
fallback: true,
|
||||
},
|
||||
|
||||
// Additional edge cases
|
||||
{
|
||||
folderName: "a",
|
||||
expected: "a",
|
||||
},
|
||||
{
|
||||
folderName: "1",
|
||||
expected: "1",
|
||||
},
|
||||
{
|
||||
folderName: "a1b2c3",
|
||||
expected: "a1b2c3",
|
||||
},
|
||||
{
|
||||
folderName: "CamelCase",
|
||||
expected: "camelcase",
|
||||
},
|
||||
{
|
||||
folderName: "snake_case_name",
|
||||
expected: "snake-case-name",
|
||||
},
|
||||
{
|
||||
folderName: "kebab-case-name",
|
||||
expected: "kebab-case-name",
|
||||
},
|
||||
{
|
||||
folderName: "mix3d_C4s3-N4m3",
|
||||
expected: "mix3d-c4s3-n4m3",
|
||||
},
|
||||
{
|
||||
folderName: "123-456-789",
|
||||
expected: "123-456-789",
|
||||
},
|
||||
{
|
||||
folderName: "abc123def456",
|
||||
expected: "abc123def456",
|
||||
},
|
||||
{
|
||||
folderName: " spaces everywhere ",
|
||||
expected: "spaces-everywhere",
|
||||
},
|
||||
{
|
||||
folderName: "unicode-café-naïve",
|
||||
expected: "unicode-caf-na-ve",
|
||||
},
|
||||
{
|
||||
folderName: "path/with/slashes",
|
||||
expected: "path-with-slashes",
|
||||
},
|
||||
{
|
||||
folderName: "file.tar.gz",
|
||||
expected: "file-tar-gz",
|
||||
},
|
||||
{
|
||||
folderName: "version-1.2.3-alpha",
|
||||
expected: "version-1-2-3-alpha",
|
||||
},
|
||||
|
||||
// Truncation test for names exceeding 64 characters
|
||||
{
|
||||
folderName: "this-is-a-very-long-folder-name-that-exceeds-sixty-four-characters-limit-and-should-be-truncated",
|
||||
expected: "this-is-a-very-long-folder-name-that-exceeds-sixty-four-characte",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.folderName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
name, usingWorkspaceFolder := safeAgentName(tt.folderName, "friendly-fallback")
|
||||
|
||||
assert.Equal(t, tt.expected, name)
|
||||
assert.True(t, provisioner.AgentNameRegex.Match([]byte(name)))
|
||||
assert.Equal(t, tt.fallback, !usingWorkspaceFolder)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandedAgentName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workspaceFolder string
|
||||
friendlyName string
|
||||
depth int
|
||||
expected string
|
||||
fallback bool
|
||||
}{
|
||||
{
|
||||
name: "simple path depth 1",
|
||||
workspaceFolder: "/home/coder/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 0,
|
||||
expected: "project",
|
||||
},
|
||||
{
|
||||
name: "simple path depth 2",
|
||||
workspaceFolder: "/home/coder/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "coder-project",
|
||||
},
|
||||
{
|
||||
name: "simple path depth 3",
|
||||
workspaceFolder: "/home/coder/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 2,
|
||||
expected: "home-coder-project",
|
||||
},
|
||||
{
|
||||
name: "simple path depth exceeds available",
|
||||
workspaceFolder: "/home/coder/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 9,
|
||||
expected: "home-coder-project",
|
||||
},
|
||||
// Cases with special characters that need sanitization
|
||||
{
|
||||
name: "path with spaces and special chars",
|
||||
workspaceFolder: "/home/coder/My Project_v2",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "coder-my-project-v2",
|
||||
},
|
||||
{
|
||||
name: "path with dots and underscores",
|
||||
workspaceFolder: "/home/user.name/project_folder.git",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "user-name-project-folder-git",
|
||||
},
|
||||
// Edge cases
|
||||
{
|
||||
name: "empty path",
|
||||
workspaceFolder: "",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 0,
|
||||
expected: "friendly-fallback",
|
||||
fallback: true,
|
||||
},
|
||||
{
|
||||
name: "root path",
|
||||
workspaceFolder: "/",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 0,
|
||||
expected: "friendly-fallback",
|
||||
fallback: true,
|
||||
},
|
||||
{
|
||||
name: "single component",
|
||||
workspaceFolder: "project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 0,
|
||||
expected: "project",
|
||||
},
|
||||
{
|
||||
name: "single component with depth 2",
|
||||
workspaceFolder: "project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "project",
|
||||
},
|
||||
// Collision simulation cases
|
||||
{
|
||||
name: "foo/project depth 1",
|
||||
workspaceFolder: "/home/coder/foo/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 0,
|
||||
expected: "project",
|
||||
},
|
||||
{
|
||||
name: "foo/project depth 2",
|
||||
workspaceFolder: "/home/coder/foo/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "foo-project",
|
||||
},
|
||||
{
|
||||
name: "bar/project depth 1",
|
||||
workspaceFolder: "/home/coder/bar/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 0,
|
||||
expected: "project",
|
||||
},
|
||||
{
|
||||
name: "bar/project depth 2",
|
||||
workspaceFolder: "/home/coder/bar/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "bar-project",
|
||||
},
|
||||
// Path with trailing slashes
|
||||
{
|
||||
name: "path with trailing slash",
|
||||
workspaceFolder: "/home/coder/project/",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "coder-project",
|
||||
},
|
||||
{
|
||||
name: "path with multiple trailing slashes",
|
||||
workspaceFolder: "/home/coder/project///",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "coder-project",
|
||||
},
|
||||
// Path with leading slashes
|
||||
{
|
||||
name: "path with multiple leading slashes",
|
||||
workspaceFolder: "///home/coder/project",
|
||||
friendlyName: "friendly-fallback",
|
||||
depth: 1,
|
||||
expected: "coder-project",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
name, usingWorkspaceFolder := expandedAgentName(tt.workspaceFolder, tt.friendlyName, tt.depth)
|
||||
|
||||
assert.Equal(t, tt.expected, name)
|
||||
assert.True(t, provisioner.AgentNameRegex.Match([]byte(name)))
|
||||
assert.Equal(t, tt.fallback, !usingWorkspaceFolder)
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// ContainerCLI is an interface for interacting with containers in a workspace.
|
||||
type ContainerCLI 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)
|
||||
// DetectArchitecture detects the architecture of a container.
|
||||
DetectArchitecture(ctx context.Context, containerName string) (string, error)
|
||||
// Copy copies a file from the host to a container.
|
||||
Copy(ctx context.Context, containerName, src, dst string) error
|
||||
// ExecAs executes a command in a container as a specific user.
|
||||
ExecAs(ctx context.Context, containerName, user string, args ...string) ([]byte, error)
|
||||
}
|
||||
|
||||
// noopContainerCLI is a ContainerCLI that does nothing.
|
||||
type noopContainerCLI struct{}
|
||||
|
||||
var _ ContainerCLI = noopContainerCLI{}
|
||||
|
||||
func (noopContainerCLI) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
return codersdk.WorkspaceAgentListContainersResponse{}, nil
|
||||
}
|
||||
|
||||
func (noopContainerCLI) DetectArchitecture(_ context.Context, _ string) (string, error) {
|
||||
return "<none>", nil
|
||||
}
|
||||
func (noopContainerCLI) Copy(_ context.Context, _ string, _ string, _ string) error { return nil }
|
||||
func (noopContainerCLI) ExecAs(_ context.Context, _ string, _ string, _ ...string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,597 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/user"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// dockerCLI is an implementation for Docker CLI that lists containers.
|
||||
type dockerCLI struct {
|
||||
execer agentexec.Execer
|
||||
}
|
||||
|
||||
var _ ContainerCLI = (*dockerCLI)(nil)
|
||||
|
||||
func NewDockerCLI(execer agentexec.Execer) ContainerCLI {
|
||||
return &dockerCLI{
|
||||
execer: execer,
|
||||
}
|
||||
}
|
||||
|
||||
func (dcli *dockerCLI) List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) {
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
// List all container IDs, one per line, with no truncation
|
||||
cmd := dcli.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, dcli.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) {
|
||||
if ctx.Err() != nil {
|
||||
// If the context is done, we don't want to run the command.
|
||||
return []byte{}, []byte{}, ctx.Err()
|
||||
}
|
||||
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 {
|
||||
if ctx.Err() != nil {
|
||||
// If the context was canceled while running the command,
|
||||
// return the context error instead of the command error,
|
||||
// which is likely to be "signal: killed".
|
||||
return stdout, stderr, ctx.Err()
|
||||
}
|
||||
if bytes.Contains(stderr, []byte("No such object:")) {
|
||||
// This can happen if a container is deleted between the time we check for its existence and the time we inspect it.
|
||||
return stdout, stderr, 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, "/")
|
||||
p, err := strconv.ParseUint(parts[0], 10, 16)
|
||||
if err != nil {
|
||||
return 0, "", xerrors.Errorf("invalid port format: %s", in)
|
||||
}
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// assume it's a TCP port
|
||||
return uint16(p), "tcp", nil
|
||||
case 2:
|
||||
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()
|
||||
}
|
||||
|
||||
// DetectArchitecture detects the architecture of a container by inspecting its
|
||||
// image.
|
||||
func (dcli *dockerCLI) DetectArchitecture(ctx context.Context, containerName string) (string, error) {
|
||||
// Inspect the container to get the image name, which contains the architecture.
|
||||
stdout, stderr, err := runCmd(ctx, dcli.execer, "docker", "inspect", "--format", "{{.Config.Image}}", containerName)
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("inspect container %s: %w: %s", containerName, err, stderr)
|
||||
}
|
||||
imageName := string(stdout)
|
||||
if imageName == "" {
|
||||
return "", xerrors.Errorf("no image found for container %s", containerName)
|
||||
}
|
||||
|
||||
stdout, stderr, err = runCmd(ctx, dcli.execer, "docker", "inspect", "--format", "{{.Architecture}}", imageName)
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("inspect image %s: %w: %s", imageName, err, stderr)
|
||||
}
|
||||
arch := string(stdout)
|
||||
if arch == "" {
|
||||
return "", xerrors.Errorf("no architecture found for image %s", imageName)
|
||||
}
|
||||
return arch, nil
|
||||
}
|
||||
|
||||
// Copy copies a file from the host to a container.
|
||||
func (dcli *dockerCLI) Copy(ctx context.Context, containerName, src, dst string) error {
|
||||
_, stderr, err := runCmd(ctx, dcli.execer, "docker", "cp", src, containerName+":"+dst)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("copy %s to %s:%s: %w: %s", src, containerName, dst, err, stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecAs executes a command in a container as a specific user.
|
||||
func (dcli *dockerCLI) ExecAs(ctx context.Context, containerName, uid string, args ...string) ([]byte, error) {
|
||||
execArgs := []string{"exec"}
|
||||
if uid != "" {
|
||||
altUID := uid
|
||||
if uid == "root" {
|
||||
// UID 0 is more portable than the name root, so we use that
|
||||
// because some containers may not have a user named "root".
|
||||
altUID = "0"
|
||||
}
|
||||
execArgs = append(execArgs, "--user", altUID)
|
||||
}
|
||||
execArgs = append(execArgs, containerName)
|
||||
execArgs = append(execArgs, args...)
|
||||
|
||||
stdout, stderr, err := runCmd(ctx, dcli.execer, "docker", execArgs...)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("exec in container %s as user %s: %w: %s", containerName, uid, err, stderr)
|
||||
}
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
// runCmd is a helper function that runs a command with the given
|
||||
// arguments and returns the stdout and stderr output.
|
||||
func runCmd(ctx context.Context, execer agentexec.Execer, cmd string, args ...string) (stdout, stderr []byte, err error) {
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
c := execer.CommandContext(ctx, cmd, args...)
|
||||
c.Stdout = &stdoutBuf
|
||||
c.Stderr = &stderrBuf
|
||||
err = c.Run()
|
||||
stdout = bytes.TrimSpace(stdoutBuf.Bytes())
|
||||
stderr = bytes.TrimSpace(stderrBuf.Bytes())
|
||||
return stdout, stderr, err
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package agentcontainers_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// TestIntegrationDockerCLI tests the DetectArchitecture, Copy, and
|
||||
// ExecAs methods using a real Docker container. All tests share a
|
||||
// single container to avoid setup overhead.
|
||||
//
|
||||
// Run manually with: CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestIntegrationDockerCLI
|
||||
//
|
||||
//nolint:tparallel,paralleltest // Docker integration tests don't run in parallel to avoid flakiness.
|
||||
func TestIntegrationDockerCLI(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")
|
||||
|
||||
// Start a simple busybox container for all subtests to share.
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "busybox",
|
||||
Tag: "latest",
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
}, 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")
|
||||
|
||||
dcli := agentcontainers.NewDockerCLI(agentexec.DefaultExecer)
|
||||
ctx := testutil.Context(t, testutil.WaitMedium) // Longer timeout for multiple subtests
|
||||
containerName := strings.TrimPrefix(ct.Container.Name, "/")
|
||||
|
||||
t.Run("DetectArchitecture", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
arch, err := dcli.DetectArchitecture(ctx, containerName)
|
||||
require.NoError(t, err, "DetectArchitecture failed")
|
||||
require.NotEmpty(t, arch, "arch has no content")
|
||||
require.Equal(t, runtime.GOARCH, arch, "architecture does not match runtime, did you run this test with a remote Docker socket?")
|
||||
|
||||
t.Logf("Detected architecture: %s", arch)
|
||||
})
|
||||
|
||||
t.Run("Copy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := "Help, I'm trapped!"
|
||||
tempFile := filepath.Join(t.TempDir(), "test-file.txt")
|
||||
err := os.WriteFile(tempFile, []byte(want), 0o600)
|
||||
require.NoError(t, err, "create test file failed")
|
||||
|
||||
destPath := "/tmp/copied-file.txt"
|
||||
err = dcli.Copy(ctx, containerName, tempFile, destPath)
|
||||
require.NoError(t, err, "Copy failed")
|
||||
|
||||
got, err := dcli.ExecAs(ctx, containerName, "", "cat", destPath)
|
||||
require.NoError(t, err, "ExecAs failed after Copy")
|
||||
require.Equal(t, want, string(got), "copied file content did not match original")
|
||||
|
||||
t.Logf("Successfully copied file from %s to container %s:%s", tempFile, containerName, destPath)
|
||||
})
|
||||
|
||||
t.Run("ExecAs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test ExecAs without specifying user (should use container's default).
|
||||
want := "root"
|
||||
got, err := dcli.ExecAs(ctx, containerName, "", "whoami")
|
||||
require.NoError(t, err, "ExecAs without user should succeed")
|
||||
require.Equal(t, want, string(got), "ExecAs without user should output expected string")
|
||||
|
||||
// Test ExecAs with numeric UID (non root).
|
||||
want = "1000"
|
||||
_, err = dcli.ExecAs(ctx, containerName, want, "whoami")
|
||||
require.Error(t, err, "ExecAs with UID 1000 should fail as user does not exist in busybox")
|
||||
require.Contains(t, err.Error(), "whoami: unknown uid 1000", "ExecAs with UID 1000 should return 'unknown uid' error")
|
||||
|
||||
// Test ExecAs with root user (should convert "root" to "0", which still outputs root due to passwd).
|
||||
want = "root"
|
||||
got, err = dcli.ExecAs(ctx, containerName, "root", "whoami")
|
||||
require.NoError(t, err, "ExecAs with root user should succeed")
|
||||
require.Equal(t, want, string(got), "ExecAs with root user should output expected string")
|
||||
|
||||
// Test ExecAs with numeric UID.
|
||||
want = "root"
|
||||
got, err = dcli.ExecAs(ctx, containerName, "0", "whoami")
|
||||
require.NoError(t, err, "ExecAs with UID 0 should succeed")
|
||||
require.Equal(t, want, string(got), "ExecAs with UID 0 should output expected string")
|
||||
|
||||
// Test ExecAs with multiple arguments.
|
||||
want = "multiple args test"
|
||||
got, err = dcli.ExecAs(ctx, containerName, "", "sh", "-c", "echo '"+want+"'")
|
||||
require.NoError(t, err, "ExecAs with multiple arguments should succeed")
|
||||
require.Equal(t, want, string(got), "ExecAs with multiple arguments should output expected string")
|
||||
|
||||
t.Logf("Successfully executed commands in container %s", containerName)
|
||||
})
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
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 {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDockerPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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",
|
||||
},
|
||||
} {
|
||||
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",
|
||||
},
|
||||
} {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
package agentcontainers_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
// 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 := agentcontainers.NewDockerCLI(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 := agentcontainers.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)
|
||||
}
|
||||
|
||||
// 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 := agentcontainers.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 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
|
||||
}
|
||||
@@ -1,601 +0,0 @@
|
||||
// Code generated by dcspec/gen.sh. DO NOT EDIT.
|
||||
//
|
||||
// This file was generated from JSON Schema using quicktype, do not modify it directly.
|
||||
// To parse and unparse this JSON data, add this code to your project and do:
|
||||
//
|
||||
// devContainer, err := UnmarshalDevContainer(bytes)
|
||||
// bytes, err = devContainer.Marshal()
|
||||
|
||||
package dcspec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func UnmarshalDevContainer(data []byte) (DevContainer, error) {
|
||||
var r DevContainer
|
||||
err := json.Unmarshal(data, &r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
func (r *DevContainer) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (x *DevContainerAppPort) UnmarshalJSON(data []byte) error {
|
||||
x.UnionArray = nil
|
||||
object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, true, &x.UnionArray, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *DevContainerAppPort) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(x.Integer, nil, nil, x.String, x.UnionArray != nil, x.UnionArray, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (x *AppPortElement) UnmarshalJSON(data []byte) error {
|
||||
object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AppPortElement) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(x.Integer, nil, nil, x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (x *CacheFrom) UnmarshalJSON(data []byte) error {
|
||||
x.StringArray = nil
|
||||
object, err := unmarshalUnion(data, nil, nil, nil, &x.String, true, &x.StringArray, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CacheFrom) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, nil, x.String, x.StringArray != nil, x.StringArray, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
type ForwardPort struct {
|
||||
Integer *int64
|
||||
String *string
|
||||
}
|
||||
|
||||
func (x *ForwardPort) UnmarshalJSON(data []byte) error {
|
||||
object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ForwardPort) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(x.Integer, nil, nil, x.String, false, nil, false, nil, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
type GPUUnion struct {
|
||||
Bool *bool
|
||||
Enum *GPUEnum
|
||||
GPUClass *GPUClass
|
||||
}
|
||||
|
||||
func (x *GPUUnion) UnmarshalJSON(data []byte) error {
|
||||
x.GPUClass = nil
|
||||
x.Enum = nil
|
||||
var c GPUClass
|
||||
object, err := unmarshalUnion(data, nil, nil, &x.Bool, nil, false, nil, true, &c, false, nil, true, &x.Enum, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
x.GPUClass = &c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GPUUnion) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, x.Bool, nil, false, nil, x.GPUClass != nil, x.GPUClass, false, nil, x.Enum != nil, x.Enum, false)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (x *Command) UnmarshalJSON(data []byte) error {
|
||||
x.StringArray = nil
|
||||
x.UnionMap = nil
|
||||
object, err := unmarshalUnion(data, nil, nil, nil, &x.String, true, &x.StringArray, false, nil, true, &x.UnionMap, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Command) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, nil, x.String, x.StringArray != nil, x.StringArray, false, nil, x.UnionMap != nil, x.UnionMap, false, nil, false)
|
||||
}
|
||||
|
||||
type MountElement struct {
|
||||
Mount *Mount
|
||||
String *string
|
||||
}
|
||||
|
||||
func (x *MountElement) UnmarshalJSON(data []byte) error {
|
||||
x.Mount = nil
|
||||
var c Mount
|
||||
object, err := unmarshalUnion(data, nil, nil, nil, &x.String, false, nil, true, &c, false, nil, false, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object {
|
||||
x.Mount = &c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MountElement) MarshalJSON() ([]byte, error) {
|
||||
return marshalUnion(nil, nil, nil, x.String, false, nil, x.Mount != nil, x.Mount, false, nil, false, nil, false)
|
||||
}
|
||||
|
||||
func unmarshalUnion(data []byte, pi **int64, pf **float64, pb **bool, ps **string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) (bool, error) {
|
||||
if pi != nil {
|
||||
*pi = nil
|
||||
}
|
||||
if pf != nil {
|
||||
*pf = nil
|
||||
}
|
||||
if pb != nil {
|
||||
*pb = nil
|
||||
}
|
||||
if ps != nil {
|
||||
*ps = nil
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.UseNumber()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch v := tok.(type) {
|
||||
case json.Number:
|
||||
if pi != nil {
|
||||
i, err := v.Int64()
|
||||
if err == nil {
|
||||
*pi = &i
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if pf != nil {
|
||||
f, err := v.Float64()
|
||||
if err == nil {
|
||||
*pf = &f
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Unparsable number")
|
||||
}
|
||||
return false, errors.New("Union does not contain number")
|
||||
case float64:
|
||||
return false, errors.New("Decoder should not return float64")
|
||||
case bool:
|
||||
if pb != nil {
|
||||
*pb = &v
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Union does not contain bool")
|
||||
case string:
|
||||
if haveEnum {
|
||||
return false, json.Unmarshal(data, pe)
|
||||
}
|
||||
if ps != nil {
|
||||
*ps = &v
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Union does not contain string")
|
||||
case nil:
|
||||
if nullable {
|
||||
return false, nil
|
||||
}
|
||||
return false, errors.New("Union does not contain null")
|
||||
case json.Delim:
|
||||
if v == '{' {
|
||||
if haveObject {
|
||||
return true, json.Unmarshal(data, pc)
|
||||
}
|
||||
if haveMap {
|
||||
return false, json.Unmarshal(data, pm)
|
||||
}
|
||||
return false, errors.New("Union does not contain object")
|
||||
}
|
||||
if v == '[' {
|
||||
if haveArray {
|
||||
return false, json.Unmarshal(data, pa)
|
||||
}
|
||||
return false, errors.New("Union does not contain array")
|
||||
}
|
||||
return false, errors.New("Cannot handle delimiter")
|
||||
}
|
||||
return false, errors.New("Cannot unmarshal union")
|
||||
}
|
||||
|
||||
func marshalUnion(pi *int64, pf *float64, pb *bool, ps *string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) ([]byte, error) {
|
||||
if pi != nil {
|
||||
return json.Marshal(*pi)
|
||||
}
|
||||
if pf != nil {
|
||||
return json.Marshal(*pf)
|
||||
}
|
||||
if pb != nil {
|
||||
return json.Marshal(*pb)
|
||||
}
|
||||
if ps != nil {
|
||||
return json.Marshal(*ps)
|
||||
}
|
||||
if haveArray {
|
||||
return json.Marshal(pa)
|
||||
}
|
||||
if haveObject {
|
||||
return json.Marshal(pc)
|
||||
}
|
||||
if haveMap {
|
||||
return json.Marshal(pm)
|
||||
}
|
||||
if haveEnum {
|
||||
return json.Marshal(pe)
|
||||
}
|
||||
if nullable {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
return nil, errors.New("Union must not be null")
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package dcspec_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers/dcspec"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
)
|
||||
|
||||
func TestUnmarshalDevContainer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
file string
|
||||
wantErr bool
|
||||
want dcspec.DevContainer
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
name: "minimal",
|
||||
file: filepath.Join("testdata", "minimal.json"),
|
||||
want: dcspec.DevContainer{
|
||||
Image: ptr.Ref("test-image"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arrays",
|
||||
file: filepath.Join("testdata", "arrays.json"),
|
||||
want: dcspec.DevContainer{
|
||||
Image: ptr.Ref("test-image"),
|
||||
RunArgs: []string{"--network=host", "--privileged"},
|
||||
ForwardPorts: []dcspec.ForwardPort{
|
||||
{
|
||||
Integer: ptr.Ref[int64](8080),
|
||||
},
|
||||
{
|
||||
String: ptr.Ref("3000:3000"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "devcontainers/template-starter",
|
||||
file: filepath.Join("testdata", "devcontainers-template-starter.json"),
|
||||
wantErr: false,
|
||||
want: dcspec.DevContainer{
|
||||
Image: ptr.Ref("mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye"),
|
||||
Features: &dcspec.Features{},
|
||||
Customizations: map[string]interface{}{
|
||||
"vscode": map[string]interface{}{
|
||||
"extensions": []interface{}{
|
||||
"mads-hartmann.bash-ide-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
},
|
||||
},
|
||||
},
|
||||
PostCreateCommand: &dcspec.Command{
|
||||
String: ptr.Ref("npm install -g @devcontainers/cli"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var missingTests []string
|
||||
files, err := filepath.Glob("testdata/*.json")
|
||||
require.NoError(t, err, "glob test files failed")
|
||||
for _, file := range files {
|
||||
if !slices.ContainsFunc(tests, func(tt testCase) bool {
|
||||
return tt.file == file
|
||||
}) {
|
||||
missingTests = append(missingTests, file)
|
||||
}
|
||||
}
|
||||
require.Empty(t, missingTests, "missing tests case for files: %v", missingTests)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data, err := os.ReadFile(tt.file)
|
||||
require.NoError(t, err, "read test file failed")
|
||||
|
||||
got, err := dcspec.UnmarshalDevContainer(data)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err, "want error but got nil")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err, "unmarshal DevContainer failed")
|
||||
|
||||
// Compare the unmarshaled data with the expected data.
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
require.Empty(t, diff, "UnmarshalDevContainer() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// Test that marshaling works (without comparing to original).
|
||||
marshaled, err := got.Marshal()
|
||||
require.NoError(t, err, "marshal DevContainer back to JSON failed")
|
||||
require.NotEmpty(t, marshaled, "marshaled JSON should not be empty")
|
||||
|
||||
// Verify the marshaled JSON can be unmarshaled back.
|
||||
var unmarshaled interface{}
|
||||
err = json.Unmarshal(marshaled, &unmarshaled)
|
||||
require.NoError(t, err, "unmarshal marshaled JSON failed")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalDevContainer_EdgeCases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty JSON",
|
||||
json: "{}",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
json: "{not valid json",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := dcspec.UnmarshalDevContainer([]byte(tt.json))
|
||||
if tt.wantErr {
|
||||
require.Error(t, err, "want error but got nil")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err, "unmarshal DevContainer failed")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,74 +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 \
|
||||
--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\/\/\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
else
|
||||
sed -i'' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n\/\/\n/' "${TMPDIR}/${DEST_FILENAME}"
|
||||
fi
|
||||
|
||||
mv -v "${TMPDIR}/${DEST_FILENAME}" "${DEST_PATH}"
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"image": "test-image",
|
||||
"runArgs": ["--network=host", "--privileged"],
|
||||
"forwardPorts": [8080, "3000:3000"]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["mads-hartmann.bash-ide-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "npm install -g @devcontainers/cli"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{ "image": "test-image" }
|
||||
@@ -1,91 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
const (
|
||||
// DevcontainerLocalFolderLabel is the label that contains the path to
|
||||
// the local workspace folder for a devcontainer.
|
||||
DevcontainerLocalFolderLabel = "devcontainer.local_folder"
|
||||
// DevcontainerConfigFileLabel is the label that contains the path to
|
||||
// the devcontainer.json configuration file.
|
||||
DevcontainerConfigFileLabel = "devcontainer.config_file"
|
||||
// DevcontainerIsTestRunLabel is set if the devcontainer is part of a test
|
||||
// and should be excluded.
|
||||
DevcontainerIsTestRunLabel = "devcontainer.is_test_run"
|
||||
// The default workspace folder inside the devcontainer.
|
||||
DevcontainerDefaultContainerWorkspaceFolder = "/workspaces"
|
||||
)
|
||||
|
||||
func ExtractDevcontainerScripts(
|
||||
devcontainers []codersdk.WorkspaceAgentDevcontainer,
|
||||
scripts []codersdk.WorkspaceAgentScript,
|
||||
) (filteredScripts []codersdk.WorkspaceAgentScript, devcontainerScripts map[uuid.UUID]codersdk.WorkspaceAgentScript) {
|
||||
devcontainerScripts = make(map[uuid.UUID]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 {
|
||||
devcontainerScripts[dc.ID] = script
|
||||
continue ScriptLoop
|
||||
}
|
||||
}
|
||||
|
||||
filteredScripts = append(filteredScripts, script)
|
||||
}
|
||||
|
||||
return filteredScripts, devcontainerScripts
|
||||
}
|
||||
|
||||
// ExpandAllDevcontainerPaths expands all devcontainer paths in the given
|
||||
// devcontainers. This is required by the devcontainer CLI, which requires
|
||||
// absolute paths for the workspace folder and config path.
|
||||
func ExpandAllDevcontainerPaths(logger slog.Logger, expandPath func(string) (string, error), devcontainers []codersdk.WorkspaceAgentDevcontainer) []codersdk.WorkspaceAgentDevcontainer {
|
||||
expanded := make([]codersdk.WorkspaceAgentDevcontainer, 0, len(devcontainers))
|
||||
for _, dc := range devcontainers {
|
||||
expanded = append(expanded, expandDevcontainerPaths(logger, expandPath, dc))
|
||||
}
|
||||
return expanded
|
||||
}
|
||||
|
||||
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,481 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// DevcontainerConfig is a wrapper around the output from `read-configuration`.
|
||||
// Unfortunately we cannot make use of `dcspec` as the output doesn't appear to
|
||||
// match.
|
||||
type DevcontainerConfig struct {
|
||||
MergedConfiguration DevcontainerMergedConfiguration `json:"mergedConfiguration"`
|
||||
Configuration DevcontainerConfiguration `json:"configuration"`
|
||||
Workspace DevcontainerWorkspace `json:"workspace"`
|
||||
}
|
||||
|
||||
type DevcontainerMergedConfiguration struct {
|
||||
Customizations DevcontainerMergedCustomizations `json:"customizations,omitempty"`
|
||||
Features DevcontainerFeatures `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
type DevcontainerMergedCustomizations struct {
|
||||
Coder []CoderCustomization `json:"coder,omitempty"`
|
||||
}
|
||||
|
||||
type DevcontainerFeatures map[string]any
|
||||
|
||||
// OptionsAsEnvs converts the DevcontainerFeatures into a list of
|
||||
// environment variables that can be used to set feature options.
|
||||
// The format is FEATURE_<FEATURE_NAME>_OPTION_<OPTION_NAME>=<value>.
|
||||
// For example, if the feature is:
|
||||
//
|
||||
// "ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
// "port": 9090,
|
||||
// }
|
||||
//
|
||||
// It will produce:
|
||||
//
|
||||
// FEATURE_CODE_SERVER_OPTION_PORT=9090
|
||||
//
|
||||
// Note that the feature name is derived from the last part of the key,
|
||||
// so "ghcr.io/coder/devcontainer-features/code-server:1" becomes
|
||||
// "CODE_SERVER". The version part (e.g. ":1") is removed, and dashes in
|
||||
// the feature and option names are replaced with underscores.
|
||||
func (f DevcontainerFeatures) OptionsAsEnvs() []string {
|
||||
var env []string
|
||||
for k, v := range f {
|
||||
vv, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Take the last part of the key as the feature name/path.
|
||||
k = k[strings.LastIndex(k, "/")+1:]
|
||||
// Remove ":" and anything following it.
|
||||
if idx := strings.Index(k, ":"); idx != -1 {
|
||||
k = k[:idx]
|
||||
}
|
||||
k = strings.ReplaceAll(k, "-", "_")
|
||||
for k2, v2 := range vv {
|
||||
k2 = strings.ReplaceAll(k2, "-", "_")
|
||||
env = append(env, fmt.Sprintf("FEATURE_%s_OPTION_%s=%s", strings.ToUpper(k), strings.ToUpper(k2), fmt.Sprintf("%v", v2)))
|
||||
}
|
||||
}
|
||||
slices.Sort(env)
|
||||
return env
|
||||
}
|
||||
|
||||
type DevcontainerConfiguration struct {
|
||||
Customizations DevcontainerCustomizations `json:"customizations,omitempty"`
|
||||
}
|
||||
|
||||
type DevcontainerCustomizations struct {
|
||||
Coder CoderCustomization `json:"coder,omitempty"`
|
||||
}
|
||||
|
||||
type CoderCustomization struct {
|
||||
DisplayApps map[codersdk.DisplayApp]bool `json:"displayApps,omitempty"`
|
||||
Apps []SubAgentApp `json:"apps,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Ignore bool `json:"ignore,omitempty"`
|
||||
}
|
||||
|
||||
type DevcontainerWorkspace struct {
|
||||
WorkspaceFolder string `json:"workspaceFolder"`
|
||||
}
|
||||
|
||||
// DevcontainerCLI is an interface for the devcontainer CLI.
|
||||
type DevcontainerCLI interface {
|
||||
Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (id string, err error)
|
||||
Exec(ctx context.Context, workspaceFolder, configPath string, cmd string, cmdArgs []string, opts ...DevcontainerCLIExecOptions) error
|
||||
ReadConfig(ctx context.Context, workspaceFolder, configPath string, env []string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error)
|
||||
}
|
||||
|
||||
// DevcontainerCLIUpOptions are options for the devcontainer CLI Up
|
||||
// command.
|
||||
type DevcontainerCLIUpOptions func(*devcontainerCLIUpConfig)
|
||||
|
||||
type devcontainerCLIUpConfig struct {
|
||||
args []string // Additional arguments for the Up command.
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
}
|
||||
|
||||
// WithRemoveExistingContainer is an option to remove the existing
|
||||
// container.
|
||||
func WithRemoveExistingContainer() DevcontainerCLIUpOptions {
|
||||
return func(o *devcontainerCLIUpConfig) {
|
||||
o.args = append(o.args, "--remove-existing-container")
|
||||
}
|
||||
}
|
||||
|
||||
// WithUpOutput sets additional stdout and stderr writers for logs
|
||||
// during Up operations.
|
||||
func WithUpOutput(stdout, stderr io.Writer) DevcontainerCLIUpOptions {
|
||||
return func(o *devcontainerCLIUpConfig) {
|
||||
o.stdout = stdout
|
||||
o.stderr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
// DevcontainerCLIExecOptions are options for the devcontainer CLI Exec
|
||||
// command.
|
||||
type DevcontainerCLIExecOptions func(*devcontainerCLIExecConfig)
|
||||
|
||||
type devcontainerCLIExecConfig struct {
|
||||
args []string // Additional arguments for the Exec command.
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
}
|
||||
|
||||
// WithExecOutput sets additional stdout and stderr writers for logs
|
||||
// during Exec operations.
|
||||
func WithExecOutput(stdout, stderr io.Writer) DevcontainerCLIExecOptions {
|
||||
return func(o *devcontainerCLIExecConfig) {
|
||||
o.stdout = stdout
|
||||
o.stderr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
// WithExecContainerID sets the container ID to target a specific
|
||||
// container.
|
||||
func WithExecContainerID(id string) DevcontainerCLIExecOptions {
|
||||
return func(o *devcontainerCLIExecConfig) {
|
||||
o.args = append(o.args, "--container-id", id)
|
||||
}
|
||||
}
|
||||
|
||||
// WithRemoteEnv sets environment variables for the Exec command.
|
||||
func WithRemoteEnv(env ...string) DevcontainerCLIExecOptions {
|
||||
return func(o *devcontainerCLIExecConfig) {
|
||||
for _, e := range env {
|
||||
o.args = append(o.args, "--remote-env", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DevcontainerCLIExecOptions are options for the devcontainer CLI ReadConfig
|
||||
// command.
|
||||
type DevcontainerCLIReadConfigOptions func(*devcontainerCLIReadConfigConfig)
|
||||
|
||||
type devcontainerCLIReadConfigConfig struct {
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
}
|
||||
|
||||
// WithReadConfigOutput sets additional stdout and stderr writers for logs
|
||||
// during ReadConfig operations.
|
||||
func WithReadConfigOutput(stdout, stderr io.Writer) DevcontainerCLIReadConfigOptions {
|
||||
return func(o *devcontainerCLIReadConfigConfig) {
|
||||
o.stdout = stdout
|
||||
o.stderr = stderr
|
||||
}
|
||||
}
|
||||
|
||||
func applyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions) devcontainerCLIUpConfig {
|
||||
conf := devcontainerCLIUpConfig{stdout: io.Discard, stderr: io.Discard}
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
opt(&conf)
|
||||
}
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
func applyDevcontainerCLIExecOptions(opts []DevcontainerCLIExecOptions) devcontainerCLIExecConfig {
|
||||
conf := devcontainerCLIExecConfig{stdout: io.Discard, stderr: io.Discard}
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
opt(&conf)
|
||||
}
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
func applyDevcontainerCLIReadConfigOptions(opts []DevcontainerCLIReadConfigOptions) devcontainerCLIReadConfigConfig {
|
||||
conf := devcontainerCLIReadConfigConfig{stdout: io.Discard, stderr: io.Discard}
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
opt(&conf)
|
||||
}
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
type devcontainerCLI struct {
|
||||
logger slog.Logger
|
||||
execer agentexec.Execer
|
||||
}
|
||||
|
||||
var _ DevcontainerCLI = &devcontainerCLI{}
|
||||
|
||||
func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) DevcontainerCLI {
|
||||
return &devcontainerCLI{
|
||||
execer: execer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (string, error) {
|
||||
conf := applyDevcontainerCLIUpOptions(opts)
|
||||
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath))
|
||||
|
||||
args := []string{
|
||||
"up",
|
||||
"--log-format", "json",
|
||||
"--workspace-folder", workspaceFolder,
|
||||
}
|
||||
if configPath != "" {
|
||||
args = append(args, "--config", configPath)
|
||||
}
|
||||
args = append(args, conf.args...)
|
||||
cmd := d.execer.CommandContext(ctx, "devcontainer", args...)
|
||||
|
||||
// Capture stdout for parsing and stream logs for both default and provided writers.
|
||||
var stdoutBuf bytes.Buffer
|
||||
cmd.Stdout = io.MultiWriter(
|
||||
&stdoutBuf,
|
||||
&devcontainerCLILogWriter{
|
||||
ctx: ctx,
|
||||
logger: logger.With(slog.F("stdout", true)),
|
||||
writer: conf.stdout,
|
||||
},
|
||||
)
|
||||
// Stream stderr logs and provided writer if any.
|
||||
cmd.Stderr = &devcontainerCLILogWriter{
|
||||
ctx: ctx,
|
||||
logger: logger.With(slog.F("stderr", true)),
|
||||
writer: conf.stderr,
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
_, err2 := parseDevcontainerCLILastLine[devcontainerCLIResult](ctx, logger, stdoutBuf.Bytes())
|
||||
if err2 != nil {
|
||||
err = errors.Join(err, err2)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := parseDevcontainerCLILastLine[devcontainerCLIResult](ctx, logger, stdoutBuf.Bytes())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.ContainerID, nil
|
||||
}
|
||||
|
||||
func (d *devcontainerCLI) Exec(ctx context.Context, workspaceFolder, configPath string, cmd string, cmdArgs []string, opts ...DevcontainerCLIExecOptions) error {
|
||||
conf := applyDevcontainerCLIExecOptions(opts)
|
||||
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath))
|
||||
|
||||
args := []string{"exec"}
|
||||
// For now, always set workspace folder even if --container-id is provided.
|
||||
// Otherwise the environment of exec will be incomplete, like `pwd` will be
|
||||
// /home/coder instead of /workspaces/coder. The downside is that the local
|
||||
// `devcontainer.json` config will overwrite settings serialized in the
|
||||
// container label.
|
||||
if workspaceFolder != "" {
|
||||
args = append(args, "--workspace-folder", workspaceFolder)
|
||||
}
|
||||
if configPath != "" {
|
||||
args = append(args, "--config", configPath)
|
||||
}
|
||||
args = append(args, conf.args...)
|
||||
args = append(args, cmd)
|
||||
args = append(args, cmdArgs...)
|
||||
c := d.execer.CommandContext(ctx, "devcontainer", args...)
|
||||
|
||||
c.Stdout = io.MultiWriter(conf.stdout, &devcontainerCLILogWriter{
|
||||
ctx: ctx,
|
||||
logger: logger.With(slog.F("stdout", true)),
|
||||
writer: io.Discard,
|
||||
})
|
||||
c.Stderr = io.MultiWriter(conf.stderr, &devcontainerCLILogWriter{
|
||||
ctx: ctx,
|
||||
logger: logger.With(slog.F("stderr", true)),
|
||||
writer: io.Discard,
|
||||
})
|
||||
|
||||
if err := c.Run(); err != nil {
|
||||
return xerrors.Errorf("devcontainer exec failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, configPath string, env []string, opts ...DevcontainerCLIReadConfigOptions) (DevcontainerConfig, error) {
|
||||
conf := applyDevcontainerCLIReadConfigOptions(opts)
|
||||
logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath))
|
||||
|
||||
args := []string{"read-configuration", "--include-merged-configuration"}
|
||||
if workspaceFolder != "" {
|
||||
args = append(args, "--workspace-folder", workspaceFolder)
|
||||
}
|
||||
if configPath != "" {
|
||||
args = append(args, "--config", configPath)
|
||||
}
|
||||
|
||||
c := d.execer.CommandContext(ctx, "devcontainer", args...)
|
||||
c.Env = append(c.Env, env...)
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
c.Stdout = io.MultiWriter(
|
||||
&stdoutBuf,
|
||||
&devcontainerCLILogWriter{
|
||||
ctx: ctx,
|
||||
logger: logger.With(slog.F("stdout", true)),
|
||||
writer: conf.stdout,
|
||||
},
|
||||
)
|
||||
c.Stderr = &devcontainerCLILogWriter{
|
||||
ctx: ctx,
|
||||
logger: logger.With(slog.F("stderr", true)),
|
||||
writer: conf.stderr,
|
||||
}
|
||||
|
||||
if err := c.Run(); err != nil {
|
||||
return DevcontainerConfig{}, xerrors.Errorf("devcontainer read-configuration failed: %w", err)
|
||||
}
|
||||
|
||||
config, err := parseDevcontainerCLILastLine[DevcontainerConfig](ctx, logger, stdoutBuf.Bytes())
|
||||
if err != nil {
|
||||
return DevcontainerConfig{}, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output
|
||||
// which is a JSON object.
|
||||
func parseDevcontainerCLILastLine[T any](ctx context.Context, logger slog.Logger, p []byte) (T, error) {
|
||||
var result T
|
||||
|
||||
s := bufio.NewScanner(bytes.NewReader(p))
|
||||
var lastLine []byte
|
||||
for s.Scan() {
|
||||
b := s.Bytes()
|
||||
if len(b) == 0 || b[0] != '{' {
|
||||
continue
|
||||
}
|
||||
lastLine = b
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
if len(lastLine) == 0 || lastLine[0] != '{' {
|
||||
logger.Error(ctx, "devcontainer result is not json", slog.F("result", string(lastLine)))
|
||||
return result, xerrors.Errorf("devcontainer result is not json: %q", string(lastLine))
|
||||
}
|
||||
if err := json.Unmarshal(lastLine, &result); err != nil {
|
||||
logger.Error(ctx, "parse devcontainer result failed", slog.Error(err), slog.F("result", string(lastLine)))
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// devcontainerCLIResult is the result of the devcontainer CLI command.
|
||||
// It is parsed from the last line of the devcontainer CLI stdout which
|
||||
// is a JSON object.
|
||||
type devcontainerCLIResult struct {
|
||||
Outcome string `json:"outcome"` // "error", "success".
|
||||
|
||||
// The following fields are set if outcome is success.
|
||||
ContainerID string `json:"containerId"`
|
||||
RemoteUser string `json:"remoteUser"`
|
||||
RemoteWorkspaceFolder string `json:"remoteWorkspaceFolder"`
|
||||
|
||||
// The following fields are set if outcome is error.
|
||||
Message string `json:"message"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (r *devcontainerCLIResult) UnmarshalJSON(data []byte) error {
|
||||
type wrapperResult devcontainerCLIResult
|
||||
|
||||
var wrappedResult wrapperResult
|
||||
if err := json.Unmarshal(data, &wrappedResult); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*r = devcontainerCLIResult(wrappedResult)
|
||||
return r.Err()
|
||||
}
|
||||
|
||||
func (r devcontainerCLIResult) Err() error {
|
||||
if r.Outcome == "success" {
|
||||
return nil
|
||||
}
|
||||
return xerrors.Errorf("devcontainer up failed: %s (description: %s, message: %s)", r.Outcome, r.Description, r.Message)
|
||||
}
|
||||
|
||||
// devcontainerCLIJSONLogLine is a log line from the devcontainer CLI.
|
||||
type devcontainerCLIJSONLogLine struct {
|
||||
Type string `json:"type"` // "progress", "raw", "start", "stop", "text", etc.
|
||||
Level int `json:"level"` // 1, 2, 3.
|
||||
Timestamp int `json:"timestamp"` // Unix timestamp in milliseconds.
|
||||
Text string `json:"text"`
|
||||
|
||||
// More fields can be added here as needed.
|
||||
}
|
||||
|
||||
// devcontainerCLILogWriter splits on newlines and logs each line
|
||||
// separately.
|
||||
type devcontainerCLILogWriter struct {
|
||||
ctx context.Context
|
||||
logger slog.Logger
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (l *devcontainerCLILogWriter) Write(p []byte) (n int, err error) {
|
||||
s := bufio.NewScanner(bytes.NewReader(p))
|
||||
for s.Scan() {
|
||||
line := s.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
if line[0] != '{' {
|
||||
l.logger.Debug(l.ctx, "@devcontainer/cli", slog.F("line", string(line)))
|
||||
continue
|
||||
}
|
||||
var logLine devcontainerCLIJSONLogLine
|
||||
if err := json.Unmarshal(line, &logLine); err != nil {
|
||||
l.logger.Error(l.ctx, "parse devcontainer json log line failed", slog.Error(err), slog.F("line", string(line)))
|
||||
continue
|
||||
}
|
||||
if logLine.Level >= 3 {
|
||||
l.logger.Info(l.ctx, "@devcontainer/cli", slog.F("line", string(line)))
|
||||
_, _ = l.writer.Write([]byte(strings.TrimSpace(logLine.Text) + "\n"))
|
||||
continue
|
||||
}
|
||||
// If we've successfully parsed the final log line, it will successfully parse
|
||||
// but will not fill out any of the fields for `logLine`. In this scenario we
|
||||
// assume it is the final log line, unmarshal it as that, and check if the
|
||||
// outcome is a non-empty string.
|
||||
if logLine.Level == 0 {
|
||||
var lastLine devcontainerCLIResult
|
||||
if err := json.Unmarshal(line, &lastLine); err == nil && lastLine.Outcome != "" {
|
||||
_, _ = l.writer.Write(line)
|
||||
_, _ = l.writer.Write([]byte{'\n'})
|
||||
}
|
||||
}
|
||||
l.logger.Debug(l.ctx, "@devcontainer/cli", slog.F("line", string(line)))
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
l.logger.Error(l.ctx, "devcontainer log line scan failed", slog.Error(err))
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
@@ -1,750 +0,0 @@
|
||||
package agentcontainers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testExePath, err := os.Executable()
|
||||
require.NoError(t, err, "get test executable path")
|
||||
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
t.Run("Up", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
logFile string
|
||||
workspace string
|
||||
config string
|
||||
opts []agentcontainers.DevcontainerCLIUpOptions
|
||||
wantArgs string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
logFile: "up.log",
|
||||
workspace: "/test/workspace",
|
||||
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "success with config",
|
||||
logFile: "up.log",
|
||||
workspace: "/test/workspace",
|
||||
config: "/test/config.json",
|
||||
wantArgs: "up --log-format json --workspace-folder /test/workspace --config /test/config.json",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "already exists",
|
||||
logFile: "up-already-exists.log",
|
||||
workspace: "/test/workspace",
|
||||
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "docker error",
|
||||
logFile: "up-error-docker.log",
|
||||
workspace: "/test/workspace",
|
||||
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "bad outcome",
|
||||
logFile: "up-error-bad-outcome.log",
|
||||
workspace: "/test/workspace",
|
||||
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "does not exist",
|
||||
logFile: "up-error-does-not-exist.log",
|
||||
workspace: "/test/workspace",
|
||||
wantArgs: "up --log-format json --workspace-folder /test/workspace",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "with remove existing container",
|
||||
logFile: "up.log",
|
||||
workspace: "/test/workspace",
|
||||
opts: []agentcontainers.DevcontainerCLIUpOptions{
|
||||
agentcontainers.WithRemoveExistingContainer(),
|
||||
},
|
||||
wantArgs: "up --log-format json --workspace-folder /test/workspace --remove-existing-container",
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
testExecer := &testDevcontainerExecer{
|
||||
testExePath: testExePath,
|
||||
wantArgs: tt.wantArgs,
|
||||
wantError: tt.wantError,
|
||||
logFile: filepath.Join("testdata", "devcontainercli", "parse", tt.logFile),
|
||||
}
|
||||
|
||||
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
||||
containerID, err := dccli.Up(ctx, tt.workspace, tt.config, tt.opts...)
|
||||
if tt.wantError {
|
||||
assert.Error(t, err, "want error")
|
||||
assert.Empty(t, containerID, "expected empty container ID")
|
||||
} else {
|
||||
assert.NoError(t, err, "want no error")
|
||||
assert.NotEmpty(t, containerID, "expected non-empty container ID")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Exec", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
workspaceFolder string
|
||||
configPath string
|
||||
cmd string
|
||||
cmdArgs []string
|
||||
opts []agentcontainers.DevcontainerCLIExecOptions
|
||||
wantArgs string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "simple command",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "",
|
||||
cmd: "echo",
|
||||
cmdArgs: []string{"hello"},
|
||||
wantArgs: "exec --workspace-folder /test/workspace echo hello",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "command with multiple args",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "/test/config.json",
|
||||
cmd: "ls",
|
||||
cmdArgs: []string{"-la", "/workspace"},
|
||||
wantArgs: "exec --workspace-folder /test/workspace --config /test/config.json ls -la /workspace",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "empty command args",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "",
|
||||
cmd: "bash",
|
||||
cmdArgs: nil,
|
||||
wantArgs: "exec --workspace-folder /test/workspace bash",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "workspace not found",
|
||||
workspaceFolder: "/nonexistent/workspace",
|
||||
configPath: "",
|
||||
cmd: "echo",
|
||||
cmdArgs: []string{"test"},
|
||||
wantArgs: "exec --workspace-folder /nonexistent/workspace echo test",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "with container ID",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "",
|
||||
cmd: "echo",
|
||||
cmdArgs: []string{"hello"},
|
||||
opts: []agentcontainers.DevcontainerCLIExecOptions{agentcontainers.WithExecContainerID("test-container-123")},
|
||||
wantArgs: "exec --workspace-folder /test/workspace --container-id test-container-123 echo hello",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "with container ID and config",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "/test/config.json",
|
||||
cmd: "bash",
|
||||
cmdArgs: []string{"-c", "ls -la"},
|
||||
opts: []agentcontainers.DevcontainerCLIExecOptions{agentcontainers.WithExecContainerID("my-container")},
|
||||
wantArgs: "exec --workspace-folder /test/workspace --config /test/config.json --container-id my-container bash -c ls -la",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "with container ID and output capture",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "",
|
||||
cmd: "cat",
|
||||
cmdArgs: []string{"/etc/hostname"},
|
||||
opts: []agentcontainers.DevcontainerCLIExecOptions{
|
||||
agentcontainers.WithExecContainerID("test-container-789"),
|
||||
},
|
||||
wantArgs: "exec --workspace-folder /test/workspace --container-id test-container-789 cat /etc/hostname",
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
testExecer := &testDevcontainerExecer{
|
||||
testExePath: testExePath,
|
||||
wantArgs: tt.wantArgs,
|
||||
wantError: tt.wantError,
|
||||
logFile: "", // Exec doesn't need log file parsing
|
||||
}
|
||||
|
||||
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
||||
err := dccli.Exec(ctx, tt.workspaceFolder, tt.configPath, tt.cmd, tt.cmdArgs, tt.opts...)
|
||||
if tt.wantError {
|
||||
assert.Error(t, err, "want error")
|
||||
} else {
|
||||
assert.NoError(t, err, "want no error")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ReadConfig", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
logFile string
|
||||
workspaceFolder string
|
||||
configPath string
|
||||
opts []agentcontainers.DevcontainerCLIReadConfigOptions
|
||||
wantArgs string
|
||||
wantError bool
|
||||
wantConfig agentcontainers.DevcontainerConfig
|
||||
}{
|
||||
{
|
||||
name: "WithCoderCustomization",
|
||||
logFile: "read-config-with-coder-customization.log",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "",
|
||||
wantArgs: "read-configuration --include-merged-configuration --workspace-folder /test/workspace",
|
||||
wantError: false,
|
||||
wantConfig: agentcontainers.DevcontainerConfig{
|
||||
MergedConfiguration: agentcontainers.DevcontainerMergedConfiguration{
|
||||
Customizations: agentcontainers.DevcontainerMergedCustomizations{
|
||||
Coder: []agentcontainers.CoderCustomization{
|
||||
{
|
||||
DisplayApps: map[codersdk.DisplayApp]bool{
|
||||
codersdk.DisplayAppVSCodeDesktop: true,
|
||||
codersdk.DisplayAppWebTerminal: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
DisplayApps: map[codersdk.DisplayApp]bool{
|
||||
codersdk.DisplayAppVSCodeInsiders: true,
|
||||
codersdk.DisplayAppWebTerminal: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "WithoutCoderCustomization",
|
||||
logFile: "read-config-without-coder-customization.log",
|
||||
workspaceFolder: "/test/workspace",
|
||||
configPath: "/test/config.json",
|
||||
wantArgs: "read-configuration --include-merged-configuration --workspace-folder /test/workspace --config /test/config.json",
|
||||
wantError: false,
|
||||
wantConfig: agentcontainers.DevcontainerConfig{
|
||||
MergedConfiguration: agentcontainers.DevcontainerMergedConfiguration{
|
||||
Customizations: agentcontainers.DevcontainerMergedCustomizations{
|
||||
Coder: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FileNotFound",
|
||||
logFile: "read-config-error-not-found.log",
|
||||
workspaceFolder: "/nonexistent/workspace",
|
||||
configPath: "",
|
||||
wantArgs: "read-configuration --include-merged-configuration --workspace-folder /nonexistent/workspace",
|
||||
wantError: true,
|
||||
wantConfig: agentcontainers.DevcontainerConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
testExecer := &testDevcontainerExecer{
|
||||
testExePath: testExePath,
|
||||
wantArgs: tt.wantArgs,
|
||||
wantError: tt.wantError,
|
||||
logFile: filepath.Join("testdata", "devcontainercli", "readconfig", tt.logFile),
|
||||
}
|
||||
|
||||
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
||||
config, err := dccli.ReadConfig(ctx, tt.workspaceFolder, tt.configPath, []string{}, tt.opts...)
|
||||
if tt.wantError {
|
||||
assert.Error(t, err, "want error")
|
||||
assert.Equal(t, agentcontainers.DevcontainerConfig{}, config, "expected empty config on error")
|
||||
} else {
|
||||
assert.NoError(t, err, "want no error")
|
||||
assert.Equal(t, tt.wantConfig, config, "expected config to match")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDevcontainerCLI_WithOutput tests that WithUpOutput and WithExecOutput capture CLI
|
||||
// logs to provided writers.
|
||||
func TestDevcontainerCLI_WithOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Prepare test executable and logger.
|
||||
testExePath, err := os.Executable()
|
||||
require.NoError(t, err, "get test executable path")
|
||||
|
||||
t.Run("Up", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Windows uses CRLF line endings, golden file is LF")
|
||||
}
|
||||
|
||||
// Buffers to capture stdout and stderr.
|
||||
outBuf := &bytes.Buffer{}
|
||||
errBuf := &bytes.Buffer{}
|
||||
|
||||
// Simulate CLI execution with a standard up.log file.
|
||||
wantArgs := "up --log-format json --workspace-folder /test/workspace"
|
||||
testExecer := &testDevcontainerExecer{
|
||||
testExePath: testExePath,
|
||||
wantArgs: wantArgs,
|
||||
wantError: false,
|
||||
logFile: filepath.Join("testdata", "devcontainercli", "parse", "up.log"),
|
||||
}
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
||||
|
||||
// Call Up with WithUpOutput to capture CLI logs.
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
containerID, err := dccli.Up(ctx, "/test/workspace", "", agentcontainers.WithUpOutput(outBuf, errBuf))
|
||||
require.NoError(t, err, "Up should succeed")
|
||||
require.NotEmpty(t, containerID, "expected non-empty container ID")
|
||||
|
||||
// Read expected log content.
|
||||
expLog, err := os.ReadFile(filepath.Join("testdata", "devcontainercli", "parse", "up.golden"))
|
||||
require.NoError(t, err, "reading expected log file")
|
||||
|
||||
// Verify stdout buffer contains the CLI logs and stderr is empty.
|
||||
assert.Equal(t, string(expLog), outBuf.String(), "stdout buffer should match CLI logs")
|
||||
assert.Empty(t, errBuf.String(), "stderr buffer should be empty on success")
|
||||
})
|
||||
|
||||
t.Run("Exec", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logFile := filepath.Join(t.TempDir(), "exec.log")
|
||||
f, err := os.Create(logFile)
|
||||
require.NoError(t, err, "create exec log file")
|
||||
_, err = f.WriteString("exec command log\n")
|
||||
require.NoError(t, err, "write to exec log file")
|
||||
err = f.Close()
|
||||
require.NoError(t, err, "close exec log file")
|
||||
|
||||
// Buffers to capture stdout and stderr.
|
||||
outBuf := &bytes.Buffer{}
|
||||
errBuf := &bytes.Buffer{}
|
||||
|
||||
// Simulate CLI execution for exec command with container ID.
|
||||
wantArgs := "exec --workspace-folder /test/workspace --container-id test-container-456 echo hello"
|
||||
testExecer := &testDevcontainerExecer{
|
||||
testExePath: testExePath,
|
||||
wantArgs: wantArgs,
|
||||
wantError: false,
|
||||
logFile: logFile,
|
||||
}
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer)
|
||||
|
||||
// Call Exec with WithExecOutput and WithContainerID to capture any command output.
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
err = dccli.Exec(ctx, "/test/workspace", "", "echo", []string{"hello"},
|
||||
agentcontainers.WithExecContainerID("test-container-456"),
|
||||
agentcontainers.WithExecOutput(outBuf, errBuf),
|
||||
)
|
||||
require.NoError(t, err, "Exec should succeed")
|
||||
|
||||
assert.NotEmpty(t, outBuf.String(), "stdout buffer should not be empty for exec with log file")
|
||||
assert.Empty(t, errBuf.String(), "stderr buffer should be empty")
|
||||
})
|
||||
}
|
||||
|
||||
// testDevcontainerExecer implements the agentexec.Execer interface for testing.
|
||||
type testDevcontainerExecer struct {
|
||||
testExePath string
|
||||
wantArgs string
|
||||
wantError bool
|
||||
logFile string
|
||||
}
|
||||
|
||||
// CommandContext returns a test binary command that simulates devcontainer responses.
|
||||
func (e *testDevcontainerExecer) CommandContext(ctx context.Context, name string, args ...string) *exec.Cmd {
|
||||
// Only handle "devcontainer" commands.
|
||||
if name != "devcontainer" {
|
||||
// For non-devcontainer commands, use a standard execer.
|
||||
return agentexec.DefaultExecer.CommandContext(ctx, name, args...)
|
||||
}
|
||||
|
||||
// Create a command that runs the test binary with special flags
|
||||
// that tell it to simulate a devcontainer command.
|
||||
testArgs := []string{
|
||||
"-test.run=TestDevcontainerHelperProcess",
|
||||
"--",
|
||||
name,
|
||||
}
|
||||
testArgs = append(testArgs, args...)
|
||||
|
||||
//nolint:gosec // This is a test binary, so we don't need to worry about command injection.
|
||||
cmd := exec.CommandContext(ctx, e.testExePath, testArgs...)
|
||||
// Set this environment variable so the child process knows it's the helper.
|
||||
cmd.Env = append(os.Environ(),
|
||||
"TEST_DEVCONTAINER_WANT_HELPER_PROCESS=1",
|
||||
"TEST_DEVCONTAINER_WANT_ARGS="+e.wantArgs,
|
||||
"TEST_DEVCONTAINER_WANT_ERROR="+fmt.Sprintf("%v", e.wantError),
|
||||
"TEST_DEVCONTAINER_LOG_FILE="+e.logFile,
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// PTYCommandContext returns a PTY command.
|
||||
func (*testDevcontainerExecer) PTYCommandContext(_ context.Context, name string, args ...string) *pty.Cmd {
|
||||
// This method shouldn't be called for our devcontainer tests.
|
||||
panic("PTYCommandContext not expected in devcontainer tests")
|
||||
}
|
||||
|
||||
// This is a special test helper that is executed as a subprocess.
|
||||
// It simulates the behavior of the devcontainer CLI.
|
||||
//
|
||||
//nolint:revive,paralleltest // This is a test helper function.
|
||||
func TestDevcontainerHelperProcess(t *testing.T) {
|
||||
// If not called by the test as a helper process, do nothing.
|
||||
if os.Getenv("TEST_DEVCONTAINER_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
helperArgs := flag.Args()
|
||||
if len(helperArgs) < 1 {
|
||||
fmt.Fprintf(os.Stderr, "No command\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if helperArgs[0] != "devcontainer" {
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", helperArgs[0])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Verify arguments against expected arguments and skip
|
||||
// "devcontainer", it's not included in the input args.
|
||||
wantArgs := os.Getenv("TEST_DEVCONTAINER_WANT_ARGS")
|
||||
gotArgs := strings.Join(helperArgs[1:], " ")
|
||||
if gotArgs != wantArgs {
|
||||
fmt.Fprintf(os.Stderr, "Arguments don't match.\nWant: %q\nGot: %q\n",
|
||||
wantArgs, gotArgs)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
logFilePath := os.Getenv("TEST_DEVCONTAINER_LOG_FILE")
|
||||
if logFilePath != "" {
|
||||
// Read and output log file for commands that need it (like "up")
|
||||
output, err := os.ReadFile(logFilePath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Reading log file %s failed: %v\n", logFilePath, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
_, _ = io.Copy(os.Stdout, bytes.NewReader(output))
|
||||
}
|
||||
|
||||
if os.Getenv("TEST_DEVCONTAINER_WANT_ERROR") == "true" {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// TestDockerDevcontainerCLI tests the DevcontainerCLI component with real Docker containers.
|
||||
// This test verifies that containers can be created and recreated using the actual
|
||||
// devcontainer CLI and Docker. It is skipped by default and can be run with:
|
||||
//
|
||||
// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerDevcontainerCLI
|
||||
//
|
||||
// The test requires Docker to be installed and running.
|
||||
func TestDockerDevcontainerCLI(t *testing.T) {
|
||||
t.Parallel()
|
||||
if os.Getenv("CODER_TEST_USE_DOCKER") != "1" {
|
||||
t.Skip("skipping Docker test; set CODER_TEST_USE_DOCKER=1 to run")
|
||||
}
|
||||
if _, err := exec.LookPath("devcontainer"); err != nil {
|
||||
t.Fatal("this test requires the devcontainer CLI: npm install -g @devcontainers/cli")
|
||||
}
|
||||
|
||||
// Connect to Docker.
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "connect to Docker")
|
||||
|
||||
t.Run("ContainerLifecycle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Set up workspace directory with a devcontainer configuration.
|
||||
workspaceFolder := t.TempDir()
|
||||
configPath := setupDevcontainerWorkspace(t, workspaceFolder)
|
||||
|
||||
// Use a long timeout because container operations are slow.
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
|
||||
// Create the devcontainer CLI under test.
|
||||
dccli := agentcontainers.NewDevcontainerCLI(logger, agentexec.DefaultExecer)
|
||||
|
||||
// Create a container.
|
||||
firstID, err := dccli.Up(ctx, workspaceFolder, configPath)
|
||||
require.NoError(t, err, "create container")
|
||||
require.NotEmpty(t, firstID, "container ID should not be empty")
|
||||
defer removeDevcontainerByID(t, pool, firstID)
|
||||
|
||||
// Verify container exists.
|
||||
firstContainer, found := findDevcontainerByID(t, pool, firstID)
|
||||
require.True(t, found, "container should exist")
|
||||
|
||||
// Remember the container creation time.
|
||||
firstCreated := firstContainer.Created
|
||||
|
||||
// Recreate the container.
|
||||
secondID, err := dccli.Up(ctx, workspaceFolder, configPath, agentcontainers.WithRemoveExistingContainer())
|
||||
require.NoError(t, err, "recreate container")
|
||||
require.NotEmpty(t, secondID, "recreated container ID should not be empty")
|
||||
defer removeDevcontainerByID(t, pool, secondID)
|
||||
|
||||
// Verify the new container exists and is different.
|
||||
secondContainer, found := findDevcontainerByID(t, pool, secondID)
|
||||
require.True(t, found, "recreated container should exist")
|
||||
|
||||
// Verify it's a different container by checking creation time.
|
||||
secondCreated := secondContainer.Created
|
||||
assert.NotEqual(t, firstCreated, secondCreated, "recreated container should have different creation time")
|
||||
|
||||
// Verify the first container is removed by the recreation.
|
||||
_, found = findDevcontainerByID(t, pool, firstID)
|
||||
assert.False(t, found, "first container should be removed")
|
||||
})
|
||||
}
|
||||
|
||||
// setupDevcontainerWorkspace prepares a test environment with a minimal
|
||||
// devcontainer.json configuration and returns the path to the config file.
|
||||
func setupDevcontainerWorkspace(t *testing.T, workspaceFolder string) string {
|
||||
t.Helper()
|
||||
|
||||
// Create the devcontainer directory structure.
|
||||
devcontainerDir := filepath.Join(workspaceFolder, ".devcontainer")
|
||||
err := os.MkdirAll(devcontainerDir, 0o755)
|
||||
require.NoError(t, err, "create .devcontainer directory")
|
||||
|
||||
// Write a minimal configuration with test labels for identification.
|
||||
configPath := filepath.Join(devcontainerDir, "devcontainer.json")
|
||||
content := `{
|
||||
"image": "alpine:latest",
|
||||
"containerEnv": {
|
||||
"TEST_CONTAINER": "true"
|
||||
},
|
||||
"runArgs": ["--label", "com.coder.test=devcontainercli"]
|
||||
}`
|
||||
err = os.WriteFile(configPath, []byte(content), 0o600)
|
||||
require.NoError(t, err, "create devcontainer.json file")
|
||||
|
||||
return configPath
|
||||
}
|
||||
|
||||
// findDevcontainerByID locates a container by its ID and verifies it has our
|
||||
// test label. Returns the container and whether it was found.
|
||||
func findDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) (*docker.Container, bool) {
|
||||
t.Helper()
|
||||
|
||||
container, err := pool.Client.InspectContainer(id)
|
||||
if err != nil {
|
||||
t.Logf("Inspect container failed: %v", err)
|
||||
return nil, false
|
||||
}
|
||||
require.Equal(t, "devcontainercli", container.Config.Labels["com.coder.test"], "sanity check failed: container should have the test label")
|
||||
|
||||
return container, true
|
||||
}
|
||||
|
||||
// removeDevcontainerByID safely cleans up a test container by ID, verifying
|
||||
// it has our test label before removal to prevent accidental deletion.
|
||||
func removeDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) {
|
||||
t.Helper()
|
||||
|
||||
errNoSuchContainer := &docker.NoSuchContainer{}
|
||||
|
||||
// Check if the container has the expected label.
|
||||
container, err := pool.Client.InspectContainer(id)
|
||||
if err != nil {
|
||||
if errors.As(err, &errNoSuchContainer) {
|
||||
t.Logf("Container %s not found, skipping removal", id)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err, "inspect container")
|
||||
}
|
||||
require.Equal(t, "devcontainercli", container.Config.Labels["com.coder.test"], "sanity check failed: container should have the test label")
|
||||
|
||||
t.Logf("Removing container with ID: %s", id)
|
||||
err = pool.Client.RemoveContainer(docker.RemoveContainerOptions{
|
||||
ID: container.ID,
|
||||
Force: true,
|
||||
RemoveVolumes: true,
|
||||
})
|
||||
if err != nil && !errors.As(err, &errNoSuchContainer) {
|
||||
assert.NoError(t, err, "remove container failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevcontainerFeatures_OptionsAsEnvs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realConfigJSON := `{
|
||||
"mergedConfiguration": {
|
||||
"features": {
|
||||
"./code-server": {
|
||||
"port": 9090
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var realConfig agentcontainers.DevcontainerConfig
|
||||
err := json.Unmarshal([]byte(realConfigJSON), &realConfig)
|
||||
require.NoError(t, err, "unmarshal JSON payload")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
features agentcontainers.DevcontainerFeatures
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "code-server feature",
|
||||
features: agentcontainers.DevcontainerFeatures{
|
||||
"./code-server": map[string]any{
|
||||
"port": 9090,
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "docker-in-docker feature",
|
||||
features: agentcontainers.DevcontainerFeatures{
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": map[string]any{
|
||||
"moby": "false",
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple features with multiple options",
|
||||
features: agentcontainers.DevcontainerFeatures{
|
||||
"./code-server": map[string]any{
|
||||
"port": 9090,
|
||||
"password": "secret",
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": map[string]any{
|
||||
"moby": "false",
|
||||
"docker-dash-compose-version": "v2",
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
"FEATURE_CODE_SERVER_OPTION_PASSWORD=secret",
|
||||
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
|
||||
"FEATURE_DOCKER_IN_DOCKER_OPTION_DOCKER_DASH_COMPOSE_VERSION=v2",
|
||||
"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "feature with non-map value (should be ignored)",
|
||||
features: agentcontainers.DevcontainerFeatures{
|
||||
"./code-server": map[string]any{
|
||||
"port": 9090,
|
||||
},
|
||||
"./invalid-feature": "not-a-map",
|
||||
},
|
||||
want: []string{
|
||||
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "real config example",
|
||||
features: realConfig.MergedConfiguration.Features,
|
||||
want: []string{
|
||||
"FEATURE_CODE_SERVER_OPTION_PORT=9090",
|
||||
"FEATURE_DOCKER_IN_DOCKER_OPTION_MOBY=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty features",
|
||||
features: agentcontainers.DevcontainerFeatures{},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := tt.features.OptionsAsEnvs()
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
require.Failf(t, "OptionsAsEnvs() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
"github.com/coder/coder/v2/pty"
|
||||
)
|
||||
|
||||
// CommandEnv is a function that returns the shell, working directory,
|
||||
// and environment variables to use when executing a command. It takes
|
||||
// an EnvInfoer and a pre-existing environment slice as arguments.
|
||||
// This signature matches agentssh.Server.CommandEnv.
|
||||
type CommandEnv func(ei usershell.EnvInfoer, addEnv []string) (shell, dir string, env []string, err error)
|
||||
|
||||
// commandEnvExecer is an agentexec.Execer that uses a CommandEnv to
|
||||
// determine the shell, working directory, and environment variables
|
||||
// for commands. It wraps another agentexec.Execer to provide the
|
||||
// necessary context.
|
||||
type commandEnvExecer struct {
|
||||
logger slog.Logger
|
||||
commandEnv CommandEnv
|
||||
execer agentexec.Execer
|
||||
}
|
||||
|
||||
func newCommandEnvExecer(
|
||||
logger slog.Logger,
|
||||
commandEnv CommandEnv,
|
||||
execer agentexec.Execer,
|
||||
) *commandEnvExecer {
|
||||
return &commandEnvExecer{
|
||||
logger: logger,
|
||||
commandEnv: commandEnv,
|
||||
execer: execer,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure commandEnvExecer implements agentexec.Execer.
|
||||
var _ agentexec.Execer = (*commandEnvExecer)(nil)
|
||||
|
||||
func (e *commandEnvExecer) prepare(ctx context.Context, inName string, inArgs ...string) (name string, args []string, dir string, env []string) {
|
||||
shell, dir, env, err := e.commandEnv(nil, nil)
|
||||
if err != nil {
|
||||
e.logger.Error(ctx, "get command environment failed", slog.Error(err))
|
||||
return inName, inArgs, "", nil
|
||||
}
|
||||
|
||||
caller := "-c"
|
||||
if runtime.GOOS == "windows" {
|
||||
caller = "/c"
|
||||
}
|
||||
name = shell
|
||||
for _, arg := range append([]string{inName}, inArgs...) {
|
||||
args = append(args, fmt.Sprintf("%q", arg))
|
||||
}
|
||||
args = []string{caller, strings.Join(args, " ")}
|
||||
return name, args, dir, env
|
||||
}
|
||||
|
||||
func (e *commandEnvExecer) CommandContext(ctx context.Context, cmd string, args ...string) *exec.Cmd {
|
||||
name, args, dir, env := e.prepare(ctx, cmd, args...)
|
||||
c := e.execer.CommandContext(ctx, name, args...)
|
||||
c.Dir = dir
|
||||
c.Env = env
|
||||
return c
|
||||
}
|
||||
|
||||
func (e *commandEnvExecer) PTYCommandContext(ctx context.Context, cmd string, args ...string) *pty.Cmd {
|
||||
name, args, dir, env := e.prepare(ctx, cmd, args...)
|
||||
c := e.execer.PTYCommandContext(ctx, name, args...)
|
||||
c.Dir = dir
|
||||
c.Env = env
|
||||
return c
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
package agentcontainers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// SubAgent represents an agent running in a dev container.
|
||||
type SubAgent struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
AuthToken uuid.UUID
|
||||
Directory string
|
||||
Architecture string
|
||||
OperatingSystem string
|
||||
Apps []SubAgentApp
|
||||
DisplayApps []codersdk.DisplayApp
|
||||
}
|
||||
|
||||
// CloneConfig makes a copy of SubAgent without ID and AuthToken. The
|
||||
// name is inherited from the devcontainer.
|
||||
func (s SubAgent) CloneConfig(dc codersdk.WorkspaceAgentDevcontainer) SubAgent {
|
||||
return SubAgent{
|
||||
Name: dc.Name,
|
||||
Directory: s.Directory,
|
||||
Architecture: s.Architecture,
|
||||
OperatingSystem: s.OperatingSystem,
|
||||
DisplayApps: slices.Clone(s.DisplayApps),
|
||||
Apps: slices.Clone(s.Apps),
|
||||
}
|
||||
}
|
||||
|
||||
func (s SubAgent) EqualConfig(other SubAgent) bool {
|
||||
return s.Name == other.Name &&
|
||||
s.Directory == other.Directory &&
|
||||
s.Architecture == other.Architecture &&
|
||||
s.OperatingSystem == other.OperatingSystem &&
|
||||
slices.Equal(s.DisplayApps, other.DisplayApps) &&
|
||||
slices.Equal(s.Apps, other.Apps)
|
||||
}
|
||||
|
||||
type SubAgentApp struct {
|
||||
Slug string `json:"slug"`
|
||||
Command string `json:"command"`
|
||||
DisplayName string `json:"displayName"`
|
||||
External bool `json:"external"`
|
||||
Group string `json:"group"`
|
||||
HealthCheck SubAgentHealthCheck `json:"healthCheck"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Icon string `json:"icon"`
|
||||
OpenIn codersdk.WorkspaceAppOpenIn `json:"openIn"`
|
||||
Order int32 `json:"order"`
|
||||
Share codersdk.WorkspaceAppSharingLevel `json:"share"`
|
||||
Subdomain bool `json:"subdomain"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (app SubAgentApp) ToProtoApp() (*agentproto.CreateSubAgentRequest_App, error) {
|
||||
proto := agentproto.CreateSubAgentRequest_App{
|
||||
Slug: app.Slug,
|
||||
External: &app.External,
|
||||
Hidden: &app.Hidden,
|
||||
Order: &app.Order,
|
||||
Subdomain: &app.Subdomain,
|
||||
}
|
||||
|
||||
if app.Command != "" {
|
||||
proto.Command = &app.Command
|
||||
}
|
||||
if app.DisplayName != "" {
|
||||
proto.DisplayName = &app.DisplayName
|
||||
}
|
||||
if app.Group != "" {
|
||||
proto.Group = &app.Group
|
||||
}
|
||||
if app.Icon != "" {
|
||||
proto.Icon = &app.Icon
|
||||
}
|
||||
if app.URL != "" {
|
||||
proto.Url = &app.URL
|
||||
}
|
||||
|
||||
if app.HealthCheck.URL != "" {
|
||||
proto.Healthcheck = &agentproto.CreateSubAgentRequest_App_Healthcheck{
|
||||
Interval: app.HealthCheck.Interval,
|
||||
Threshold: app.HealthCheck.Threshold,
|
||||
Url: app.HealthCheck.URL,
|
||||
}
|
||||
}
|
||||
|
||||
if app.OpenIn != "" {
|
||||
switch app.OpenIn {
|
||||
case codersdk.WorkspaceAppOpenInSlimWindow:
|
||||
proto.OpenIn = agentproto.CreateSubAgentRequest_App_SLIM_WINDOW.Enum()
|
||||
case codersdk.WorkspaceAppOpenInTab:
|
||||
proto.OpenIn = agentproto.CreateSubAgentRequest_App_TAB.Enum()
|
||||
default:
|
||||
return nil, xerrors.Errorf("unexpected codersdk.WorkspaceAppOpenIn: %#v", app.OpenIn)
|
||||
}
|
||||
}
|
||||
|
||||
if app.Share != "" {
|
||||
switch app.Share {
|
||||
case codersdk.WorkspaceAppSharingLevelAuthenticated:
|
||||
proto.Share = agentproto.CreateSubAgentRequest_App_AUTHENTICATED.Enum()
|
||||
case codersdk.WorkspaceAppSharingLevelOwner:
|
||||
proto.Share = agentproto.CreateSubAgentRequest_App_OWNER.Enum()
|
||||
case codersdk.WorkspaceAppSharingLevelPublic:
|
||||
proto.Share = agentproto.CreateSubAgentRequest_App_PUBLIC.Enum()
|
||||
case codersdk.WorkspaceAppSharingLevelOrganization:
|
||||
proto.Share = agentproto.CreateSubAgentRequest_App_ORGANIZATION.Enum()
|
||||
default:
|
||||
return nil, xerrors.Errorf("unexpected codersdk.WorkspaceAppSharingLevel: %#v", app.Share)
|
||||
}
|
||||
}
|
||||
|
||||
return &proto, nil
|
||||
}
|
||||
|
||||
type SubAgentHealthCheck struct {
|
||||
Interval int32 `json:"interval"`
|
||||
Threshold int32 `json:"threshold"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// SubAgentClient is an interface for managing sub agents and allows
|
||||
// changing the implementation without having to deal with the
|
||||
// agentproto package directly.
|
||||
type SubAgentClient interface {
|
||||
// List returns a list of all agents.
|
||||
List(ctx context.Context) ([]SubAgent, error)
|
||||
// Create adds a new agent.
|
||||
Create(ctx context.Context, agent SubAgent) (SubAgent, error)
|
||||
// Delete removes an agent by its ID.
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// NewSubAgentClient returns a SubAgentClient that uses the provided
|
||||
// agent API client.
|
||||
type subAgentAPIClient struct {
|
||||
logger slog.Logger
|
||||
api agentproto.DRPCAgentClient26
|
||||
}
|
||||
|
||||
var _ SubAgentClient = (*subAgentAPIClient)(nil)
|
||||
|
||||
func NewSubAgentClientFromAPI(logger slog.Logger, agentAPI agentproto.DRPCAgentClient26) SubAgentClient {
|
||||
if agentAPI == nil {
|
||||
panic("developer error: agentAPI cannot be nil")
|
||||
}
|
||||
return &subAgentAPIClient{
|
||||
logger: logger.Named("subagentclient"),
|
||||
api: agentAPI,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *subAgentAPIClient) List(ctx context.Context) ([]SubAgent, error) {
|
||||
a.logger.Debug(ctx, "listing sub agents")
|
||||
resp, err := a.api.ListSubAgents(ctx, &agentproto.ListSubAgentsRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agents := make([]SubAgent, len(resp.Agents))
|
||||
for i, agent := range resp.Agents {
|
||||
id, err := uuid.FromBytes(agent.GetId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authToken, err := uuid.FromBytes(agent.GetAuthToken())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agents[i] = SubAgent{
|
||||
ID: id,
|
||||
Name: agent.GetName(),
|
||||
AuthToken: authToken,
|
||||
}
|
||||
}
|
||||
return agents, nil
|
||||
}
|
||||
|
||||
func (a *subAgentAPIClient) Create(ctx context.Context, agent SubAgent) (SubAgent, error) {
|
||||
a.logger.Debug(ctx, "creating sub agent", slog.F("name", agent.Name), slog.F("directory", agent.Directory))
|
||||
|
||||
displayApps := make([]agentproto.CreateSubAgentRequest_DisplayApp, 0, len(agent.DisplayApps))
|
||||
for _, displayApp := range agent.DisplayApps {
|
||||
var app agentproto.CreateSubAgentRequest_DisplayApp
|
||||
switch displayApp {
|
||||
case codersdk.DisplayAppPortForward:
|
||||
app = agentproto.CreateSubAgentRequest_PORT_FORWARDING_HELPER
|
||||
case codersdk.DisplayAppSSH:
|
||||
app = agentproto.CreateSubAgentRequest_SSH_HELPER
|
||||
case codersdk.DisplayAppVSCodeDesktop:
|
||||
app = agentproto.CreateSubAgentRequest_VSCODE
|
||||
case codersdk.DisplayAppVSCodeInsiders:
|
||||
app = agentproto.CreateSubAgentRequest_VSCODE_INSIDERS
|
||||
case codersdk.DisplayAppWebTerminal:
|
||||
app = agentproto.CreateSubAgentRequest_WEB_TERMINAL
|
||||
default:
|
||||
return SubAgent{}, xerrors.Errorf("unexpected codersdk.DisplayApp: %#v", displayApp)
|
||||
}
|
||||
|
||||
displayApps = append(displayApps, app)
|
||||
}
|
||||
|
||||
apps := make([]*agentproto.CreateSubAgentRequest_App, 0, len(agent.Apps))
|
||||
for _, app := range agent.Apps {
|
||||
protoApp, err := app.ToProtoApp()
|
||||
if err != nil {
|
||||
return SubAgent{}, xerrors.Errorf("convert app: %w", err)
|
||||
}
|
||||
|
||||
apps = append(apps, protoApp)
|
||||
}
|
||||
|
||||
resp, err := a.api.CreateSubAgent(ctx, &agentproto.CreateSubAgentRequest{
|
||||
Name: agent.Name,
|
||||
Directory: agent.Directory,
|
||||
Architecture: agent.Architecture,
|
||||
OperatingSystem: agent.OperatingSystem,
|
||||
DisplayApps: displayApps,
|
||||
Apps: apps,
|
||||
})
|
||||
if err != nil {
|
||||
return SubAgent{}, err
|
||||
}
|
||||
|
||||
agent.Name = resp.Agent.Name
|
||||
agent.ID, err = uuid.FromBytes(resp.Agent.Id)
|
||||
if err != nil {
|
||||
return agent, err
|
||||
}
|
||||
agent.AuthToken, err = uuid.FromBytes(resp.Agent.AuthToken)
|
||||
if err != nil {
|
||||
return agent, err
|
||||
}
|
||||
|
||||
for _, appError := range resp.AppCreationErrors {
|
||||
app := apps[appError.Index]
|
||||
|
||||
a.logger.Warn(ctx, "unable to create app",
|
||||
slog.F("agent_name", agent.Name),
|
||||
slog.F("agent_id", agent.ID),
|
||||
slog.F("directory", agent.Directory),
|
||||
slog.F("app_slug", app.Slug),
|
||||
slog.F("field", appError.GetField()),
|
||||
slog.F("error", appError.GetError()),
|
||||
)
|
||||
}
|
||||
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (a *subAgentAPIClient) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
a.logger.Debug(ctx, "deleting sub agent", slog.F("id", id.String()))
|
||||
_, err := a.api.DeleteSubAgent(ctx, &agentproto.DeleteSubAgentRequest{
|
||||
Id: id[:],
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// noopSubAgentClient is a SubAgentClient that does nothing.
|
||||
type noopSubAgentClient struct{}
|
||||
|
||||
var _ SubAgentClient = noopSubAgentClient{}
|
||||
|
||||
func (noopSubAgentClient) List(_ context.Context) ([]SubAgent, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (noopSubAgentClient) Create(_ context.Context, _ SubAgent) (SubAgent, error) {
|
||||
return SubAgent{}, xerrors.New("noopSubAgentClient does not support creating sub agents")
|
||||
}
|
||||
|
||||
func (noopSubAgentClient) Delete(_ context.Context, _ uuid.UUID) error {
|
||||
return xerrors.New("noopSubAgentClient does not support deleting sub agents")
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
package agentcontainers_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
agentproto "github.com/coder/coder/v2/agent/proto"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestSubAgentClient_CreateWithDisplayApps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("CreateWithDisplayApps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
displayApps []codersdk.DisplayApp
|
||||
expectedApps []agentproto.CreateSubAgentRequest_DisplayApp
|
||||
}{
|
||||
{
|
||||
name: "single display app",
|
||||
displayApps: []codersdk.DisplayApp{codersdk.DisplayAppVSCodeDesktop},
|
||||
expectedApps: []agentproto.CreateSubAgentRequest_DisplayApp{
|
||||
agentproto.CreateSubAgentRequest_VSCODE,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple display apps",
|
||||
displayApps: []codersdk.DisplayApp{
|
||||
codersdk.DisplayAppVSCodeDesktop,
|
||||
codersdk.DisplayAppSSH,
|
||||
codersdk.DisplayAppPortForward,
|
||||
},
|
||||
expectedApps: []agentproto.CreateSubAgentRequest_DisplayApp{
|
||||
agentproto.CreateSubAgentRequest_VSCODE,
|
||||
agentproto.CreateSubAgentRequest_SSH_HELPER,
|
||||
agentproto.CreateSubAgentRequest_PORT_FORWARDING_HELPER,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all display apps",
|
||||
displayApps: []codersdk.DisplayApp{
|
||||
codersdk.DisplayAppPortForward,
|
||||
codersdk.DisplayAppSSH,
|
||||
codersdk.DisplayAppVSCodeDesktop,
|
||||
codersdk.DisplayAppVSCodeInsiders,
|
||||
codersdk.DisplayAppWebTerminal,
|
||||
},
|
||||
expectedApps: []agentproto.CreateSubAgentRequest_DisplayApp{
|
||||
agentproto.CreateSubAgentRequest_PORT_FORWARDING_HELPER,
|
||||
agentproto.CreateSubAgentRequest_SSH_HELPER,
|
||||
agentproto.CreateSubAgentRequest_VSCODE,
|
||||
agentproto.CreateSubAgentRequest_VSCODE_INSIDERS,
|
||||
agentproto.CreateSubAgentRequest_WEB_TERMINAL,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no display apps",
|
||||
displayApps: []codersdk.DisplayApp{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
logger := testutil.Logger(t)
|
||||
statsCh := make(chan *agentproto.Stats)
|
||||
|
||||
agentAPI := agenttest.NewClient(t, logger, uuid.New(), agentsdk.Manifest{}, statsCh, tailnet.NewCoordinator(logger))
|
||||
|
||||
agentClient, _, err := agentAPI.ConnectRPC26(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
subAgentClient := agentcontainers.NewSubAgentClientFromAPI(logger, agentClient)
|
||||
|
||||
// When: We create a sub agent with display apps.
|
||||
subAgent, err := subAgentClient.Create(ctx, agentcontainers.SubAgent{
|
||||
Name: "sub-agent-" + tt.name,
|
||||
Directory: "/workspaces/coder",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
DisplayApps: tt.displayApps,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
displayApps, err := agentAPI.GetSubAgentDisplayApps(subAgent.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: We expect the apps to be created.
|
||||
require.Equal(t, tt.expectedApps, displayApps)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateWithApps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
apps []agentcontainers.SubAgentApp
|
||||
expectedApps []*agentproto.CreateSubAgentRequest_App
|
||||
}{
|
||||
{
|
||||
name: "SlugOnly",
|
||||
apps: []agentcontainers.SubAgentApp{
|
||||
{
|
||||
Slug: "code-server",
|
||||
},
|
||||
},
|
||||
expectedApps: []*agentproto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "code-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AllFields",
|
||||
apps: []agentcontainers.SubAgentApp{
|
||||
{
|
||||
Slug: "jupyter",
|
||||
Command: "jupyter lab --port=8888",
|
||||
DisplayName: "Jupyter Lab",
|
||||
External: false,
|
||||
Group: "Development",
|
||||
HealthCheck: agentcontainers.SubAgentHealthCheck{
|
||||
Interval: 30,
|
||||
Threshold: 3,
|
||||
URL: "http://localhost:8888/api",
|
||||
},
|
||||
Hidden: false,
|
||||
Icon: "/icon/jupyter.svg",
|
||||
OpenIn: codersdk.WorkspaceAppOpenInTab,
|
||||
Order: int32(1),
|
||||
Share: codersdk.WorkspaceAppSharingLevelAuthenticated,
|
||||
Subdomain: true,
|
||||
URL: "http://localhost:8888",
|
||||
},
|
||||
},
|
||||
expectedApps: []*agentproto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "jupyter",
|
||||
Command: ptr.Ref("jupyter lab --port=8888"),
|
||||
DisplayName: ptr.Ref("Jupyter Lab"),
|
||||
External: ptr.Ref(false),
|
||||
Group: ptr.Ref("Development"),
|
||||
Healthcheck: &agentproto.CreateSubAgentRequest_App_Healthcheck{
|
||||
Interval: 30,
|
||||
Threshold: 3,
|
||||
Url: "http://localhost:8888/api",
|
||||
},
|
||||
Hidden: ptr.Ref(false),
|
||||
Icon: ptr.Ref("/icon/jupyter.svg"),
|
||||
OpenIn: agentproto.CreateSubAgentRequest_App_TAB.Enum(),
|
||||
Order: ptr.Ref(int32(1)),
|
||||
Share: agentproto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
|
||||
Subdomain: ptr.Ref(true),
|
||||
Url: ptr.Ref("http://localhost:8888"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AllSharingLevels",
|
||||
apps: []agentcontainers.SubAgentApp{
|
||||
{
|
||||
Slug: "owner-app",
|
||||
Share: codersdk.WorkspaceAppSharingLevelOwner,
|
||||
},
|
||||
{
|
||||
Slug: "authenticated-app",
|
||||
Share: codersdk.WorkspaceAppSharingLevelAuthenticated,
|
||||
},
|
||||
{
|
||||
Slug: "public-app",
|
||||
Share: codersdk.WorkspaceAppSharingLevelPublic,
|
||||
},
|
||||
{
|
||||
Slug: "organization-app",
|
||||
Share: codersdk.WorkspaceAppSharingLevelOrganization,
|
||||
},
|
||||
},
|
||||
expectedApps: []*agentproto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "owner-app",
|
||||
Share: agentproto.CreateSubAgentRequest_App_OWNER.Enum(),
|
||||
},
|
||||
{
|
||||
Slug: "authenticated-app",
|
||||
Share: agentproto.CreateSubAgentRequest_App_AUTHENTICATED.Enum(),
|
||||
},
|
||||
{
|
||||
Slug: "public-app",
|
||||
Share: agentproto.CreateSubAgentRequest_App_PUBLIC.Enum(),
|
||||
},
|
||||
{
|
||||
Slug: "organization-app",
|
||||
Share: agentproto.CreateSubAgentRequest_App_ORGANIZATION.Enum(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "WithHealthCheck",
|
||||
apps: []agentcontainers.SubAgentApp{
|
||||
{
|
||||
Slug: "health-app",
|
||||
HealthCheck: agentcontainers.SubAgentHealthCheck{
|
||||
Interval: 60,
|
||||
Threshold: 5,
|
||||
URL: "http://localhost:3000/health",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedApps: []*agentproto.CreateSubAgentRequest_App{
|
||||
{
|
||||
Slug: "health-app",
|
||||
Healthcheck: &agentproto.CreateSubAgentRequest_App_Healthcheck{
|
||||
Interval: 60,
|
||||
Threshold: 5,
|
||||
Url: "http://localhost:3000/health",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
logger := testutil.Logger(t)
|
||||
statsCh := make(chan *agentproto.Stats)
|
||||
|
||||
agentAPI := agenttest.NewClient(t, logger, uuid.New(), agentsdk.Manifest{}, statsCh, tailnet.NewCoordinator(logger))
|
||||
|
||||
agentClient, _, err := agentAPI.ConnectRPC26(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
subAgentClient := agentcontainers.NewSubAgentClientFromAPI(logger, agentClient)
|
||||
|
||||
// When: We create a sub agent with display apps.
|
||||
subAgent, err := subAgentClient.Create(ctx, agentcontainers.SubAgent{
|
||||
Name: "sub-agent-" + tt.name,
|
||||
Directory: "/workspaces/coder",
|
||||
Architecture: "amd64",
|
||||
OperatingSystem: "linux",
|
||||
Apps: tt.apps,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
apps, err := agentAPI.GetSubAgentApps(subAgent.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: We expect the apps to be created.
|
||||
require.Len(t, apps, len(tt.expectedApps))
|
||||
for i, expectedApp := range tt.expectedApps {
|
||||
actualApp := apps[i]
|
||||
|
||||
assert.Equal(t, expectedApp.Slug, actualApp.Slug)
|
||||
assert.Equal(t, expectedApp.Command, actualApp.Command)
|
||||
assert.Equal(t, expectedApp.DisplayName, actualApp.DisplayName)
|
||||
assert.Equal(t, ptr.NilToEmpty(expectedApp.External), ptr.NilToEmpty(actualApp.External))
|
||||
assert.Equal(t, expectedApp.Group, actualApp.Group)
|
||||
assert.Equal(t, ptr.NilToEmpty(expectedApp.Hidden), ptr.NilToEmpty(actualApp.Hidden))
|
||||
assert.Equal(t, expectedApp.Icon, actualApp.Icon)
|
||||
assert.Equal(t, ptr.NilToEmpty(expectedApp.Order), ptr.NilToEmpty(actualApp.Order))
|
||||
assert.Equal(t, ptr.NilToEmpty(expectedApp.Subdomain), ptr.NilToEmpty(actualApp.Subdomain))
|
||||
assert.Equal(t, expectedApp.Url, actualApp.Url)
|
||||
|
||||
if expectedApp.OpenIn != nil {
|
||||
require.NotNil(t, actualApp.OpenIn)
|
||||
assert.Equal(t, *expectedApp.OpenIn, *actualApp.OpenIn)
|
||||
} else {
|
||||
assert.Equal(t, expectedApp.OpenIn, actualApp.OpenIn)
|
||||
}
|
||||
|
||||
if expectedApp.Share != nil {
|
||||
require.NotNil(t, actualApp.Share)
|
||||
assert.Equal(t, *expectedApp.Share, *actualApp.Share)
|
||||
} else {
|
||||
assert.Equal(t, expectedApp.Share, actualApp.Share)
|
||||
}
|
||||
|
||||
if expectedApp.Healthcheck != nil {
|
||||
require.NotNil(t, expectedApp.Healthcheck)
|
||||
assert.Equal(t, expectedApp.Healthcheck.Interval, actualApp.Healthcheck.Interval)
|
||||
assert.Equal(t, expectedApp.Healthcheck.Threshold, actualApp.Healthcheck.Threshold)
|
||||
assert.Equal(t, expectedApp.Healthcheck.Url, actualApp.Healthcheck.Url)
|
||||
} else {
|
||||
assert.Equal(t, expectedApp.Healthcheck, actualApp.Healthcheck)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,68 +0,0 @@
|
||||
{"type":"text","level":3,"timestamp":1744102135254,"text":"@devcontainers/cli 0.75.0. Node.js v23.9.0. darwin 24.4.0 arm64."}
|
||||
{"type":"start","level":2,"timestamp":1744102135254,"text":"Run: docker buildx version"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135300,"text":"Run: docker buildx version","startTimestamp":1744102135254}
|
||||
{"type":"text","level":2,"timestamp":1744102135300,"text":"github.com/docker/buildx v0.21.2 1360a9e8d25a2c3d03c2776d53ae62e6ff0a843d\r\n"}
|
||||
{"type":"text","level":2,"timestamp":1744102135300,"text":"\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"}
|
||||
{"type":"start","level":2,"timestamp":1744102135300,"text":"Run: docker -v"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135309,"text":"Run: docker -v","startTimestamp":1744102135300}
|
||||
{"type":"start","level":2,"timestamp":1744102135309,"text":"Resolving Remote"}
|
||||
{"type":"start","level":2,"timestamp":1744102135311,"text":"Run: git rev-parse --show-cdup"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135316,"text":"Run: git rev-parse --show-cdup","startTimestamp":1744102135311}
|
||||
{"type":"start","level":2,"timestamp":1744102135316,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135333,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102135316}
|
||||
{"type":"start","level":2,"timestamp":1744102135333,"text":"Run: docker inspect --type container 4f22413fe134"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135347,"text":"Run: docker inspect --type container 4f22413fe134","startTimestamp":1744102135333}
|
||||
{"type":"start","level":2,"timestamp":1744102135348,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135364,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102135348}
|
||||
{"type":"start","level":2,"timestamp":1744102135364,"text":"Run: docker inspect --type container 4f22413fe134"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135378,"text":"Run: docker inspect --type container 4f22413fe134","startTimestamp":1744102135364}
|
||||
{"type":"start","level":2,"timestamp":1744102135379,"text":"Inspecting container"}
|
||||
{"type":"start","level":2,"timestamp":1744102135379,"text":"Run: docker inspect --type container 4f22413fe13472200500a66ca057df5aafba6b45743afd499c3f26fc886eb236"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135393,"text":"Run: docker inspect --type container 4f22413fe13472200500a66ca057df5aafba6b45743afd499c3f26fc886eb236","startTimestamp":1744102135379}
|
||||
{"type":"stop","level":2,"timestamp":1744102135393,"text":"Inspecting container","startTimestamp":1744102135379}
|
||||
{"type":"start","level":2,"timestamp":1744102135393,"text":"Run in container: /bin/sh"}
|
||||
{"type":"start","level":2,"timestamp":1744102135394,"text":"Run in container: uname -m"}
|
||||
{"type":"text","level":2,"timestamp":1744102135428,"text":"aarch64\n"}
|
||||
{"type":"text","level":2,"timestamp":1744102135428,"text":""}
|
||||
{"type":"stop","level":2,"timestamp":1744102135428,"text":"Run in container: uname -m","startTimestamp":1744102135394}
|
||||
{"type":"start","level":2,"timestamp":1744102135428,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null"}
|
||||
{"type":"text","level":2,"timestamp":1744102135428,"text":"PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"}
|
||||
{"type":"text","level":2,"timestamp":1744102135428,"text":""}
|
||||
{"type":"stop","level":2,"timestamp":1744102135428,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null","startTimestamp":1744102135428}
|
||||
{"type":"start","level":2,"timestamp":1744102135429,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135429,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)","startTimestamp":1744102135429}
|
||||
{"type":"start","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'"}
|
||||
{"type":"text","level":2,"timestamp":1744102135430,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135430,"text":""}
|
||||
{"type":"stop","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'","startTimestamp":1744102135430}
|
||||
{"type":"start","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'"}
|
||||
{"type":"text","level":2,"timestamp":1744102135430,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135430,"text":""}
|
||||
{"type":"stop","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'","startTimestamp":1744102135430}
|
||||
{"type":"text","level":2,"timestamp":1744102135431,"text":"userEnvProbe: loginInteractiveShell (default)"}
|
||||
{"type":"text","level":1,"timestamp":1744102135431,"text":"LifecycleCommandExecutionMap: {\n \"onCreateCommand\": [],\n \"updateContentCommand\": [],\n \"postCreateCommand\": [\n {\n \"origin\": \"devcontainer.json\",\n \"command\": \"npm install -g @devcontainers/cli\"\n }\n ],\n \"postStartCommand\": [],\n \"postAttachCommand\": [],\n \"initializeCommand\": []\n}"}
|
||||
{"type":"text","level":2,"timestamp":1744102135431,"text":"userEnvProbe: not found in cache"}
|
||||
{"type":"text","level":2,"timestamp":1744102135431,"text":"userEnvProbe shell: /bin/bash"}
|
||||
{"type":"start","level":2,"timestamp":1744102135431,"text":"Run in container: /bin/bash -lic echo -n 5805f204-cd2b-4911-8a88-96de28d5deb7; cat /proc/self/environ; echo -n 5805f204-cd2b-4911-8a88-96de28d5deb7"}
|
||||
{"type":"start","level":2,"timestamp":1744102135431,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.onCreateCommandMarker'"}
|
||||
{"type":"text","level":2,"timestamp":1744102135432,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135432,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135432,"text":"Exit code 1"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135432,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.onCreateCommandMarker'","startTimestamp":1744102135431}
|
||||
{"type":"start","level":2,"timestamp":1744102135432,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.updateContentCommandMarker'"}
|
||||
{"type":"text","level":2,"timestamp":1744102135434,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135434,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135434,"text":"Exit code 1"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135434,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.updateContentCommandMarker'","startTimestamp":1744102135432}
|
||||
{"type":"start","level":2,"timestamp":1744102135434,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.postCreateCommandMarker'"}
|
||||
{"type":"text","level":2,"timestamp":1744102135435,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135435,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135435,"text":"Exit code 1"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135435,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.postCreateCommandMarker'","startTimestamp":1744102135434}
|
||||
{"type":"start","level":2,"timestamp":1744102135435,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:48:29.406495039Z}\" != '2025-04-08T08:48:29.406495039Z' ] && echo '2025-04-08T08:48:29.406495039Z' > '/home/node/.devcontainer/.postStartCommandMarker'"}
|
||||
{"type":"text","level":2,"timestamp":1744102135436,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135436,"text":""}
|
||||
{"type":"text","level":2,"timestamp":1744102135436,"text":"Exit code 1"}
|
||||
{"type":"stop","level":2,"timestamp":1744102135436,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:48:29.406495039Z}\" != '2025-04-08T08:48:29.406495039Z' ] && echo '2025-04-08T08:48:29.406495039Z' > '/home/node/.devcontainer/.postStartCommandMarker'","startTimestamp":1744102135435}
|
||||
{"type":"stop","level":2,"timestamp":1744102135436,"text":"Resolving Remote","startTimestamp":1744102135309}
|
||||
{"outcome":"success","containerId":"4f22413fe13472200500a66ca057df5aafba6b45743afd499c3f26fc886eb236","remoteUser":"node","remoteWorkspaceFolder":"/workspaces/devcontainers-template-starter"}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user