Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08998a1974 | |||
| ed3bb76c9b | |||
| 05b02cf887 | |||
| da71e546bb | |||
| 67024b80fa | |||
| 90b8ef63b8 | |||
| f6ddfdecb7 | |||
| 3641404e93 | |||
| 46fd4aa03e | |||
| cdb3ddcc2c | |||
| aa4fd67142 | |||
| 0771555a89 | |||
| 27c3ec072e | |||
| a59a84b2a7 | |||
| 6abb889fab | |||
| fca93438ee | |||
| 8f15caad22 | |||
| 84760f4a8f | |||
| d92649ae3b | |||
| 4379230a27 | |||
| e31578da4b | |||
| 4844c978d8 | |||
| f6b025ec7b | |||
| 779f1571a6 | |||
| ea9f003cdd | |||
| f3e26ca557 | |||
| b04e6870ea | |||
| ce9e7ad909 | |||
| b199eb1c38 | |||
| 97bc7eb9e5 | |||
| 244e6ca027 | |||
| 3a0e8af6e3 | |||
| 50d42ab0b9 | |||
| a6285dde5e | |||
| ac1d51aeca | |||
| 493f77120c | |||
| b228c6b135 | |||
| 3ae0600174 | |||
| 731683ab4f | |||
| 7fc8ee4c60 | |||
| d351821ec3 | |||
| 0c453d7f8e | |||
| 04d5ff88e4 | |||
| 52243557a2 | |||
| 4d15b30a63 | |||
| 8a3f8373e8 | |||
| 25400fedca | |||
| 82bb833099 | |||
| 61beb7bfa8 | |||
| b4be5bcfed | |||
| ceaba0778e | |||
| e24cc5e6da | |||
| 259dee2ea8 | |||
| 8e0516a19c | |||
| 770fdb377c | |||
| 83dbf73dde | |||
| 0ab23abb19 | |||
| c4bf5a2d81 | |||
| 5cb02a6cc0 | |||
| cfdd4a9b88 | |||
| d9159103cd | |||
| 532a1f3054 | |||
| 6aeb144a98 | |||
| f94d8fc019 | |||
| e93a917c2f | |||
| 0f054096e4 | |||
| 2f829286f2 | |||
| 6acfcd5736 | |||
| 9e021f7b57 | |||
| aa306f2262 |
@@ -0,0 +1,321 @@
|
||||
# Documentation Style Guide
|
||||
|
||||
This guide documents documentation patterns observed in the Coder repository, based on analysis of existing admin guides, tutorials, and reference documentation. This is specifically for documentation files in the `docs/` directory - see [CONTRIBUTING.md](../../docs/about/contributing/CONTRIBUTING.md) for general contribution guidelines.
|
||||
|
||||
## Research Before Writing
|
||||
|
||||
Before documenting a feature:
|
||||
|
||||
1. **Research similar documentation** - Read recent documentation pages in `docs/` to understand writing style, structure, and conventions for your content type (admin guides, tutorials, reference docs, etc.)
|
||||
2. **Read the code implementation** - Check backend endpoints, frontend components, database queries
|
||||
3. **Verify permissions model** - Look up RBAC actions in `coderd/rbac/` (e.g., `view_insights` for Template Insights)
|
||||
4. **Check UI thresholds and defaults** - Review frontend code for color thresholds, time intervals, display logic
|
||||
5. **Cross-reference with tests** - Test files document expected behavior and edge cases
|
||||
6. **Verify API endpoints** - Check `coderd/coderd.go` for route registration
|
||||
|
||||
### Code Verification Checklist
|
||||
|
||||
When documenting features, always verify these implementation details:
|
||||
|
||||
- Read handler implementation in `coderd/`
|
||||
- Check permission requirements in `coderd/rbac/`
|
||||
- Review frontend components in `site/src/pages/` or `site/src/modules/`
|
||||
- Verify display thresholds and intervals (e.g., color codes, time defaults)
|
||||
- Confirm API endpoint paths and parameters
|
||||
- Check for server flags in serpent configuration
|
||||
|
||||
## Document Structure
|
||||
|
||||
### Title and Introduction Pattern
|
||||
|
||||
**H1 heading**: Single clear title without prefix
|
||||
|
||||
```markdown
|
||||
# Template Insights
|
||||
```
|
||||
|
||||
**Introduction**: 1-2 sentences describing what the feature does, concise and actionable
|
||||
|
||||
```markdown
|
||||
Template Insights provides detailed analytics and usage metrics for your Coder templates.
|
||||
```
|
||||
|
||||
### Premium Feature Callout
|
||||
|
||||
For Premium-only features, add `(Premium)` suffix to the H1 heading. The documentation system automatically links these to premium pricing information. You should also add a premium badge in the `docs/manifest.json` file with `"state": ["premium"]`.
|
||||
|
||||
```markdown
|
||||
# Template Insights (Premium)
|
||||
```
|
||||
|
||||
### Overview Section Pattern
|
||||
|
||||
Common pattern after introduction:
|
||||
|
||||
```markdown
|
||||
## Overview
|
||||
|
||||
Template Insights offers visibility into:
|
||||
|
||||
- **Active Users**: Track the number of users actively using workspaces
|
||||
- **Application Usage**: See which applications users are accessing
|
||||
```
|
||||
|
||||
Use bold labels for capabilities, provides high-level understanding before details.
|
||||
|
||||
## Image Usage
|
||||
|
||||
### Placement and Format
|
||||
|
||||
**Place images after descriptive text**, then add caption:
|
||||
|
||||
```markdown
|
||||

|
||||
|
||||
<small>Template Insights showing weekly active users and connection latency metrics.</small>
|
||||
```
|
||||
|
||||
- Image format: ``
|
||||
- Caption: Use `<small>` tag below images
|
||||
- Alt text: Describe what's shown, not just repeat heading
|
||||
|
||||
### Image-Driven Documentation
|
||||
|
||||
When you have multiple screenshots showing different aspects of a feature:
|
||||
|
||||
1. **Structure sections around images** - Each major screenshot gets its own section
|
||||
2. **Describe what's visible** - Reference specific UI elements, data values shown in the screenshot
|
||||
3. **Flow naturally** - Let screenshots guide the reader through the feature
|
||||
|
||||
**Example**: Template Insights documentation has 3 screenshots that define the 3 main content sections.
|
||||
|
||||
### Screenshot Guidelines
|
||||
|
||||
**When screenshots are not yet available**: If you're documenting a feature before screenshots exist, you can use image placeholders with descriptive alt text and ask the user to provide screenshots:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
Then ask: "Could you provide a screenshot of the Template Insights page? I've added a placeholder at [location]."
|
||||
|
||||
**When documenting with screenshots**:
|
||||
|
||||
- Illustrate features being discussed in preceding text
|
||||
- Show actual UI/data, not abstract concepts
|
||||
- Reference specific values shown when explaining features
|
||||
- Organize documentation around key screenshots
|
||||
|
||||
## Content Organization
|
||||
|
||||
### Section Hierarchy
|
||||
|
||||
1. **H2 (##)**: Major sections - "Overview", "Accessing [Feature]", "Use Cases"
|
||||
2. **H3 (###)**: Subsections within major sections
|
||||
3. **H4 (####)**: Rare, only for deeply nested content
|
||||
|
||||
### Common Section Patterns
|
||||
|
||||
- **Accessing [Feature]**: How to navigate to/use the feature
|
||||
- **Use Cases**: Practical applications
|
||||
- **Permissions**: Access control information
|
||||
- **API Access**: Programmatic access details
|
||||
- **Related Documentation**: Links to related content
|
||||
|
||||
### Lists and Callouts
|
||||
|
||||
- **Unordered lists**: Non-sequential items, features, capabilities
|
||||
- **Ordered lists**: Step-by-step instructions
|
||||
- **Tables**: Comparing options, showing permissions, listing parameters
|
||||
- **Callouts**:
|
||||
- `> [!NOTE]` for additional information
|
||||
- `> [!WARNING]` for important warnings
|
||||
- `> [!TIP]` for helpful tips
|
||||
- **Tabs**: Use tabs for presenting related but parallel content, such as different installation methods or platform-specific instructions. Tabs work well when readers need to choose one path that applies to their specific situation.
|
||||
|
||||
## Writing Style
|
||||
|
||||
### Tone and Voice
|
||||
|
||||
- **Direct and concise**: Avoid unnecessary words
|
||||
- **Active voice**: "Template Insights tracks users" not "Users are tracked"
|
||||
- **Present tense**: "The chart displays..." not "The chart will display..."
|
||||
- **Second person**: "You can view..." for instructions
|
||||
|
||||
### Terminology
|
||||
|
||||
- **Consistent terms**: Use same term throughout (e.g., "workspace" not "workspace environment")
|
||||
- **Bold for UI elements**: "Navigate to the **Templates** page"
|
||||
- **Code formatting**: Use backticks for commands, file paths, code
|
||||
- Inline: `` `coder server` ``
|
||||
- Blocks: Use triple backticks with language identifier
|
||||
|
||||
### Instructions
|
||||
|
||||
- **Numbered lists** for sequential steps
|
||||
- **Start with verb**: "Navigate to", "Click", "Select", "Run"
|
||||
- **Be specific**: Include exact button/menu names in bold
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Command Examples
|
||||
|
||||
````markdown
|
||||
```sh
|
||||
coder server --disable-template-insights
|
||||
```
|
||||
````
|
||||
|
||||
### Environment Variables
|
||||
|
||||
````markdown
|
||||
```sh
|
||||
CODER_DISABLE_TEMPLATE_INSIGHTS=true
|
||||
```
|
||||
````
|
||||
|
||||
### Code Comments
|
||||
|
||||
- Keep minimal
|
||||
- Explain non-obvious parameters
|
||||
- Use `# Comment` for shell, `// Comment` for other languages
|
||||
|
||||
## Links and References
|
||||
|
||||
### Internal Links
|
||||
|
||||
Use relative paths from current file location:
|
||||
|
||||
- `[Template Permissions](./template-permissions.md)`
|
||||
- `[API documentation](../../reference/api/insights.md)`
|
||||
|
||||
For cross-linking to Coder registry templates or other external Coder resources, reference the appropriate registry URLs.
|
||||
|
||||
### Cross-References
|
||||
|
||||
- Link to related documentation at the end
|
||||
- Use descriptive text: "Learn about [template access control](./template-permissions.md)"
|
||||
- Not just: "[Click here](./template-permissions.md)"
|
||||
|
||||
### API References
|
||||
|
||||
Link to specific endpoints:
|
||||
|
||||
```markdown
|
||||
- `/api/v2/insights/templates` - Template usage metrics
|
||||
```
|
||||
|
||||
## Accuracy Standards
|
||||
|
||||
### Specific Numbers Matter
|
||||
|
||||
Document exact values from code:
|
||||
|
||||
- **Thresholds**: "green < 150ms, yellow 150-300ms, red ≥300ms"
|
||||
- **Time intervals**: "daily for templates < 5 weeks old, weekly for 5+ weeks"
|
||||
- **Counts and limits**: Use precise numbers, not approximations
|
||||
|
||||
### Permission Actions
|
||||
|
||||
- Use exact RBAC action names from code (e.g., `view_insights` not "view insights")
|
||||
- Reference permission system correctly (`template:view_insights` scope)
|
||||
- Specify which roles have permissions by default
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- Use full, correct paths (e.g., `/api/v2/insights/templates` not `/insights/templates`)
|
||||
- Link to generated API documentation in `docs/reference/api/`
|
||||
|
||||
## Documentation Manifest
|
||||
|
||||
**CRITICAL**: All documentation pages must be added to `docs/manifest.json` to appear in navigation. Read the manifest file to understand the structure and find the appropriate section for your documentation. Place new pages in logical sections matching the existing hierarchy.
|
||||
|
||||
## Proactive Documentation
|
||||
|
||||
When documenting features that depend on upcoming PRs:
|
||||
|
||||
1. **Reference the PR explicitly** - Mention PR number and what it adds
|
||||
2. **Document the feature anyway** - Write as if feature exists
|
||||
3. **Link to auto-generated docs** - Point to CLI reference sections that will be created
|
||||
4. **Update PR description** - Note documentation is included proactively
|
||||
|
||||
**Example**: Template Insights docs include `--disable-template-insights` flag from PR #20940 before it merged, with link to `../../reference/cli/server.md#--disable-template-insights` that will exist when the PR lands.
|
||||
|
||||
## Special Sections
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **H3 subheadings** for each issue
|
||||
- Format: Issue description followed by solution steps
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Bullet or numbered list
|
||||
- Include version requirements, dependencies, permissions
|
||||
|
||||
## Formatting and Linting
|
||||
|
||||
**Always run these commands before submitting documentation:**
|
||||
|
||||
```sh
|
||||
make fmt/markdown # Format markdown tables and content
|
||||
make lint/markdown # Lint and fix markdown issues
|
||||
```
|
||||
|
||||
These ensure consistent formatting and catch common documentation errors.
|
||||
|
||||
## Formatting Conventions
|
||||
|
||||
### Text Formatting
|
||||
|
||||
- **Bold** (`**text**`): UI elements, important concepts, labels
|
||||
- *Italic* (`*text*`): Rare, mainly for emphasis
|
||||
- `Code` (`` `text` ``): Commands, file paths, parameter names
|
||||
|
||||
### Tables
|
||||
|
||||
- Use for comparing options, listing parameters, showing permissions
|
||||
- Left-align text, right-align numbers
|
||||
- Keep simple - avoid nested formatting when possible
|
||||
|
||||
### Code Blocks
|
||||
|
||||
- **Always specify language**: `` ```sh ``, `` ```yaml ``, `` ```go ``
|
||||
- Include comments for complex examples
|
||||
- Keep minimal - show only relevant configuration
|
||||
|
||||
## Document Length
|
||||
|
||||
- **Comprehensive but scannable**: Cover all aspects but use clear headings
|
||||
- **Break up long sections**: Use H3 subheadings for logical chunks
|
||||
- **Visual hierarchy**: Images and code blocks break up text
|
||||
|
||||
## Auto-Generated Content
|
||||
|
||||
Some content is auto-generated with comments:
|
||||
|
||||
```markdown
|
||||
<!-- Code generated by 'make docs/...' DO NOT EDIT -->
|
||||
```
|
||||
|
||||
Don't manually edit auto-generated sections.
|
||||
|
||||
## URL Redirects
|
||||
|
||||
When renaming or moving documentation pages, redirects must be added to prevent broken links.
|
||||
|
||||
**Important**: Redirects are NOT configured in this repository. The coder.com website runs on Vercel with Next.js and reads redirects from a separate repository:
|
||||
|
||||
- **Redirect configuration**: https://github.com/coder/coder.com/blob/master/redirects.json
|
||||
- **Do NOT create** a `docs/_redirects` file - this format (used by Netlify/Cloudflare Pages) is not processed by coder.com
|
||||
|
||||
When you rename or move a doc page, create a PR in coder/coder.com to add the redirect.
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **Research first** - Verify against actual code implementation
|
||||
2. **Be precise** - Use exact numbers, permission names, API paths
|
||||
3. **Visual structure** - Organize around screenshots when available
|
||||
4. **Link everything** - Related docs, API endpoints, CLI references
|
||||
5. **Manifest inclusion** - Add to manifest.json for navigation
|
||||
6. **Add redirects** - When moving/renaming pages, add redirects in coder/coder.com repo
|
||||
@@ -0,0 +1,256 @@
|
||||
# Pull Request Description Style Guide
|
||||
|
||||
This guide documents the PR description style used in the Coder repository, based on analysis of recent merged PRs.
|
||||
|
||||
## PR Title Format
|
||||
|
||||
Follow [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) format:
|
||||
|
||||
```text
|
||||
type(scope): brief description
|
||||
```
|
||||
|
||||
**Common types:**
|
||||
|
||||
- `feat`: New features
|
||||
- `fix`: Bug fixes
|
||||
- `refactor`: Code refactoring without behavior change
|
||||
- `perf`: Performance improvements
|
||||
- `docs`: Documentation changes
|
||||
- `chore`: Dependency updates, tooling changes
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `feat: add tracing to aibridge`
|
||||
- `fix: move contexts to appropriate locations`
|
||||
- `perf(coderd/database): add index on workspace_app_statuses.app_id`
|
||||
- `docs: fix swagger tags for license endpoints`
|
||||
- `refactor(site): remove redundant client-side sorting of app statuses`
|
||||
|
||||
## PR Description Structure
|
||||
|
||||
### Default Pattern: Keep It Concise
|
||||
|
||||
Most PRs use a simple 1-2 paragraph format:
|
||||
|
||||
```markdown
|
||||
[Brief statement of what changed]
|
||||
|
||||
[One sentence explaining technical details or context if needed]
|
||||
```
|
||||
|
||||
**Example (bugfix):**
|
||||
|
||||
```markdown
|
||||
Previously, when a devcontainer config file was modified, the dirty
|
||||
status was updated internally but not broadcast to websocket listeners.
|
||||
|
||||
Add `broadcastUpdatesLocked()` call in `markDevcontainerDirty` to notify
|
||||
websocket listeners immediately when a config file changes.
|
||||
```
|
||||
|
||||
**Example (dependency update):**
|
||||
|
||||
```markdown
|
||||
Changes from https://github.com/upstream/repo/pull/XXX/
|
||||
```
|
||||
|
||||
**Example (docs correction):**
|
||||
|
||||
```markdown
|
||||
Removes incorrect references to database replicas from the scaling documentation.
|
||||
Coder only supports a single database connection URL.
|
||||
```
|
||||
|
||||
### For Complex Changes: Use "Summary", "Problem", "Fix"
|
||||
|
||||
Only use structured sections when the change requires significant explanation:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Brief overview of the change
|
||||
|
||||
## Problem
|
||||
Detailed explanation of the issue being addressed
|
||||
|
||||
## Fix
|
||||
How the solution works
|
||||
```
|
||||
|
||||
**Example (API documentation fix):**
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Change `@Tags` from `Organizations` to `Enterprise` for POST /licenses...
|
||||
|
||||
## Problem
|
||||
The license API endpoints were inconsistently tagged...
|
||||
|
||||
## Fix
|
||||
Simply updated the `@Tags` annotation from `Organizations` to `Enterprise`...
|
||||
```
|
||||
|
||||
### For Large Refactors: Lead with Context
|
||||
|
||||
When rewriting significant documentation or code, start with the problems being fixed:
|
||||
|
||||
```markdown
|
||||
This PR rewrites [component] for [reason].
|
||||
|
||||
The previous [component] had [specific issues]: [details].
|
||||
|
||||
[What changed]: [specific improvements made].
|
||||
|
||||
[Additional changes]: [context].
|
||||
|
||||
Refs #[issue-number]
|
||||
```
|
||||
|
||||
**Example (major documentation rewrite):**
|
||||
|
||||
- Started with "This PR rewrites the dev containers documentation for GA readiness"
|
||||
- Listed specific inaccuracies being fixed
|
||||
- Explained organizational changes
|
||||
- Referenced related issue
|
||||
|
||||
## What to Include
|
||||
|
||||
### Always Include
|
||||
|
||||
1. **Link Related Work**
|
||||
- `Closes https://github.com/coder/internal/issues/XXX`
|
||||
- `Depends on #XXX`
|
||||
- `Fixes: https://github.com/coder/aibridge/issues/XX`
|
||||
- `Refs #XXX` (for general reference)
|
||||
|
||||
2. **Performance Context** (when relevant)
|
||||
|
||||
```markdown
|
||||
Each query took ~30ms on average with 80 requests/second to the cluster,
|
||||
resulting in ~5.2 query-seconds every second.
|
||||
```
|
||||
|
||||
3. **Migration Warnings** (when relevant)
|
||||
|
||||
```markdown
|
||||
**NOTE**: This migration creates an index on `workspace_app_statuses`.
|
||||
For deployments with heavy task usage, this may take a moment to complete.
|
||||
```
|
||||
|
||||
4. **Visual Evidence** (for UI changes)
|
||||
|
||||
```markdown
|
||||
<img width="1281" height="425" alt="image" src="..." />
|
||||
```
|
||||
|
||||
### Never Include
|
||||
|
||||
- ❌ **Test plans** - Testing is handled through code review and CI
|
||||
- ❌ **"Benefits" sections** - Benefits should be clear from the description
|
||||
- ❌ **Implementation details** - Keep it high-level
|
||||
- ❌ **Marketing language** - Stay technical and factual
|
||||
- ❌ **Bullet lists of features** (unless it's a large refactor that needs enumeration)
|
||||
|
||||
## Special Patterns
|
||||
|
||||
### Simple Chore PRs
|
||||
|
||||
For straightforward updates (dependency bumps, minor fixes):
|
||||
|
||||
```markdown
|
||||
Changes from [link to upstream PR/issue]
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```markdown
|
||||
Reference:
|
||||
[link explaining why this change is needed]
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Start with the problem, then explain the fix:
|
||||
|
||||
```markdown
|
||||
[What was broken and why it matters]
|
||||
|
||||
[What you changed to fix it]
|
||||
```
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
Dependabot PRs are auto-generated - don't try to match their verbose style for manual updates. Instead use:
|
||||
|
||||
```markdown
|
||||
Changes from https://github.com/upstream/repo/pull/XXX/
|
||||
```
|
||||
|
||||
## Attribution Footer
|
||||
|
||||
For AI-generated PRs, end with:
|
||||
|
||||
```markdown
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
## Creating PRs as Draft
|
||||
|
||||
**IMPORTANT**: Unless explicitly told otherwise, always create PRs as drafts using the `--draft` flag:
|
||||
|
||||
```bash
|
||||
gh pr create --draft --title "..." --body "..."
|
||||
```
|
||||
|
||||
After creating the PR, encourage the user to review it before marking as ready:
|
||||
|
||||
```
|
||||
I've created draft PR #XXXX. Please review the changes and mark it as ready for review when you're satisfied.
|
||||
```
|
||||
|
||||
This allows the user to:
|
||||
- Review the code changes before requesting reviews from maintainers
|
||||
- Make additional adjustments if needed
|
||||
- Ensure CI passes before notifying reviewers
|
||||
- Control when the PR enters the review queue
|
||||
|
||||
Only create non-draft PRs when the user explicitly requests it or when following up on an existing draft.
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **Always create draft PRs** - Unless explicitly told otherwise
|
||||
2. **Be concise** - Default to 1-2 paragraphs unless complexity demands more
|
||||
3. **Be technical** - Explain what and why, not detailed how
|
||||
4. **Link everything** - Issues, PRs, upstream changes, Notion docs
|
||||
5. **Show impact** - Metrics for performance, screenshots for UI, warnings for migrations
|
||||
6. **No test plans** - Code review and CI handle testing
|
||||
7. **No benefits sections** - Benefits should be obvious from the technical description
|
||||
|
||||
## Examples by Category
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
Includes query timing metrics and explains the index solution
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Describes broken behavior then the fix in two sentences
|
||||
|
||||
### Documentation
|
||||
|
||||
- **Major rewrite**: Long form explaining inaccuracies and improvements
|
||||
- **Simple correction**: One sentence for simple correction
|
||||
|
||||
### Features
|
||||
|
||||
Simple statement of what was added and dependencies
|
||||
|
||||
### Refactoring
|
||||
|
||||
Explains why client-side sorting is now redundant
|
||||
|
||||
### Configuration
|
||||
|
||||
Adds guidelines with issue reference
|
||||
@@ -7,5 +7,5 @@ runs:
|
||||
- name: Install Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
with:
|
||||
terraform_version: 1.13.4
|
||||
terraform_version: 1.14.1
|
||||
terraform_wrapper: false
|
||||
|
||||
@@ -6,6 +6,8 @@ updates:
|
||||
interval: "weekly"
|
||||
time: "06:00"
|
||||
timezone: "America/Chicago"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
labels: []
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
@@ -68,8 +70,8 @@ updates:
|
||||
interval: "monthly"
|
||||
time: "06:00"
|
||||
timezone: "America/Chicago"
|
||||
reviewers:
|
||||
- "coder/ts"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
labels: []
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignores": ["PLAN.md"],
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
# Coder Development Guidelines
|
||||
|
||||
You are an experienced, pragmatic software engineer. You don't over-engineer a solution when a simple one is possible.
|
||||
Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permission first. BREAKING THE LETTER OR SPIRIT OF THE RULES IS FAILURE.
|
||||
|
||||
## Foundational rules
|
||||
|
||||
- Doing it right is better than doing it fast. You are not in a rush. NEVER skip steps or take shortcuts.
|
||||
- Tedious, systematic work is often the correct solution. Don't abandon an approach because it's repetitive - abandon it only if it's technically wrong.
|
||||
- Honesty is a core value.
|
||||
|
||||
## Our relationship
|
||||
|
||||
- Act as a critical peer reviewer. Your job is to disagree with me when I'm wrong, not to please me. Prioritize accuracy and reasoning over agreement.
|
||||
- YOU MUST speak up immediately when you don't know something or we're in over our heads
|
||||
- YOU MUST call out bad ideas, unreasonable expectations, and mistakes - I depend on this
|
||||
- NEVER be agreeable just to be nice - I NEED your HONEST technical judgment
|
||||
- NEVER write the phrase "You're absolutely right!" You are not a sycophant. We're working together because I value your opinion. Do not agree with me unless you can justify it with evidence or reasoning.
|
||||
- YOU MUST ALWAYS STOP and ask for clarification rather than making assumptions.
|
||||
- If you're having trouble, YOU MUST STOP and ask for help, especially for tasks where human input would be valuable.
|
||||
- When you disagree with my approach, YOU MUST push back. Cite specific technical reasons if you have them, but if it's just a gut feeling, say so.
|
||||
- If you're uncomfortable pushing back out loud, just say "Houston, we have a problem". I'll know what you mean
|
||||
- We discuss architectutral decisions (framework changes, major refactoring, system design) together before implementation. Routine fixes and clear implementations don't need discussion.
|
||||
|
||||
## Proactiveness
|
||||
|
||||
When asked to do something, just do it - including obvious follow-up actions needed to complete the task properly.
|
||||
Only pause to ask for confirmation when:
|
||||
|
||||
- Multiple valid approaches exist and the choice matters
|
||||
- The action would delete or significantly restructure existing code
|
||||
- You genuinely don't understand what's being asked
|
||||
- Your partner asked a question (answer the question, don't jump to implementation)
|
||||
|
||||
@.claude/docs/WORKFLOWS.md
|
||||
@package.json
|
||||
|
||||
## Essential Commands
|
||||
|
||||
| Task | Command | Notes |
|
||||
|-------------------|--------------------------|----------------------------------|
|
||||
| **Development** | `./scripts/develop.sh` | ⚠️ Don't use manual build |
|
||||
| **Build** | `make build` | Fat binaries (includes server) |
|
||||
| **Build Slim** | `make build-slim` | Slim binaries |
|
||||
| **Test** | `make test` | Full test suite |
|
||||
| **Test Single** | `make test RUN=TestName` | Faster than full suite |
|
||||
| **Test Postgres** | `make test-postgres` | Run tests with Postgres database |
|
||||
| **Test Race** | `make test-race` | Run tests with Go race detector |
|
||||
| **Lint** | `make lint` | Always run after changes |
|
||||
| **Generate** | `make gen` | After database changes |
|
||||
| **Format** | `make fmt` | Auto-format code |
|
||||
| **Clean** | `make clean` | Clean build artifacts |
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
- `pnpm run format-docs` - Format markdown tables in docs
|
||||
- `pnpm run lint-docs` - Lint and fix markdown files
|
||||
- `pnpm run storybook` - Run Storybook (from site directory)
|
||||
|
||||
## Critical Patterns
|
||||
|
||||
### Database Changes (ALWAYS FOLLOW)
|
||||
|
||||
1. Modify `coderd/database/queries/*.sql` files
|
||||
2. Run `make gen`
|
||||
3. If audit errors: update `enterprise/audit/table.go`
|
||||
4. Run `make gen` again
|
||||
|
||||
### LSP Navigation (USE FIRST)
|
||||
|
||||
#### Go LSP (for backend code)
|
||||
|
||||
- **Find definitions**: `mcp__go-language-server__definition symbolName`
|
||||
- **Find references**: `mcp__go-language-server__references symbolName`
|
||||
- **Get type info**: `mcp__go-language-server__hover filePath line column`
|
||||
- **Rename symbol**: `mcp__go-language-server__rename_symbol filePath line column newName`
|
||||
|
||||
#### TypeScript LSP (for frontend code in site/)
|
||||
|
||||
- **Find definitions**: `mcp__typescript-language-server__definition symbolName`
|
||||
- **Find references**: `mcp__typescript-language-server__references symbolName`
|
||||
- **Get type info**: `mcp__typescript-language-server__hover filePath line column`
|
||||
- **Rename symbol**: `mcp__typescript-language-server__rename_symbol filePath line column newName`
|
||||
|
||||
### OAuth2 Error Handling
|
||||
|
||||
```go
|
||||
// OAuth2-compliant error responses
|
||||
writeOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_grant", "description")
|
||||
```
|
||||
|
||||
### Authorization Context
|
||||
|
||||
```go
|
||||
// Public endpoints needing system access
|
||||
app, err := api.Database.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), clientID)
|
||||
|
||||
// Authenticated endpoints with user context
|
||||
app, err := api.Database.GetOAuth2ProviderAppByClientID(ctx, clientID)
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Full workflows available in imported WORKFLOWS.md
|
||||
|
||||
### New Feature Checklist
|
||||
|
||||
- [ ] Run `git pull` to ensure latest code
|
||||
- [ ] Check if feature touches database - you'll need migrations
|
||||
- [ ] Check if feature touches audit logs - update `enterprise/audit/table.go`
|
||||
|
||||
## Architecture
|
||||
|
||||
- **coderd**: Main API service
|
||||
- **provisionerd**: Infrastructure provisioning
|
||||
- **Agents**: Workspace services (SSH, port forwarding)
|
||||
- **Database**: PostgreSQL with `dbauthz` authorization
|
||||
|
||||
## Testing
|
||||
|
||||
### Race Condition Prevention
|
||||
|
||||
- Use unique identifiers: `fmt.Sprintf("test-client-%s-%d", t.Name(), time.Now().UnixNano())`
|
||||
- Never use hardcoded names in concurrent tests
|
||||
|
||||
### OAuth2 Testing
|
||||
|
||||
- Full suite: `./scripts/oauth2/test-mcp-oauth2.sh`
|
||||
- Manual testing: `./scripts/oauth2/test-manual-flow.sh`
|
||||
|
||||
### Timing Issues
|
||||
|
||||
NEVER use `time.Sleep` to mitigate timing issues. If an issue
|
||||
seems like it should use `time.Sleep`, read through https://github.com/coder/quartz and specifically the [README](https://github.com/coder/quartz/blob/main/README.md) to better understand how to handle timing issues.
|
||||
|
||||
## Code Style
|
||||
|
||||
### Detailed guidelines in imported WORKFLOWS.md
|
||||
|
||||
- Follow [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
|
||||
- Commit format: `type(scope): message`
|
||||
|
||||
### Writing Comments
|
||||
|
||||
Code comments should be clear, well-formatted, and add meaningful context.
|
||||
|
||||
**Proper sentence structure**: Comments are sentences and should end with
|
||||
periods or other appropriate punctuation. This improves readability and
|
||||
maintains professional code standards.
|
||||
|
||||
**Explain why, not what**: Good comments explain the reasoning behind code
|
||||
rather than describing what the code does. The code itself should be
|
||||
self-documenting through clear naming and structure. Focus your comments on
|
||||
non-obvious decisions, edge cases, or business logic that isn't immediately
|
||||
apparent from reading the implementation.
|
||||
|
||||
**Line length and wrapping**: Keep comment lines to 80 characters wide
|
||||
(including the comment prefix like `//` or `#`). When a comment spans multiple
|
||||
lines, wrap it naturally at word boundaries rather than writing one sentence
|
||||
per line. This creates more readable, paragraph-like blocks of documentation.
|
||||
|
||||
```go
|
||||
// Good: Explains the rationale with proper sentence structure.
|
||||
// We need a custom timeout here because workspace builds can take several
|
||||
// minutes on slow networks, and the default 30s timeout causes false
|
||||
// failures during initial template imports.
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
|
||||
// Bad: Describes what the code does without punctuation or wrapping
|
||||
// Set a custom timeout
|
||||
// Workspace builds can take a long time
|
||||
// Default timeout is too short
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
```
|
||||
|
||||
### Avoid Unnecessary Changes
|
||||
|
||||
When fixing a bug or adding a feature, don't modify code unrelated to your
|
||||
task. Unnecessary changes make PRs harder to review and can introduce
|
||||
regressions.
|
||||
|
||||
**Don't reword existing comments or code** unless the change is directly
|
||||
motivated by your task. Rewording comments to be shorter or "cleaner" wastes
|
||||
reviewer time and clutters the diff.
|
||||
|
||||
**Don't delete existing comments** that explain non-obvious behavior. These
|
||||
comments preserve important context about why code works a certain way.
|
||||
|
||||
**When adding tests for new behavior**, add new test cases instead of modifying
|
||||
existing ones. This preserves coverage for the original behavior and makes it
|
||||
clear what the new test covers.
|
||||
|
||||
## Detailed Development Guides
|
||||
|
||||
@.claude/docs/ARCHITECTURE.md
|
||||
@.claude/docs/OAUTH2.md
|
||||
@.claude/docs/TESTING.md
|
||||
@.claude/docs/TROUBLESHOOTING.md
|
||||
@.claude/docs/DATABASE.md
|
||||
@.claude/docs/PR_STYLE_GUIDE.md
|
||||
@.claude/docs/DOCS_STYLE_GUIDE.md
|
||||
|
||||
## Local Configuration
|
||||
|
||||
These files may be gitignored, read manually if not auto-loaded.
|
||||
|
||||
@AGENTS.local.md
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Audit table errors** → Update `enterprise/audit/table.go`
|
||||
2. **OAuth2 errors** → Return RFC-compliant format
|
||||
3. **Race conditions** → Use unique test identifiers
|
||||
4. **Missing newlines** → Ensure files end with newline
|
||||
|
||||
---
|
||||
|
||||
*This file stays lean and actionable. Detailed workflows and explanations are imported automatically.*
|
||||
@@ -1,199 +0,0 @@
|
||||
# Coder Development Guidelines
|
||||
|
||||
You are an experienced, pragmatic software engineer. You don't over-engineer a solution when a simple one is possible.
|
||||
Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permission first. BREAKING THE LETTER OR SPIRIT OF THE RULES IS FAILURE.
|
||||
|
||||
## Foundational rules
|
||||
|
||||
- Doing it right is better than doing it fast. You are not in a rush. NEVER skip steps or take shortcuts.
|
||||
- Tedious, systematic work is often the correct solution. Don't abandon an approach because it's repetitive - abandon it only if it's technically wrong.
|
||||
- Honesty is a core value.
|
||||
|
||||
## Our relationship
|
||||
|
||||
- Act as a critical peer reviewer. Your job is to disagree with me when I'm wrong, not to please me. Prioritize accuracy and reasoning over agreement.
|
||||
- YOU MUST speak up immediately when you don't know something or we're in over our heads
|
||||
- YOU MUST call out bad ideas, unreasonable expectations, and mistakes - I depend on this
|
||||
- NEVER be agreeable just to be nice - I NEED your HONEST technical judgment
|
||||
- NEVER write the phrase "You're absolutely right!" You are not a sycophant. We're working together because I value your opinion. Do not agree with me unless you can justify it with evidence or reasoning.
|
||||
- YOU MUST ALWAYS STOP and ask for clarification rather than making assumptions.
|
||||
- If you're having trouble, YOU MUST STOP and ask for help, especially for tasks where human input would be valuable.
|
||||
- When you disagree with my approach, YOU MUST push back. Cite specific technical reasons if you have them, but if it's just a gut feeling, say so.
|
||||
- If you're uncomfortable pushing back out loud, just say "Houston, we have a problem". I'll know what you mean
|
||||
- We discuss architectutral decisions (framework changes, major refactoring, system design) together before implementation. Routine fixes and clear implementations don't need discussion.
|
||||
|
||||
## Proactiveness
|
||||
|
||||
When asked to do something, just do it - including obvious follow-up actions needed to complete the task properly.
|
||||
Only pause to ask for confirmation when:
|
||||
|
||||
- Multiple valid approaches exist and the choice matters
|
||||
- The action would delete or significantly restructure existing code
|
||||
- You genuinely don't understand what's being asked
|
||||
- Your partner asked a question (answer the question, don't jump to implementation)
|
||||
|
||||
@.claude/docs/WORKFLOWS.md
|
||||
@package.json
|
||||
|
||||
## Essential Commands
|
||||
|
||||
| Task | Command | Notes |
|
||||
|-------------------|--------------------------|----------------------------------|
|
||||
| **Development** | `./scripts/develop.sh` | ⚠️ Don't use manual build |
|
||||
| **Build** | `make build` | Fat binaries (includes server) |
|
||||
| **Build Slim** | `make build-slim` | Slim binaries |
|
||||
| **Test** | `make test` | Full test suite |
|
||||
| **Test Single** | `make test RUN=TestName` | Faster than full suite |
|
||||
| **Test Postgres** | `make test-postgres` | Run tests with Postgres database |
|
||||
| **Test Race** | `make test-race` | Run tests with Go race detector |
|
||||
| **Lint** | `make lint` | Always run after changes |
|
||||
| **Generate** | `make gen` | After database changes |
|
||||
| **Format** | `make fmt` | Auto-format code |
|
||||
| **Clean** | `make clean` | Clean build artifacts |
|
||||
|
||||
### Documentation Commands
|
||||
|
||||
- `pnpm run format-docs` - Format markdown tables in docs
|
||||
- `pnpm run lint-docs` - Lint and fix markdown files
|
||||
- `pnpm run storybook` - Run Storybook (from site directory)
|
||||
|
||||
## Critical Patterns
|
||||
|
||||
### Database Changes (ALWAYS FOLLOW)
|
||||
|
||||
1. Modify `coderd/database/queries/*.sql` files
|
||||
2. Run `make gen`
|
||||
3. If audit errors: update `enterprise/audit/table.go`
|
||||
4. Run `make gen` again
|
||||
|
||||
### LSP Navigation (USE FIRST)
|
||||
|
||||
#### Go LSP (for backend code)
|
||||
|
||||
- **Find definitions**: `mcp__go-language-server__definition symbolName`
|
||||
- **Find references**: `mcp__go-language-server__references symbolName`
|
||||
- **Get type info**: `mcp__go-language-server__hover filePath line column`
|
||||
- **Rename symbol**: `mcp__go-language-server__rename_symbol filePath line column newName`
|
||||
|
||||
#### TypeScript LSP (for frontend code in site/)
|
||||
|
||||
- **Find definitions**: `mcp__typescript-language-server__definition symbolName`
|
||||
- **Find references**: `mcp__typescript-language-server__references symbolName`
|
||||
- **Get type info**: `mcp__typescript-language-server__hover filePath line column`
|
||||
- **Rename symbol**: `mcp__typescript-language-server__rename_symbol filePath line column newName`
|
||||
|
||||
### OAuth2 Error Handling
|
||||
|
||||
```go
|
||||
// OAuth2-compliant error responses
|
||||
writeOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_grant", "description")
|
||||
```
|
||||
|
||||
### Authorization Context
|
||||
|
||||
```go
|
||||
// Public endpoints needing system access
|
||||
app, err := api.Database.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), clientID)
|
||||
|
||||
// Authenticated endpoints with user context
|
||||
app, err := api.Database.GetOAuth2ProviderAppByClientID(ctx, clientID)
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Full workflows available in imported WORKFLOWS.md
|
||||
|
||||
### New Feature Checklist
|
||||
|
||||
- [ ] Run `git pull` to ensure latest code
|
||||
- [ ] Check if feature touches database - you'll need migrations
|
||||
- [ ] Check if feature touches audit logs - update `enterprise/audit/table.go`
|
||||
|
||||
## Architecture
|
||||
|
||||
- **coderd**: Main API service
|
||||
- **provisionerd**: Infrastructure provisioning
|
||||
- **Agents**: Workspace services (SSH, port forwarding)
|
||||
- **Database**: PostgreSQL with `dbauthz` authorization
|
||||
|
||||
## Testing
|
||||
|
||||
### Race Condition Prevention
|
||||
|
||||
- Use unique identifiers: `fmt.Sprintf("test-client-%s-%d", t.Name(), time.Now().UnixNano())`
|
||||
- Never use hardcoded names in concurrent tests
|
||||
|
||||
### OAuth2 Testing
|
||||
|
||||
- Full suite: `./scripts/oauth2/test-mcp-oauth2.sh`
|
||||
- Manual testing: `./scripts/oauth2/test-manual-flow.sh`
|
||||
|
||||
### Timing Issues
|
||||
|
||||
NEVER use `time.Sleep` to mitigate timing issues. If an issue
|
||||
seems like it should use `time.Sleep`, read through https://github.com/coder/quartz and specifically the [README](https://github.com/coder/quartz/blob/main/README.md) to better understand how to handle timing issues.
|
||||
|
||||
## Code Style
|
||||
|
||||
### Detailed guidelines in imported WORKFLOWS.md
|
||||
|
||||
- Follow [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
|
||||
- Commit format: `type(scope): message`
|
||||
|
||||
### Writing Comments
|
||||
|
||||
Code comments should be clear, well-formatted, and add meaningful context.
|
||||
|
||||
**Proper sentence structure**: Comments are sentences and should end with
|
||||
periods or other appropriate punctuation. This improves readability and
|
||||
maintains professional code standards.
|
||||
|
||||
**Explain why, not what**: Good comments explain the reasoning behind code
|
||||
rather than describing what the code does. The code itself should be
|
||||
self-documenting through clear naming and structure. Focus your comments on
|
||||
non-obvious decisions, edge cases, or business logic that isn't immediately
|
||||
apparent from reading the implementation.
|
||||
|
||||
**Line length and wrapping**: Keep comment lines to 80 characters wide
|
||||
(including the comment prefix like `//` or `#`). When a comment spans multiple
|
||||
lines, wrap it naturally at word boundaries rather than writing one sentence
|
||||
per line. This creates more readable, paragraph-like blocks of documentation.
|
||||
|
||||
```go
|
||||
// Good: Explains the rationale with proper sentence structure.
|
||||
// We need a custom timeout here because workspace builds can take several
|
||||
// minutes on slow networks, and the default 30s timeout causes false
|
||||
// failures during initial template imports.
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
|
||||
// Bad: Describes what the code does without punctuation or wrapping
|
||||
// Set a custom timeout
|
||||
// Workspace builds can take a long time
|
||||
// Default timeout is too short
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
```
|
||||
|
||||
## Detailed Development Guides
|
||||
|
||||
@.claude/docs/ARCHITECTURE.md
|
||||
@.claude/docs/OAUTH2.md
|
||||
@.claude/docs/TESTING.md
|
||||
@.claude/docs/TROUBLESHOOTING.md
|
||||
@.claude/docs/DATABASE.md
|
||||
|
||||
## Local Configuration
|
||||
|
||||
These files may be gitignored, read manually if not auto-loaded.
|
||||
|
||||
@AGENTS.local.md
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Audit table errors** → Update `enterprise/audit/table.go`
|
||||
2. **OAuth2 errors** → Return RFC-compliant format
|
||||
3. **Race conditions** → Use unique test identifiers
|
||||
4. **Missing newlines** → Ensure files end with newline
|
||||
|
||||
---
|
||||
|
||||
*This file stays lean and actionable. Detailed workflows and explanations are imported automatically.*
|
||||
+11
-6
@@ -2189,14 +2189,19 @@ func (a *apiConnRoutineManager) startTailnetAPI(
|
||||
a.eg.Go(func() error {
|
||||
logger.Debug(ctx, "starting tailnet routine")
|
||||
err := f(ctx, a.tAPI)
|
||||
if xerrors.Is(err, context.Canceled) && ctx.Err() != nil {
|
||||
logger.Debug(ctx, "swallowing context canceled")
|
||||
if (xerrors.Is(err, context.Canceled) ||
|
||||
xerrors.Is(err, io.EOF)) &&
|
||||
ctx.Err() != nil {
|
||||
logger.Debug(ctx, "swallowing error because context is canceled", slog.Error(err))
|
||||
// Don't propagate context canceled errors to the error group, because we don't want the
|
||||
// graceful context being canceled to halt the work of routines with
|
||||
// gracefulShutdownBehaviorRemain. Note that we check both that the error is
|
||||
// context.Canceled and that *our* context is currently canceled, because when Coderd
|
||||
// unilaterally closes the API connection (for example if the build is outdated), it can
|
||||
// sometimes show up as context.Canceled in our RPC calls.
|
||||
// gracefulShutdownBehaviorRemain. Unfortunately, the dRPC library closes the stream
|
||||
// when context is canceled on an RPC, so canceling the context can also show up as
|
||||
// io.EOF. Also, when Coderd unilaterally closes the API connection (for example if the
|
||||
// build is outdated), it can sometimes show up as context.Canceled in our RPC calls.
|
||||
// We can't reliably distinguish between a context cancelation and a legit EOF, so we
|
||||
// also check that *our* context is currently canceled. If it is, we can safely ignore
|
||||
// the error.
|
||||
return nil
|
||||
}
|
||||
logger.Debug(ctx, "routine exited", slog.Error(err))
|
||||
|
||||
+1
-1
@@ -465,7 +465,7 @@ func TestAgent_SessionTTYShell(t *testing.T) {
|
||||
for _, port := range sshPorts {
|
||||
t.Run(fmt.Sprintf("(%d)", port), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
session := setupSSHSessionOnPort(t, agentsdk.Manifest{}, codersdk.ServiceBannerConfig{}, nil, port)
|
||||
command := "sh"
|
||||
|
||||
@@ -1457,6 +1457,8 @@ func (api *API) markDevcontainerDirty(configPath string, modifiedAt time.Time) {
|
||||
|
||||
api.knownDevcontainers[dc.WorkspaceFolder] = dc
|
||||
}
|
||||
|
||||
api.broadcastUpdatesLocked()
|
||||
}
|
||||
|
||||
// cleanupSubAgents removes subagents that are no longer managed by
|
||||
|
||||
@@ -1641,6 +1641,77 @@ func TestAPI(t *testing.T) {
|
||||
require.NotNil(t, response.Devcontainers[0].Container, "container should not be nil")
|
||||
})
|
||||
|
||||
// Verify that modifying a config file broadcasts the dirty status
|
||||
// over websocket immediately.
|
||||
t.Run("FileWatcherDirtyBroadcast", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
configPath := "/workspace/project/.devcontainer/devcontainer.json"
|
||||
fWatcher := newFakeWatcher(t)
|
||||
fLister := &fakeContainerCLI{
|
||||
containers: codersdk.WorkspaceAgentListContainersResponse{
|
||||
Containers: []codersdk.WorkspaceAgentContainer{
|
||||
{
|
||||
ID: "container-id",
|
||||
FriendlyName: "container-name",
|
||||
Running: true,
|
||||
Labels: map[string]string{
|
||||
agentcontainers.DevcontainerLocalFolderLabel: "/workspace/project",
|
||||
agentcontainers.DevcontainerConfigFileLabel: configPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mClock := quartz.NewMock(t)
|
||||
tickerTrap := mClock.Trap().TickerFunc("updaterLoop")
|
||||
|
||||
api := agentcontainers.NewAPI(
|
||||
slogtest.Make(t, nil).Leveled(slog.LevelDebug),
|
||||
agentcontainers.WithContainerCLI(fLister),
|
||||
agentcontainers.WithWatcher(fWatcher),
|
||||
agentcontainers.WithClock(mClock),
|
||||
)
|
||||
api.Start()
|
||||
defer api.Close()
|
||||
|
||||
srv := httptest.NewServer(api.Routes())
|
||||
defer srv.Close()
|
||||
|
||||
tickerTrap.MustWait(ctx).MustRelease(ctx)
|
||||
tickerTrap.Close()
|
||||
|
||||
wsConn, resp, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(srv.URL, "http")+"/watch", nil)
|
||||
require.NoError(t, err)
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
defer wsConn.Close(websocket.StatusNormalClosure, "")
|
||||
|
||||
// Read and discard initial state.
|
||||
_, _, err = wsConn.Read(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
fWatcher.waitNext(ctx)
|
||||
fWatcher.sendEventWaitNextCalled(ctx, fsnotify.Event{
|
||||
Name: configPath,
|
||||
Op: fsnotify.Write,
|
||||
})
|
||||
|
||||
// Verify dirty status is broadcast without advancing the clock.
|
||||
_, msg, err := wsConn.Read(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
var response codersdk.WorkspaceAgentListContainersResponse
|
||||
err = json.Unmarshal(msg, &response)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, response.Devcontainers, 1)
|
||||
assert.True(t, response.Devcontainers[0].Dirty,
|
||||
"devcontainer should be marked as dirty after config file modification")
|
||||
})
|
||||
|
||||
t.Run("SubAgentLifecycle", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+69
-12
@@ -17,12 +17,12 @@
|
||||
"useSemanticElements": "off",
|
||||
"noStaticElementInteractions": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedImports": "warn",
|
||||
"correctness": {
|
||||
"noUnusedImports": "warn",
|
||||
"useUniqueElementIds": "off", // TODO: This is new but we want to fix it
|
||||
"noNestedComponentDefinitions": "off", // TODO: Investigate, since it is used by shadcn components
|
||||
"noUnusedVariables": {
|
||||
"level": "warn",
|
||||
"noUnusedVariables": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
@@ -40,19 +40,76 @@
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error",
|
||||
"noRestrictedImports": {
|
||||
"level": "error",
|
||||
"noRestrictedImports": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"paths": {
|
||||
"@mui/material": "Use @mui/material/<name> instead. See: https://material-ui.com/guides/minimizing-bundle-size/.",
|
||||
// "@mui/material/Alert": "Use components/Alert/Alert instead.",
|
||||
// "@mui/material/AlertTitle": "Use components/Alert/Alert instead.",
|
||||
// "@mui/material/Autocomplete": "Use shadcn/ui Combobox instead.",
|
||||
"@mui/material/Avatar": "Use components/Avatar/Avatar instead.",
|
||||
"@mui/material/Alert": "Use components/Alert/Alert instead.",
|
||||
"@mui/material/Box": "Use a <div> with Tailwind classes instead.",
|
||||
"@mui/material/Button": "Use components/Button/Button instead.",
|
||||
// "@mui/material/Card": "Use shadcn/ui Card component instead.",
|
||||
// "@mui/material/CardActionArea": "Use shadcn/ui Card component instead.",
|
||||
// "@mui/material/CardContent": "Use shadcn/ui Card component instead.",
|
||||
// "@mui/material/Checkbox": "Use shadcn/ui Checkbox component instead.",
|
||||
// "@mui/material/Chip": "Use components/Badge or Tailwind styles instead.",
|
||||
// "@mui/material/CircularProgress": "Use components/Spinner/Spinner instead.",
|
||||
// "@mui/material/Collapse": "Use shadcn/ui Collapsible instead.",
|
||||
// "@mui/material/CssBaseline": "Use Tailwind CSS base styles instead.",
|
||||
// "@mui/material/Dialog": "Use shadcn/ui Dialog component instead.",
|
||||
// "@mui/material/DialogActions": "Use shadcn/ui Dialog component instead.",
|
||||
// "@mui/material/DialogContent": "Use shadcn/ui Dialog component instead.",
|
||||
// "@mui/material/DialogContentText": "Use shadcn/ui Dialog component instead.",
|
||||
// "@mui/material/DialogTitle": "Use shadcn/ui Dialog component instead.",
|
||||
// "@mui/material/Divider": "Use shadcn/ui Separator or <hr> with Tailwind instead.",
|
||||
// "@mui/material/Drawer": "Use shadcn/ui Sheet component instead.",
|
||||
// "@mui/material/FormControl": "Use native form elements with Tailwind instead.",
|
||||
// "@mui/material/FormControlLabel": "Use shadcn/ui Label with form components instead.",
|
||||
// "@mui/material/FormGroup": "Use a <div> with Tailwind classes instead.",
|
||||
// "@mui/material/FormHelperText": "Use a <p> with Tailwind classes instead.",
|
||||
// "@mui/material/FormLabel": "Use shadcn/ui Label component instead.",
|
||||
// "@mui/material/Grid": "Use Tailwind grid utilities instead.",
|
||||
// "@mui/material/IconButton": "Use components/Button/Button with variant='icon' instead.",
|
||||
// "@mui/material/InputAdornment": "Use Tailwind positioning in input wrapper instead.",
|
||||
// "@mui/material/InputBase": "Use shadcn/ui Input component instead.",
|
||||
// "@mui/material/LinearProgress": "Use a progress bar with Tailwind instead.",
|
||||
// "@mui/material/Link": "Use React Router Link or native <a> tags instead.",
|
||||
// "@mui/material/List": "Use native <ul> with Tailwind instead.",
|
||||
// "@mui/material/ListItem": "Use native <li> with Tailwind instead.",
|
||||
// "@mui/material/ListItemIcon": "Use lucide-react icons in list items instead.",
|
||||
// "@mui/material/ListItemText": "Use native elements with Tailwind instead.",
|
||||
// "@mui/material/Menu": "Use shadcn/ui DropdownMenu instead.",
|
||||
// "@mui/material/MenuItem": "Use shadcn/ui DropdownMenu components instead.",
|
||||
// "@mui/material/MenuList": "Use shadcn/ui DropdownMenu components instead.",
|
||||
// "@mui/material/Paper": "Use a <div> with Tailwind shadow/border classes instead.",
|
||||
"@mui/material/Popover": "Use components/Popover/Popover instead.",
|
||||
// "@mui/material/Radio": "Use shadcn/ui RadioGroup instead.",
|
||||
// "@mui/material/RadioGroup": "Use shadcn/ui RadioGroup instead.",
|
||||
// "@mui/material/Select": "Use shadcn/ui Select component instead.",
|
||||
// "@mui/material/Skeleton": "Use shadcn/ui Skeleton component instead.",
|
||||
// "@mui/material/Snackbar": "Use components/GlobalSnackbar instead.",
|
||||
// "@mui/material/Stack": "Use Tailwind flex utilities instead (e.g., <div className='flex flex-col gap-4'>).",
|
||||
// "@mui/material/styles": "Use Tailwind CSS instead.",
|
||||
// "@mui/material/SvgIcon": "Use lucide-react icons instead.",
|
||||
// "@mui/material/Switch": "Use shadcn/ui Switch component instead.",
|
||||
"@mui/material/Table": "Import from components/Table/Table instead.",
|
||||
// "@mui/material/TableRow": "Import from components/Table/Table instead.",
|
||||
// "@mui/material/TextField": "Use shadcn/ui Input component instead.",
|
||||
// "@mui/material/ToggleButton": "Use shadcn/ui Toggle or custom component instead.",
|
||||
// "@mui/material/ToggleButtonGroup": "Use shadcn/ui Toggle or custom component instead.",
|
||||
// "@mui/material/Tooltip": "Use shadcn/ui Tooltip component instead.",
|
||||
"@mui/material/Typography": "Use native HTML elements instead. Eg: <span>, <p>, <h1>, etc.",
|
||||
"@mui/material/Box": "Use a <div> instead.",
|
||||
"@mui/material/Button": "Use a components/Button/Button instead.",
|
||||
"@mui/material/styles": "Import from @emotion/react instead.",
|
||||
"@mui/material/Table*": "Import from components/Table/Table instead.",
|
||||
// "@mui/material/useMediaQuery": "Use Tailwind responsive classes or custom hook instead.",
|
||||
// "@mui/system": "Use Tailwind CSS instead.",
|
||||
// "@mui/utils": "Use native alternatives or utility libraries instead.",
|
||||
// "@mui/x-tree-view": "Use a Tailwind-compatible alternative.",
|
||||
// "@emotion/css": "Use Tailwind CSS instead.",
|
||||
// "@emotion/react": "Use Tailwind CSS instead.",
|
||||
"@emotion/styled": "Use Tailwind CSS instead.",
|
||||
// "@emotion/cache": "Use Tailwind CSS instead.",
|
||||
// "components/Stack/Stack": "Use Tailwind flex utilities instead (e.g., <div className='flex flex-col gap-4'>).",
|
||||
"lodash": "Use lodash/<name> instead."
|
||||
}
|
||||
}
|
||||
|
||||
+254
-167
@@ -20,6 +20,12 @@ import (
|
||||
|
||||
var errAgentShuttingDown = xerrors.New("agent is shutting down")
|
||||
|
||||
// fetchAgentResult is used to pass agent fetch results through channels.
|
||||
type fetchAgentResult struct {
|
||||
agent codersdk.WorkspaceAgent
|
||||
err error
|
||||
}
|
||||
|
||||
type AgentOptions struct {
|
||||
FetchInterval time.Duration
|
||||
Fetch func(ctx context.Context, agentID uuid.UUID) (codersdk.WorkspaceAgent, error)
|
||||
@@ -28,6 +34,14 @@ type AgentOptions struct {
|
||||
DocsURL string
|
||||
}
|
||||
|
||||
// agentWaiter encapsulates the state machine for waiting on a workspace agent.
|
||||
type agentWaiter struct {
|
||||
opts AgentOptions
|
||||
sw *stageWriter
|
||||
logSources map[uuid.UUID]codersdk.WorkspaceAgentLogSource
|
||||
fetchAgent func(context.Context) (codersdk.WorkspaceAgent, error)
|
||||
}
|
||||
|
||||
// Agent displays a spinning indicator that waits for a workspace agent to connect.
|
||||
func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentOptions) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
@@ -44,11 +58,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
|
||||
}
|
||||
}
|
||||
|
||||
type fetchAgent struct {
|
||||
agent codersdk.WorkspaceAgent
|
||||
err error
|
||||
}
|
||||
fetchedAgent := make(chan fetchAgent, 1)
|
||||
fetchedAgent := make(chan fetchAgentResult, 1)
|
||||
go func() {
|
||||
t := time.NewTimer(0)
|
||||
defer t.Stop()
|
||||
@@ -67,10 +77,10 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
|
||||
default:
|
||||
}
|
||||
if err != nil {
|
||||
fetchedAgent <- fetchAgent{err: xerrors.Errorf("fetch workspace agent: %w", err)}
|
||||
fetchedAgent <- fetchAgentResult{err: xerrors.Errorf("fetch workspace agent: %w", err)}
|
||||
return
|
||||
}
|
||||
fetchedAgent <- fetchAgent{agent: agent}
|
||||
fetchedAgent <- fetchAgentResult{agent: agent}
|
||||
|
||||
// Adjust the interval based on how long we've been waiting.
|
||||
elapsed := time.Since(startTime)
|
||||
@@ -79,7 +89,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
|
||||
}
|
||||
}
|
||||
}()
|
||||
fetch := func() (codersdk.WorkspaceAgent, error) {
|
||||
fetch := func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return codersdk.WorkspaceAgent{}, ctx.Err()
|
||||
@@ -91,7 +101,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
|
||||
}
|
||||
}
|
||||
|
||||
agent, err := fetch()
|
||||
agent, err := fetch(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
@@ -100,9 +110,23 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
|
||||
logSources[source.ID] = source
|
||||
}
|
||||
|
||||
sw := &stageWriter{w: writer}
|
||||
w := &agentWaiter{
|
||||
opts: opts,
|
||||
sw: &stageWriter{w: writer},
|
||||
logSources: logSources,
|
||||
fetchAgent: fetch,
|
||||
}
|
||||
|
||||
return w.wait(ctx, agent, fetchedAgent)
|
||||
}
|
||||
|
||||
// wait runs the main state machine loop.
|
||||
func (aw *agentWaiter) wait(ctx context.Context, agent codersdk.WorkspaceAgent, fetchedAgent chan fetchAgentResult) error {
|
||||
var err error
|
||||
// Track whether we've gone through a wait state, which determines if we
|
||||
// should show startup logs when connected.
|
||||
waitedForConnection := false
|
||||
|
||||
showStartupLogs := false
|
||||
for {
|
||||
// It doesn't matter if we're connected or not, if the agent is
|
||||
// shutting down, we don't know if it's coming back.
|
||||
@@ -112,173 +136,236 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
|
||||
|
||||
switch agent.Status {
|
||||
case codersdk.WorkspaceAgentConnecting, codersdk.WorkspaceAgentTimeout:
|
||||
// Since we were waiting for the agent to connect, also show
|
||||
// startup logs if applicable.
|
||||
showStartupLogs = true
|
||||
|
||||
stage := "Waiting for the workspace agent to connect"
|
||||
sw.Start(stage)
|
||||
for agent.Status == codersdk.WorkspaceAgentConnecting {
|
||||
if agent, err = fetch(); err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if agent.Status == codersdk.WorkspaceAgentTimeout {
|
||||
now := time.Now()
|
||||
sw.Log(now, codersdk.LogLevelInfo, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
|
||||
sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", opts.DocsURL)))
|
||||
for agent.Status == codersdk.WorkspaceAgentTimeout {
|
||||
if agent, err = fetch(); err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
sw.Complete(stage, agent.FirstConnectedAt.Sub(agent.CreatedAt))
|
||||
|
||||
case codersdk.WorkspaceAgentConnected:
|
||||
if !showStartupLogs && agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady {
|
||||
// The workspace is ready, there's nothing to do but connect.
|
||||
return nil
|
||||
}
|
||||
|
||||
stage := "Running workspace agent startup scripts"
|
||||
follow := opts.Wait && agent.LifecycleState.Starting()
|
||||
if !follow {
|
||||
stage += " (non-blocking)"
|
||||
}
|
||||
sw.Start(stage)
|
||||
if follow {
|
||||
sw.Log(time.Time{}, codersdk.LogLevelInfo, "==> ℹ︎ To connect immediately, reconnect with --wait=no or CODER_SSH_WAIT=no, see --help for more information.")
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
// In non-blocking mode, skip streaming logs.
|
||||
// See: https://github.com/coder/coder/issues/13580
|
||||
if !opts.Wait {
|
||||
return nil
|
||||
}
|
||||
|
||||
logStream, logsCloser, err := opts.FetchLogs(ctx, agent.ID, 0, follow)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch workspace agent startup logs: %w", err)
|
||||
}
|
||||
defer logsCloser.Close()
|
||||
|
||||
var lastLog codersdk.WorkspaceAgentLog
|
||||
fetchedAgentWhileFollowing := fetchedAgent
|
||||
if !follow {
|
||||
fetchedAgentWhileFollowing = nil
|
||||
}
|
||||
for {
|
||||
// This select is essentially and inline `fetch()`.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case f := <-fetchedAgentWhileFollowing:
|
||||
if f.err != nil {
|
||||
return xerrors.Errorf("fetch: %w", f.err)
|
||||
}
|
||||
agent = f.agent
|
||||
|
||||
// If the agent is no longer starting, stop following
|
||||
// logs because FetchLogs will keep streaming forever.
|
||||
// We do one last non-follow request to ensure we have
|
||||
// fetched all logs.
|
||||
if !agent.LifecycleState.Starting() {
|
||||
_ = logsCloser.Close()
|
||||
fetchedAgentWhileFollowing = nil
|
||||
|
||||
logStream, logsCloser, err = opts.FetchLogs(ctx, agent.ID, lastLog.ID, false)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch workspace agent startup logs: %w", err)
|
||||
}
|
||||
// Logs are already primed, so we can call close.
|
||||
_ = logsCloser.Close()
|
||||
}
|
||||
case logs, ok := <-logStream:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, log := range logs {
|
||||
source, hasSource := logSources[log.SourceID]
|
||||
output := log.Output
|
||||
if hasSource && source.DisplayName != "" {
|
||||
output = source.DisplayName + ": " + output
|
||||
}
|
||||
sw.Log(log.CreatedAt, log.Level, output)
|
||||
lastLog = log
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
agent, err = aw.waitForConnection(ctx, agent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Since we were waiting for the agent to connect, also show
|
||||
// startup logs if applicable.
|
||||
waitedForConnection = true
|
||||
|
||||
for follow && agent.LifecycleState.Starting() {
|
||||
if agent, err = fetch(); err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
switch agent.LifecycleState {
|
||||
case codersdk.WorkspaceAgentLifecycleReady:
|
||||
sw.Complete(stage, safeDuration(sw, agent.ReadyAt, agent.StartedAt))
|
||||
case codersdk.WorkspaceAgentLifecycleStartTimeout:
|
||||
// Backwards compatibility: Avoid printing warning if
|
||||
// coderd is old and doesn't set ReadyAt for timeouts.
|
||||
if agent.ReadyAt == nil {
|
||||
sw.Fail(stage, 0)
|
||||
} else {
|
||||
sw.Fail(stage, safeDuration(sw, agent.ReadyAt, agent.StartedAt))
|
||||
}
|
||||
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script timed out and your workspace may be incomplete.")
|
||||
case codersdk.WorkspaceAgentLifecycleStartError:
|
||||
sw.Fail(stage, safeDuration(sw, agent.ReadyAt, agent.StartedAt))
|
||||
// Use zero time (omitted) to separate these from the startup logs.
|
||||
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script exited with an error and your workspace may be incomplete.")
|
||||
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#startup-script-exited-with-an-error", opts.DocsURL)))
|
||||
default:
|
||||
switch {
|
||||
case agent.LifecycleState.Starting():
|
||||
// Use zero time (omitted) to separate these from the startup logs.
|
||||
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup scripts are still running and your workspace may be incomplete.")
|
||||
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#your-workspace-may-be-incomplete", opts.DocsURL)))
|
||||
// Note: We don't complete or fail the stage here, it's
|
||||
// intentionally left open to indicate this stage didn't
|
||||
// complete.
|
||||
case agent.LifecycleState.ShuttingDown():
|
||||
// We no longer know if the startup script failed or not,
|
||||
// but we need to tell the user something.
|
||||
sw.Complete(stage, safeDuration(sw, agent.ReadyAt, agent.StartedAt))
|
||||
return errAgentShuttingDown
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
case codersdk.WorkspaceAgentConnected:
|
||||
return aw.handleConnected(ctx, agent, waitedForConnection, fetchedAgent)
|
||||
|
||||
case codersdk.WorkspaceAgentDisconnected:
|
||||
// If the agent was still starting during disconnect, we'll
|
||||
// show startup logs.
|
||||
showStartupLogs = agent.LifecycleState.Starting()
|
||||
|
||||
stage := "The workspace agent lost connection"
|
||||
sw.Start(stage)
|
||||
sw.Log(time.Now(), codersdk.LogLevelWarn, "Wait for it to reconnect or restart your workspace.")
|
||||
sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", opts.DocsURL)))
|
||||
|
||||
disconnectedAt := agent.DisconnectedAt
|
||||
for agent.Status == codersdk.WorkspaceAgentDisconnected {
|
||||
if agent, err = fetch(); err != nil {
|
||||
return xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
agent, waitedForConnection, err = aw.waitForReconnection(ctx, agent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sw.Complete(stage, safeDuration(sw, agent.LastConnectedAt, disconnectedAt))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// waitForConnection handles the Connecting/Timeout states.
|
||||
// Returns when agent transitions to Connected or Disconnected.
|
||||
func (aw *agentWaiter) waitForConnection(ctx context.Context, agent codersdk.WorkspaceAgent) (codersdk.WorkspaceAgent, error) {
|
||||
stage := "Waiting for the workspace agent to connect"
|
||||
aw.sw.Start(stage)
|
||||
|
||||
agent, err := aw.pollWhile(ctx, agent, func(agent codersdk.WorkspaceAgent) bool {
|
||||
return agent.Status == codersdk.WorkspaceAgentConnecting
|
||||
})
|
||||
if err != nil {
|
||||
return agent, err
|
||||
}
|
||||
|
||||
if agent.Status == codersdk.WorkspaceAgentTimeout {
|
||||
now := time.Now()
|
||||
aw.sw.Log(now, codersdk.LogLevelInfo, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
|
||||
aw.sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", aw.opts.DocsURL)))
|
||||
agent, err = aw.pollWhile(ctx, agent, func(agent codersdk.WorkspaceAgent) bool {
|
||||
return agent.Status == codersdk.WorkspaceAgentTimeout
|
||||
})
|
||||
if err != nil {
|
||||
return agent, err
|
||||
}
|
||||
}
|
||||
|
||||
aw.sw.Complete(stage, agent.FirstConnectedAt.Sub(agent.CreatedAt))
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
// handleConnected handles the Connected state and startup script logic.
|
||||
// This is a terminal state, returns nil on success or error on failure.
|
||||
//
|
||||
//nolint:revive // Control flag is acceptable for internal method.
|
||||
func (aw *agentWaiter) handleConnected(ctx context.Context, agent codersdk.WorkspaceAgent, showStartupLogs bool, fetchedAgent chan fetchAgentResult) error {
|
||||
if !showStartupLogs && agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady {
|
||||
// The workspace is ready, there's nothing to do but connect.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determine if we should follow/stream logs (blocking mode).
|
||||
follow := aw.opts.Wait && agent.LifecycleState.Starting()
|
||||
|
||||
stage := "Running workspace agent startup scripts"
|
||||
if !follow {
|
||||
stage += " (non-blocking)"
|
||||
}
|
||||
aw.sw.Start(stage)
|
||||
|
||||
if follow {
|
||||
aw.sw.Log(time.Time{}, codersdk.LogLevelInfo, "==> ℹ︎ To connect immediately, reconnect with --wait=no or CODER_SSH_WAIT=no, see --help for more information.")
|
||||
}
|
||||
|
||||
// In non-blocking mode (Wait=false), we don't stream logs. This prevents
|
||||
// dumping a wall of logs on users who explicitly pass --wait=no. The stage
|
||||
// indicator is still shown, just not the log content. See issue #13580.
|
||||
if aw.opts.Wait {
|
||||
var err error
|
||||
agent, err = aw.streamLogs(ctx, agent, follow, fetchedAgent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we were following, wait until startup completes.
|
||||
if follow {
|
||||
agent, err = aw.pollWhile(ctx, agent, func(agent codersdk.WorkspaceAgent) bool {
|
||||
return agent.LifecycleState.Starting()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle final lifecycle state.
|
||||
switch agent.LifecycleState {
|
||||
case codersdk.WorkspaceAgentLifecycleReady:
|
||||
aw.sw.Complete(stage, safeDuration(aw.sw, agent.ReadyAt, agent.StartedAt))
|
||||
case codersdk.WorkspaceAgentLifecycleStartTimeout:
|
||||
// Backwards compatibility: Avoid printing warning if
|
||||
// coderd is old and doesn't set ReadyAt for timeouts.
|
||||
if agent.ReadyAt == nil {
|
||||
aw.sw.Fail(stage, 0)
|
||||
} else {
|
||||
aw.sw.Fail(stage, safeDuration(aw.sw, agent.ReadyAt, agent.StartedAt))
|
||||
}
|
||||
aw.sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script timed out and your workspace may be incomplete.")
|
||||
case codersdk.WorkspaceAgentLifecycleStartError:
|
||||
aw.sw.Fail(stage, safeDuration(aw.sw, agent.ReadyAt, agent.StartedAt))
|
||||
aw.sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script exited with an error and your workspace may be incomplete.")
|
||||
aw.sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#startup-script-exited-with-an-error", aw.opts.DocsURL)))
|
||||
default:
|
||||
switch {
|
||||
case agent.LifecycleState.Starting():
|
||||
aw.sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup scripts are still running and your workspace may be incomplete.")
|
||||
aw.sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#your-workspace-may-be-incomplete", aw.opts.DocsURL)))
|
||||
// Note: We don't complete or fail the stage here, it's
|
||||
// intentionally left open to indicate this stage didn't
|
||||
// complete.
|
||||
case agent.LifecycleState.ShuttingDown():
|
||||
// We no longer know if the startup script failed or not,
|
||||
// but we need to tell the user something.
|
||||
aw.sw.Complete(stage, safeDuration(aw.sw, agent.ReadyAt, agent.StartedAt))
|
||||
return errAgentShuttingDown
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// streamLogs handles streaming or fetching startup logs.
|
||||
//
|
||||
//nolint:revive // Control flag is acceptable for internal method.
|
||||
func (aw *agentWaiter) streamLogs(ctx context.Context, agent codersdk.WorkspaceAgent, follow bool, fetchedAgent chan fetchAgentResult) (codersdk.WorkspaceAgent, error) {
|
||||
logStream, logsCloser, err := aw.opts.FetchLogs(ctx, agent.ID, 0, follow)
|
||||
if err != nil {
|
||||
return agent, xerrors.Errorf("fetch workspace agent startup logs: %w", err)
|
||||
}
|
||||
defer logsCloser.Close()
|
||||
|
||||
var lastLog codersdk.WorkspaceAgentLog
|
||||
|
||||
// If not following, we don't need to watch for agent state changes.
|
||||
var fetchedAgentWhileFollowing chan fetchAgentResult
|
||||
if follow {
|
||||
fetchedAgentWhileFollowing = fetchedAgent
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return agent, ctx.Err()
|
||||
case f := <-fetchedAgentWhileFollowing:
|
||||
if f.err != nil {
|
||||
return agent, xerrors.Errorf("fetch: %w", f.err)
|
||||
}
|
||||
agent = f.agent
|
||||
|
||||
// If the agent is no longer starting, stop following
|
||||
// logs because FetchLogs will keep streaming forever.
|
||||
// We do one last non-follow request to ensure we have
|
||||
// fetched all logs.
|
||||
if !agent.LifecycleState.Starting() {
|
||||
_ = logsCloser.Close()
|
||||
fetchedAgentWhileFollowing = nil
|
||||
|
||||
logStream, logsCloser, err = aw.opts.FetchLogs(ctx, agent.ID, lastLog.ID, false)
|
||||
if err != nil {
|
||||
return agent, xerrors.Errorf("fetch workspace agent startup logs: %w", err)
|
||||
}
|
||||
// Logs are already primed, so we can call close.
|
||||
_ = logsCloser.Close()
|
||||
}
|
||||
case logs, ok := <-logStream:
|
||||
if !ok {
|
||||
return agent, nil
|
||||
}
|
||||
for _, log := range logs {
|
||||
source, hasSource := aw.logSources[log.SourceID]
|
||||
output := log.Output
|
||||
if hasSource && source.DisplayName != "" {
|
||||
output = source.DisplayName + ": " + output
|
||||
}
|
||||
aw.sw.Log(log.CreatedAt, log.Level, output)
|
||||
lastLog = log
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// waitForReconnection handles the Disconnected state.
|
||||
// Returns when agent reconnects along with whether to show startup logs.
|
||||
func (aw *agentWaiter) waitForReconnection(ctx context.Context, agent codersdk.WorkspaceAgent) (codersdk.WorkspaceAgent, bool, error) {
|
||||
// If the agent was still starting during disconnect, we'll
|
||||
// show startup logs.
|
||||
showStartupLogs := agent.LifecycleState.Starting()
|
||||
|
||||
stage := "The workspace agent lost connection"
|
||||
aw.sw.Start(stage)
|
||||
aw.sw.Log(time.Now(), codersdk.LogLevelWarn, "Wait for it to reconnect or restart your workspace.")
|
||||
aw.sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, fmt.Sprintf("%s/admin/templates/troubleshooting#agent-connection-issues", aw.opts.DocsURL)))
|
||||
|
||||
disconnectedAt := agent.DisconnectedAt
|
||||
agent, err := aw.pollWhile(ctx, agent, func(agent codersdk.WorkspaceAgent) bool {
|
||||
return agent.Status == codersdk.WorkspaceAgentDisconnected
|
||||
})
|
||||
if err != nil {
|
||||
return agent, showStartupLogs, err
|
||||
}
|
||||
aw.sw.Complete(stage, safeDuration(aw.sw, agent.LastConnectedAt, disconnectedAt))
|
||||
|
||||
return agent, showStartupLogs, nil
|
||||
}
|
||||
|
||||
// pollWhile polls the agent while the condition is true. It fetches the agent
|
||||
// on each iteration and returns the updated agent when the condition is false,
|
||||
// the context is canceled, or an error occurs.
|
||||
func (aw *agentWaiter) pollWhile(ctx context.Context, agent codersdk.WorkspaceAgent, cond func(agent codersdk.WorkspaceAgent) bool) (codersdk.WorkspaceAgent, error) {
|
||||
var err error
|
||||
for cond(agent) {
|
||||
agent, err = aw.fetchAgent(ctx)
|
||||
if err != nil {
|
||||
return agent, xerrors.Errorf("fetch: %w", err)
|
||||
}
|
||||
}
|
||||
if err = ctx.Err(); err != nil {
|
||||
return agent, err
|
||||
}
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func troubleshootingMessage(agent codersdk.WorkspaceAgent, url string) string {
|
||||
m := "For more information and troubleshooting, see " + url
|
||||
if agent.TroubleshootingURL != "" {
|
||||
|
||||
@@ -268,6 +268,87 @@ func TestAgent(t *testing.T) {
|
||||
"For more information and troubleshooting, see",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Verify that in non-blocking mode (Wait=false), startup script
|
||||
// logs are suppressed. This prevents dumping a wall of logs on
|
||||
// users who explicitly pass --wait=no. See issue #13580.
|
||||
name: "No logs in non-blocking mode",
|
||||
opts: cliui.AgentOptions{
|
||||
FetchInterval: time.Millisecond,
|
||||
Wait: false,
|
||||
},
|
||||
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
|
||||
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
|
||||
agent.Status = codersdk.WorkspaceAgentConnected
|
||||
agent.FirstConnectedAt = ptr.Ref(time.Now())
|
||||
agent.StartedAt = ptr.Ref(time.Now())
|
||||
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartError
|
||||
agent.ReadyAt = ptr.Ref(time.Now())
|
||||
// These logs should NOT be shown in non-blocking mode.
|
||||
logs <- []codersdk.WorkspaceAgentLog{
|
||||
{
|
||||
CreatedAt: time.Now(),
|
||||
Output: "Startup script log 1",
|
||||
},
|
||||
{
|
||||
CreatedAt: time.Now(),
|
||||
Output: "Startup script log 2",
|
||||
},
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// Note: Log content like "Startup script log 1" should NOT appear here.
|
||||
want: []string{
|
||||
"⧗ Running workspace agent startup scripts (non-blocking)",
|
||||
"✘ Running workspace agent startup scripts (non-blocking)",
|
||||
"Warning: A startup script exited with an error and your workspace may be incomplete.",
|
||||
"For more information and troubleshooting, see",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Verify that even after waiting for the agent to connect, logs
|
||||
// are still suppressed in non-blocking mode. See issue #13580.
|
||||
name: "No logs after connection wait in non-blocking mode",
|
||||
opts: cliui.AgentOptions{
|
||||
FetchInterval: time.Millisecond,
|
||||
Wait: false,
|
||||
},
|
||||
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
|
||||
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
|
||||
agent.Status = codersdk.WorkspaceAgentConnecting
|
||||
return nil
|
||||
},
|
||||
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
|
||||
return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
|
||||
},
|
||||
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
|
||||
agent.Status = codersdk.WorkspaceAgentConnected
|
||||
agent.FirstConnectedAt = ptr.Ref(time.Now())
|
||||
agent.StartedAt = ptr.Ref(time.Now())
|
||||
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartError
|
||||
agent.ReadyAt = ptr.Ref(time.Now())
|
||||
// These logs should NOT be shown in non-blocking mode,
|
||||
// even though we waited for connection.
|
||||
logs <- []codersdk.WorkspaceAgentLog{
|
||||
{
|
||||
CreatedAt: time.Now(),
|
||||
Output: "Startup script log 1",
|
||||
},
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// Note: Log content should NOT appear here despite waiting for connection.
|
||||
want: []string{
|
||||
"⧗ Waiting for the workspace agent to connect",
|
||||
"✔ Waiting for the workspace agent to connect",
|
||||
"⧗ Running workspace agent startup scripts (non-blocking)",
|
||||
"✘ Running workspace agent startup scripts (non-blocking)",
|
||||
"Warning: A startup script exited with an error and your workspace may be incomplete.",
|
||||
"For more information and troubleshooting, see",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Error when shutting down",
|
||||
opts: cliui.AgentOptions{
|
||||
@@ -485,6 +566,70 @@ func TestAgent(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, cmd.Invoke().Run())
|
||||
})
|
||||
|
||||
t.Run("ContextCancelDuringLogStreaming", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
agent := codersdk.WorkspaceAgent{
|
||||
ID: uuid.New(),
|
||||
Status: codersdk.WorkspaceAgentConnected,
|
||||
FirstConnectedAt: ptr.Ref(time.Now()),
|
||||
CreatedAt: time.Now(),
|
||||
LifecycleState: codersdk.WorkspaceAgentLifecycleStarting,
|
||||
StartedAt: ptr.Ref(time.Now()),
|
||||
}
|
||||
|
||||
logs := make(chan []codersdk.WorkspaceAgentLog, 1)
|
||||
logStreamStarted := make(chan struct{})
|
||||
|
||||
cmd := &serpent.Command{
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
return cliui.Agent(inv.Context(), io.Discard, agent.ID, cliui.AgentOptions{
|
||||
FetchInterval: time.Millisecond,
|
||||
Wait: true,
|
||||
Fetch: func(_ context.Context, _ uuid.UUID) (codersdk.WorkspaceAgent, error) {
|
||||
return agent, nil
|
||||
},
|
||||
FetchLogs: func(_ context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
|
||||
// Signal that log streaming has started.
|
||||
select {
|
||||
case <-logStreamStarted:
|
||||
default:
|
||||
close(logStreamStarted)
|
||||
}
|
||||
return logs, closeFunc(func() error { return nil }), nil
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
inv := cmd.Invoke().WithContext(ctx)
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- inv.Run()
|
||||
}()
|
||||
|
||||
// Wait for log streaming to start.
|
||||
select {
|
||||
case <-logStreamStarted:
|
||||
case <-time.After(testutil.WaitShort):
|
||||
t.Fatal("timed out waiting for log streaming to start")
|
||||
}
|
||||
|
||||
// Cancel the context while streaming logs.
|
||||
cancel()
|
||||
|
||||
// Verify that the agent function returns with a context error.
|
||||
select {
|
||||
case err := <-done:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
case <-time.After(testutil.WaitShort):
|
||||
t.Fatal("timed out waiting for agent to return after context cancellation")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPeerDiagnostics(t *testing.T) {
|
||||
|
||||
@@ -90,7 +90,6 @@ func TestExpRpty(t *testing.T) {
|
||||
wantLabel := "coder.devcontainers.TestExpRpty.Container"
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
@@ -128,14 +127,15 @@ func TestExpRpty(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
cmdDone := tGo(t, func() {
|
||||
err := inv.WithContext(ctx).Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
pty.ExpectMatch(" #")
|
||||
pty.ExpectMatchContext(ctx, " #")
|
||||
pty.WriteLine("hostname")
|
||||
pty.ExpectMatch(ct.Container.Config.Hostname)
|
||||
pty.ExpectMatchContext(ctx, ct.Container.Config.Hostname)
|
||||
pty.WriteLine("exit")
|
||||
<-cmdDone
|
||||
})
|
||||
|
||||
+3
-3
@@ -2052,7 +2052,6 @@ func TestSSH_Container(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
ct, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
@@ -2087,14 +2086,15 @@ func TestSSH_Container(t *testing.T) {
|
||||
clitest.SetupConfig(t, client, root)
|
||||
ptty := ptytest.New(t).Attach(inv)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
cmdDone := tGo(t, func() {
|
||||
err := inv.WithContext(ctx).Run()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
ptty.ExpectMatch(" #")
|
||||
ptty.ExpectMatchContext(ctx, " #")
|
||||
ptty.WriteLine("hostname")
|
||||
ptty.ExpectMatch(ct.Container.Config.Hostname)
|
||||
ptty.ExpectMatchContext(ctx, ct.Container.Config.Hostname)
|
||||
ptty.WriteLine("exit")
|
||||
<-cmdDone
|
||||
})
|
||||
|
||||
+7
@@ -46,6 +46,13 @@ OPTIONS:
|
||||
the workspace serves malicious JavaScript. This is recommended for
|
||||
security purposes if a --wildcard-access-url is configured.
|
||||
|
||||
--disable-workspace-sharing bool, $CODER_DISABLE_WORKSPACE_SHARING
|
||||
Disable workspace sharing (requires the "workspace-sharing" experiment
|
||||
to be enabled). Workspace ACL checking is disabled and only owners can
|
||||
have ssh, apps and terminal access to workspaces. Access based on the
|
||||
'owner' role is also allowed unless disabled via
|
||||
--disable-owner-workspace-access.
|
||||
|
||||
--swagger-enable bool, $CODER_SWAGGER_ENABLE
|
||||
Expose the swagger endpoint via /swagger.
|
||||
|
||||
|
||||
+6
@@ -497,6 +497,12 @@ disablePathApps: false
|
||||
# workspaces.
|
||||
# (default: <unset>, type: bool)
|
||||
disableOwnerWorkspaceAccess: false
|
||||
# Disable workspace sharing (requires the "workspace-sharing" experiment to be
|
||||
# enabled). Workspace ACL checking is disabled and only owners can have ssh, apps
|
||||
# and terminal access to workspaces. Access based on the 'owner' role is also
|
||||
# allowed unless disabled via --disable-owner-workspace-access.
|
||||
# (default: <unset>, type: bool)
|
||||
disableWorkspaceSharing: false
|
||||
# These options change the behavior of how clients interact with the Coder.
|
||||
# Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI.
|
||||
client:
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package agentapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
)
|
||||
|
||||
// CachedWorkspaceFields contains workspace data that is safe to cache for the
|
||||
@@ -50,3 +54,19 @@ func (cws *CachedWorkspaceFields) AsWorkspaceIdentity() (database.WorkspaceIdent
|
||||
}
|
||||
return cws.identity, true
|
||||
}
|
||||
|
||||
// ContextInject attempts to inject the rbac object for the cached workspace fields
|
||||
// into the given context, either returning the wrapped context or the original.
|
||||
func (cws *CachedWorkspaceFields) ContextInject(ctx context.Context) (context.Context, error) {
|
||||
var err error
|
||||
rbacCtx := ctx
|
||||
if dbws, ok := cws.AsWorkspaceIdentity(); ok {
|
||||
rbacCtx, err = dbauthz.WithWorkspaceRBAC(ctx, dbws.RBACObject())
|
||||
if err != nil {
|
||||
// Don't error level log here, will exit the function. We want to fall back to GetWorkspaceByAgentID.
|
||||
//nolint:gocritic
|
||||
return ctx, xerrors.Errorf("Cached workspace was present but RBAC object was invalid: %w", err)
|
||||
}
|
||||
}
|
||||
return rbacCtx, nil
|
||||
}
|
||||
|
||||
@@ -1680,8 +1680,8 @@ func TestTasksNotification(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaceAgent.Apps, 1)
|
||||
require.GreaterOrEqual(t, len(workspaceAgent.Apps[0].Statuses), 1)
|
||||
latestStatusIndex := len(workspaceAgent.Apps[0].Statuses) - 1
|
||||
require.Equal(t, tc.newAppStatus, workspaceAgent.Apps[0].Statuses[latestStatusIndex].State)
|
||||
// Statuses are ordered by created_at DESC, so the first element is the latest.
|
||||
require.Equal(t, tc.newAppStatus, workspaceAgent.Apps[0].Statuses[0].State)
|
||||
|
||||
if tc.isNotificationSent {
|
||||
// Then: A notification is sent to the workspace owner (memberUser)
|
||||
|
||||
Generated
+12
-3
@@ -1290,8 +1290,14 @@ const docTemplate = `{
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns existing file if duplicate",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UploadResponse"
|
||||
}
|
||||
},
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"description": "Returns newly created file",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UploadResponse"
|
||||
}
|
||||
@@ -1800,7 +1806,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Organizations"
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Add new license",
|
||||
"operationId": "add-new-license",
|
||||
@@ -1836,7 +1842,7 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Organizations"
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Update license entitlements",
|
||||
"operationId": "update-license-entitlements",
|
||||
@@ -14208,6 +14214,9 @@ const docTemplate = `{
|
||||
"disable_path_apps": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_workspace_sharing": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"docs_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
|
||||
Generated
+12
-3
@@ -1116,8 +1116,14 @@
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns existing file if duplicate",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UploadResponse"
|
||||
}
|
||||
},
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"description": "Returns newly created file",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UploadResponse"
|
||||
}
|
||||
@@ -1570,7 +1576,7 @@
|
||||
],
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Organizations"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Add new license",
|
||||
"operationId": "add-new-license",
|
||||
"parameters": [
|
||||
@@ -1602,7 +1608,7 @@
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Organizations"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Update license entitlements",
|
||||
"operationId": "update-license-entitlements",
|
||||
"responses": {
|
||||
@@ -12792,6 +12798,9 @@
|
||||
"disable_path_apps": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_workspace_sharing": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"docs_url": {
|
||||
"$ref": "#/definitions/serpent.URL"
|
||||
},
|
||||
|
||||
@@ -333,6 +333,10 @@ func New(options *Options) *API {
|
||||
})
|
||||
}
|
||||
|
||||
if options.DeploymentValues.DisableWorkspaceSharing {
|
||||
rbac.SetWorkspaceACLDisabled(true)
|
||||
}
|
||||
|
||||
if options.PrometheusRegistry == nil {
|
||||
options.PrometheusRegistry = prometheus.NewRegistry()
|
||||
}
|
||||
|
||||
@@ -2455,6 +2455,18 @@ func (q *querier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Contex
|
||||
}
|
||||
|
||||
func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
|
||||
// Fast path: Check if we have a workspace RBAC object in context.
|
||||
if rbacObj, ok := WorkspaceRBACFromContext(ctx); ok {
|
||||
// Errors here will result in falling back to GetWorkspaceByAgentID,
|
||||
// in case the cached data is stale.
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbacObj); err == nil {
|
||||
return q.db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID)
|
||||
}
|
||||
|
||||
q.log.Debug(ctx, "fast path authorization failed for GetLatestWorkspaceBuildByWorkspaceID, using slow path",
|
||||
slog.F("workspace_id", workspaceID))
|
||||
}
|
||||
|
||||
if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil {
|
||||
return database.WorkspaceBuild{}, err
|
||||
}
|
||||
|
||||
@@ -4731,3 +4731,77 @@ func (s *MethodTestSuite) TestTelemetry() {
|
||||
check.Args(database.CalculateAIBridgeInterceptionsTelemetrySummaryParams{}).Asserts(rbac.ResourceAibridgeInterception, policy.ActionRead)
|
||||
}))
|
||||
}
|
||||
|
||||
func TestGetLatestWorkspaceBuildByWorkspaceID_FastPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ownerID := uuid.New()
|
||||
wsID := uuid.New()
|
||||
orgID := uuid.New()
|
||||
|
||||
workspace := database.Workspace{
|
||||
ID: wsID,
|
||||
OwnerID: ownerID,
|
||||
OrganizationID: orgID,
|
||||
}
|
||||
|
||||
build := database.WorkspaceBuild{
|
||||
ID: uuid.New(),
|
||||
WorkspaceID: wsID,
|
||||
}
|
||||
|
||||
wsIdentity := database.WorkspaceIdentity{
|
||||
ID: wsID,
|
||||
OwnerID: ownerID,
|
||||
OrganizationID: orgID,
|
||||
}
|
||||
|
||||
actor := rbac.Subject{
|
||||
ID: ownerID.String(),
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
|
||||
Groups: []string{orgID.String()},
|
||||
Scope: rbac.ScopeAll,
|
||||
}
|
||||
|
||||
authorizer := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: (&coderdtest.FakeAuthorizer{}).AlwaysReturn(nil),
|
||||
}
|
||||
|
||||
t.Run("WithWorkspaceRBAC", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := dbauthz.As(context.Background(), actor)
|
||||
ctrl := gomock.NewController(t)
|
||||
dbm := dbmock.NewMockStore(ctrl)
|
||||
|
||||
rbacObj := wsIdentity.RBACObject()
|
||||
ctx, err := dbauthz.WithWorkspaceRBAC(ctx, rbacObj)
|
||||
require.NoError(t, err)
|
||||
|
||||
dbm.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspace.ID).Return(build, nil).AnyTimes()
|
||||
dbm.EXPECT().Wrappers().Return([]string{})
|
||||
|
||||
q := dbauthz.New(dbm, authorizer, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
|
||||
|
||||
result, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, build, result)
|
||||
})
|
||||
t.Run("WithoutWorkspaceRBAC", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := dbauthz.As(context.Background(), actor)
|
||||
ctrl := gomock.NewController(t)
|
||||
dbm := dbmock.NewMockStore(ctrl)
|
||||
|
||||
dbm.EXPECT().GetWorkspaceByID(gomock.Any(), wsID).Return(workspace, nil).AnyTimes()
|
||||
dbm.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspace.ID).Return(build, nil).AnyTimes()
|
||||
dbm.EXPECT().Wrappers().Return([]string{})
|
||||
|
||||
q := dbauthz.New(dbm, authorizer, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
|
||||
|
||||
result, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, build, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -439,6 +439,16 @@ func Workspace(t testing.TB, db database.Store, orig database.WorkspaceTable) da
|
||||
require.NoError(t, err, "set workspace as dormant")
|
||||
workspace.DormantAt = orig.DormantAt
|
||||
}
|
||||
if len(orig.UserACL) > 0 || len(orig.GroupACL) > 0 {
|
||||
err = db.UpdateWorkspaceACLByID(genCtx, database.UpdateWorkspaceACLByIDParams{
|
||||
ID: workspace.ID,
|
||||
UserACL: orig.UserACL,
|
||||
GroupACL: orig.GroupACL,
|
||||
})
|
||||
require.NoError(t, err, "set workspace ACL")
|
||||
workspace.UserACL = orig.UserACL
|
||||
workspace.GroupACL = orig.GroupACL
|
||||
}
|
||||
return workspace
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ func NewDBWithSQLDB(t testing.TB, opts ...Option) (database.Store, pubsub.Pubsub
|
||||
return db, ps, sqlDB
|
||||
}
|
||||
|
||||
var DefaultTimezone = "Canada/Newfoundland"
|
||||
var DefaultTimezone = "America/St_Johns"
|
||||
|
||||
// NowInDefaultTimezone returns the current time rounded to the nearest microsecond in the default timezone
|
||||
// used by postgres in tests. Useful for object equality checks.
|
||||
|
||||
Generated
+2
@@ -3449,6 +3449,8 @@ COMMENT ON INDEX workspace_app_audit_sessions_unique_index IS 'Unique index to e
|
||||
|
||||
CREATE INDEX workspace_app_stats_workspace_id_idx ON workspace_app_stats USING btree (workspace_id);
|
||||
|
||||
CREATE INDEX workspace_app_statuses_app_id_idx ON workspace_app_statuses USING btree (app_id, created_at DESC);
|
||||
|
||||
CREATE INDEX workspace_modules_created_at_idx ON workspace_modules USING btree (created_at);
|
||||
|
||||
CREATE INDEX workspace_next_start_at_idx ON workspaces USING btree (next_start_at) WHERE (deleted = false);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS workspace_app_statuses_app_id_idx;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX workspace_app_statuses_app_id_idx ON workspace_app_statuses (app_id, created_at DESC);
|
||||
@@ -430,9 +430,16 @@ func (w WorkspaceTable) RBACObject() rbac.Object {
|
||||
return w.DormantRBAC()
|
||||
}
|
||||
|
||||
return rbac.ResourceWorkspace.WithID(w.ID).
|
||||
obj := rbac.ResourceWorkspace.
|
||||
WithID(w.ID).
|
||||
InOrg(w.OrganizationID).
|
||||
WithOwner(w.OwnerID.String()).
|
||||
WithOwner(w.OwnerID.String())
|
||||
|
||||
if rbac.WorkspaceACLDisabled() {
|
||||
return obj
|
||||
}
|
||||
|
||||
return obj.
|
||||
WithGroupACL(w.GroupACL.RBACACL()).
|
||||
WithACLUserList(w.UserACL.RBACACL())
|
||||
}
|
||||
|
||||
@@ -143,6 +143,45 @@ func TestAPIKeyScopesExpand(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:tparallel,paralleltest
|
||||
func TestWorkspaceACLDisabled(t *testing.T) {
|
||||
uid := uuid.NewString()
|
||||
gid := uuid.NewString()
|
||||
|
||||
ws := WorkspaceTable{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: uuid.New(),
|
||||
OwnerID: uuid.New(),
|
||||
UserACL: WorkspaceACL{
|
||||
uid: WorkspaceACLEntry{Permissions: []policy.Action{policy.ActionSSH}},
|
||||
},
|
||||
GroupACL: WorkspaceACL{
|
||||
gid: WorkspaceACLEntry{Permissions: []policy.Action{policy.ActionSSH}},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("ACLsOmittedWhenDisabled", func(t *testing.T) {
|
||||
rbac.SetWorkspaceACLDisabled(true)
|
||||
t.Cleanup(func() { rbac.SetWorkspaceACLDisabled(false) })
|
||||
|
||||
obj := ws.RBACObject()
|
||||
|
||||
require.Empty(t, obj.ACLUserList, "user ACLs should be empty when disabled")
|
||||
require.Empty(t, obj.ACLGroupList, "group ACLs should be empty when disabled")
|
||||
})
|
||||
|
||||
t.Run("ACLsIncludedWhenEnabled", func(t *testing.T) {
|
||||
rbac.SetWorkspaceACLDisabled(false)
|
||||
|
||||
obj := ws.RBACObject()
|
||||
|
||||
require.NotEmpty(t, obj.ACLUserList, "user ACLs should be present when enabled")
|
||||
require.NotEmpty(t, obj.ACLGroupList, "group ACLs should be present when enabled")
|
||||
require.Contains(t, obj.ACLUserList, uid)
|
||||
require.Contains(t, obj.ACLGroupList, gid)
|
||||
})
|
||||
}
|
||||
|
||||
// Helpers
|
||||
func requirePermission(t *testing.T, s rbac.Scope, resource string, action policy.Action) {
|
||||
t.Helper()
|
||||
|
||||
@@ -4082,7 +4082,7 @@ func TestGetUserStatusCounts(t *testing.T) {
|
||||
t.Skip("https://github.com/coder/internal/issues/464")
|
||||
|
||||
timezones := []string{
|
||||
"Canada/Newfoundland",
|
||||
"America/St_Johns",
|
||||
"Africa/Johannesburg",
|
||||
"America/New_York",
|
||||
"Europe/London",
|
||||
|
||||
@@ -4209,6 +4209,21 @@ func (q *sqlQuerier) GetTemplateAppInsights(ctx context.Context, arg GetTemplate
|
||||
|
||||
const getTemplateAppInsightsByTemplate = `-- name: GetTemplateAppInsightsByTemplate :many
|
||||
WITH
|
||||
filtered_stats AS (
|
||||
SELECT
|
||||
was.workspace_id,
|
||||
was.user_id,
|
||||
was.agent_id,
|
||||
was.access_method,
|
||||
was.slug_or_port,
|
||||
was.session_started_at,
|
||||
was.session_ended_at
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
WHERE
|
||||
was.session_ended_at >= $1::timestamptz
|
||||
AND was.session_started_at < $2::timestamptz
|
||||
),
|
||||
-- This CTE is used to explode app usage into minute buckets, then
|
||||
-- flatten the users app usage within the template so that usage in
|
||||
-- multiple workspaces under one template is only counted once for
|
||||
@@ -4216,45 +4231,45 @@ WITH
|
||||
app_insights AS (
|
||||
SELECT
|
||||
w.template_id,
|
||||
was.user_id,
|
||||
fs.user_id,
|
||||
-- Both app stats and agent stats track web terminal usage, but
|
||||
-- by different means. The app stats value should be more
|
||||
-- accurate so we don't want to discard it just yet.
|
||||
CASE
|
||||
WHEN was.access_method = 'terminal'
|
||||
WHEN fs.access_method = 'terminal'
|
||||
THEN '[terminal]' -- Unique name, app names can't contain brackets.
|
||||
ELSE was.slug_or_port
|
||||
ELSE fs.slug_or_port
|
||||
END::text AS app_name,
|
||||
COALESCE(wa.display_name, '') AS display_name,
|
||||
(wa.slug IS NOT NULL)::boolean AS is_app,
|
||||
COUNT(DISTINCT s.minute_bucket) AS app_minutes
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
filtered_stats AS fs
|
||||
JOIN
|
||||
workspaces AS w
|
||||
ON
|
||||
w.id = was.workspace_id
|
||||
w.id = fs.workspace_id
|
||||
-- We do a left join here because we want to include user IDs that have used
|
||||
-- e.g. ports when counting active users.
|
||||
LEFT JOIN
|
||||
workspace_apps wa
|
||||
ON
|
||||
wa.agent_id = was.agent_id
|
||||
AND wa.slug = was.slug_or_port
|
||||
wa.agent_id = fs.agent_id
|
||||
AND wa.slug = fs.slug_or_port
|
||||
-- Generate a series of minute buckets for each session for computing the
|
||||
-- mintes/bucket.
|
||||
CROSS JOIN
|
||||
generate_series(
|
||||
date_trunc('minute', was.session_started_at),
|
||||
date_trunc('minute', fs.session_started_at),
|
||||
-- Subtract 1 μs to avoid creating an extra series.
|
||||
date_trunc('minute', was.session_ended_at - '1 microsecond'::interval),
|
||||
date_trunc('minute', fs.session_ended_at - '1 microsecond'::interval),
|
||||
'1 minute'::interval
|
||||
) AS s(minute_bucket)
|
||||
WHERE
|
||||
s.minute_bucket >= $1::timestamptz
|
||||
AND s.minute_bucket < $2::timestamptz
|
||||
GROUP BY
|
||||
w.template_id, was.user_id, was.access_method, was.slug_or_port, wa.display_name, wa.slug
|
||||
w.template_id, fs.user_id, fs.access_method, fs.slug_or_port, wa.display_name, wa.slug
|
||||
)
|
||||
|
||||
SELECT
|
||||
@@ -5109,37 +5124,52 @@ WITH
|
||||
FROM
|
||||
template_usage_stats
|
||||
),
|
||||
filtered_app_stats AS (
|
||||
SELECT
|
||||
was.workspace_id,
|
||||
was.user_id,
|
||||
was.agent_id,
|
||||
was.access_method,
|
||||
was.slug_or_port,
|
||||
was.session_started_at,
|
||||
was.session_ended_at
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
WHERE
|
||||
was.session_ended_at >= (SELECT t FROM latest_start)
|
||||
AND was.session_started_at < NOW()
|
||||
),
|
||||
workspace_app_stat_buckets AS (
|
||||
SELECT
|
||||
-- Truncate the minute to the nearest half hour, this is the bucket size
|
||||
-- for the data.
|
||||
date_trunc('hour', s.minute_bucket) + trunc(date_part('minute', s.minute_bucket) / 30) * 30 * '1 minute'::interval AS time_bucket,
|
||||
w.template_id,
|
||||
was.user_id,
|
||||
fas.user_id,
|
||||
-- Both app stats and agent stats track web terminal usage, but
|
||||
-- by different means. The app stats value should be more
|
||||
-- accurate so we don't want to discard it just yet.
|
||||
CASE
|
||||
WHEN was.access_method = 'terminal'
|
||||
WHEN fas.access_method = 'terminal'
|
||||
THEN '[terminal]' -- Unique name, app names can't contain brackets.
|
||||
ELSE was.slug_or_port
|
||||
ELSE fas.slug_or_port
|
||||
END AS app_name,
|
||||
COUNT(DISTINCT s.minute_bucket) AS app_minutes,
|
||||
-- Store each unique minute bucket for later merge between datasets.
|
||||
array_agg(DISTINCT s.minute_bucket) AS minute_buckets
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
filtered_app_stats AS fas
|
||||
JOIN
|
||||
workspaces AS w
|
||||
ON
|
||||
w.id = was.workspace_id
|
||||
w.id = fas.workspace_id
|
||||
-- Generate a series of minute buckets for each session for computing the
|
||||
-- mintes/bucket.
|
||||
CROSS JOIN
|
||||
generate_series(
|
||||
date_trunc('minute', was.session_started_at),
|
||||
date_trunc('minute', fas.session_started_at),
|
||||
-- Subtract 1 μs to avoid creating an extra series.
|
||||
date_trunc('minute', was.session_ended_at - '1 microsecond'::interval),
|
||||
date_trunc('minute', fas.session_ended_at - '1 microsecond'::interval),
|
||||
'1 minute'::interval
|
||||
) AS s(minute_bucket)
|
||||
WHERE
|
||||
@@ -5148,7 +5178,7 @@ WITH
|
||||
s.minute_bucket >= (SELECT t FROM latest_start)
|
||||
AND s.minute_bucket < NOW()
|
||||
GROUP BY
|
||||
time_bucket, w.template_id, was.user_id, was.access_method, was.slug_or_port
|
||||
time_bucket, w.template_id, fas.user_id, fas.access_method, fas.slug_or_port
|
||||
),
|
||||
agent_stats_buckets AS (
|
||||
SELECT
|
||||
@@ -19374,27 +19404,32 @@ func (q *sqlQuerier) GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]G
|
||||
}
|
||||
|
||||
const getDeploymentWorkspaceAgentStats = `-- name: GetDeploymentWorkspaceAgentStats :one
|
||||
WITH agent_stats AS (
|
||||
SELECT
|
||||
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
|
||||
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
|
||||
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
|
||||
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
|
||||
FROM workspace_agent_stats
|
||||
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
|
||||
WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
|
||||
), latest_agent_stats AS (
|
||||
SELECT
|
||||
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
|
||||
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
|
||||
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
|
||||
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
|
||||
FROM (
|
||||
SELECT id, created_at, user_id, agent_id, workspace_id, template_id, connections_by_proto, connection_count, rx_packets, rx_bytes, tx_packets, tx_bytes, connection_median_latency_ms, session_count_vscode, session_count_jetbrains, session_count_reconnecting_pty, session_count_ssh, usage, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn
|
||||
FROM workspace_agent_stats WHERE created_at > $1
|
||||
) AS a WHERE a.rn = 1
|
||||
WITH stats AS (
|
||||
SELECT
|
||||
agent_id,
|
||||
created_at,
|
||||
rx_bytes,
|
||||
tx_bytes,
|
||||
connection_median_latency_ms,
|
||||
session_count_vscode,
|
||||
session_count_ssh,
|
||||
session_count_jetbrains,
|
||||
session_count_reconnecting_pty,
|
||||
ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY created_at DESC) AS rn
|
||||
FROM workspace_agent_stats
|
||||
WHERE created_at > $1
|
||||
)
|
||||
SELECT workspace_rx_bytes, workspace_tx_bytes, workspace_connection_latency_50, workspace_connection_latency_95, session_count_vscode, session_count_ssh, session_count_jetbrains, session_count_reconnecting_pty FROM agent_stats, latest_agent_stats
|
||||
SELECT
|
||||
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
|
||||
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
|
||||
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
|
||||
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50,
|
||||
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95,
|
||||
coalesce(SUM(session_count_vscode) FILTER (WHERE rn = 1), 0)::bigint AS session_count_vscode,
|
||||
coalesce(SUM(session_count_ssh) FILTER (WHERE rn = 1), 0)::bigint AS session_count_ssh,
|
||||
coalesce(SUM(session_count_jetbrains) FILTER (WHERE rn = 1), 0)::bigint AS session_count_jetbrains,
|
||||
coalesce(SUM(session_count_reconnecting_pty) FILTER (WHERE rn = 1), 0)::bigint AS session_count_reconnecting_pty
|
||||
FROM stats
|
||||
`
|
||||
|
||||
type GetDeploymentWorkspaceAgentStatsRow struct {
|
||||
@@ -20246,6 +20281,7 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg Ge
|
||||
|
||||
const getWorkspaceAppStatusesByAppIDs = `-- name: GetWorkspaceAppStatusesByAppIDs :many
|
||||
SELECT id, created_at, agent_id, app_id, workspace_id, state, message, uri FROM workspace_app_statuses WHERE app_id = ANY($1 :: uuid [ ])
|
||||
ORDER BY created_at DESC, id DESC
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) {
|
||||
|
||||
@@ -350,6 +350,21 @@ GROUP BY
|
||||
-- GetTemplateAppInsightsByTemplate is used for Prometheus metrics. Keep
|
||||
-- in sync with GetTemplateAppInsights and UpsertTemplateUsageStats.
|
||||
WITH
|
||||
filtered_stats AS (
|
||||
SELECT
|
||||
was.workspace_id,
|
||||
was.user_id,
|
||||
was.agent_id,
|
||||
was.access_method,
|
||||
was.slug_or_port,
|
||||
was.session_started_at,
|
||||
was.session_ended_at
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
WHERE
|
||||
was.session_ended_at >= @start_time::timestamptz
|
||||
AND was.session_started_at < @end_time::timestamptz
|
||||
),
|
||||
-- This CTE is used to explode app usage into minute buckets, then
|
||||
-- flatten the users app usage within the template so that usage in
|
||||
-- multiple workspaces under one template is only counted once for
|
||||
@@ -357,45 +372,45 @@ WITH
|
||||
app_insights AS (
|
||||
SELECT
|
||||
w.template_id,
|
||||
was.user_id,
|
||||
fs.user_id,
|
||||
-- Both app stats and agent stats track web terminal usage, but
|
||||
-- by different means. The app stats value should be more
|
||||
-- accurate so we don't want to discard it just yet.
|
||||
CASE
|
||||
WHEN was.access_method = 'terminal'
|
||||
WHEN fs.access_method = 'terminal'
|
||||
THEN '[terminal]' -- Unique name, app names can't contain brackets.
|
||||
ELSE was.slug_or_port
|
||||
ELSE fs.slug_or_port
|
||||
END::text AS app_name,
|
||||
COALESCE(wa.display_name, '') AS display_name,
|
||||
(wa.slug IS NOT NULL)::boolean AS is_app,
|
||||
COUNT(DISTINCT s.minute_bucket) AS app_minutes
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
filtered_stats AS fs
|
||||
JOIN
|
||||
workspaces AS w
|
||||
ON
|
||||
w.id = was.workspace_id
|
||||
w.id = fs.workspace_id
|
||||
-- We do a left join here because we want to include user IDs that have used
|
||||
-- e.g. ports when counting active users.
|
||||
LEFT JOIN
|
||||
workspace_apps wa
|
||||
ON
|
||||
wa.agent_id = was.agent_id
|
||||
AND wa.slug = was.slug_or_port
|
||||
wa.agent_id = fs.agent_id
|
||||
AND wa.slug = fs.slug_or_port
|
||||
-- Generate a series of minute buckets for each session for computing the
|
||||
-- mintes/bucket.
|
||||
CROSS JOIN
|
||||
generate_series(
|
||||
date_trunc('minute', was.session_started_at),
|
||||
date_trunc('minute', fs.session_started_at),
|
||||
-- Subtract 1 μs to avoid creating an extra series.
|
||||
date_trunc('minute', was.session_ended_at - '1 microsecond'::interval),
|
||||
date_trunc('minute', fs.session_ended_at - '1 microsecond'::interval),
|
||||
'1 minute'::interval
|
||||
) AS s(minute_bucket)
|
||||
WHERE
|
||||
s.minute_bucket >= @start_time::timestamptz
|
||||
AND s.minute_bucket < @end_time::timestamptz
|
||||
GROUP BY
|
||||
w.template_id, was.user_id, was.access_method, was.slug_or_port, wa.display_name, wa.slug
|
||||
w.template_id, fs.user_id, fs.access_method, fs.slug_or_port, wa.display_name, wa.slug
|
||||
)
|
||||
|
||||
SELECT
|
||||
@@ -480,37 +495,52 @@ WITH
|
||||
FROM
|
||||
template_usage_stats
|
||||
),
|
||||
filtered_app_stats AS (
|
||||
SELECT
|
||||
was.workspace_id,
|
||||
was.user_id,
|
||||
was.agent_id,
|
||||
was.access_method,
|
||||
was.slug_or_port,
|
||||
was.session_started_at,
|
||||
was.session_ended_at
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
WHERE
|
||||
was.session_ended_at >= (SELECT t FROM latest_start)
|
||||
AND was.session_started_at < NOW()
|
||||
),
|
||||
workspace_app_stat_buckets AS (
|
||||
SELECT
|
||||
-- Truncate the minute to the nearest half hour, this is the bucket size
|
||||
-- for the data.
|
||||
date_trunc('hour', s.minute_bucket) + trunc(date_part('minute', s.minute_bucket) / 30) * 30 * '1 minute'::interval AS time_bucket,
|
||||
w.template_id,
|
||||
was.user_id,
|
||||
fas.user_id,
|
||||
-- Both app stats and agent stats track web terminal usage, but
|
||||
-- by different means. The app stats value should be more
|
||||
-- accurate so we don't want to discard it just yet.
|
||||
CASE
|
||||
WHEN was.access_method = 'terminal'
|
||||
WHEN fas.access_method = 'terminal'
|
||||
THEN '[terminal]' -- Unique name, app names can't contain brackets.
|
||||
ELSE was.slug_or_port
|
||||
ELSE fas.slug_or_port
|
||||
END AS app_name,
|
||||
COUNT(DISTINCT s.minute_bucket) AS app_minutes,
|
||||
-- Store each unique minute bucket for later merge between datasets.
|
||||
array_agg(DISTINCT s.minute_bucket) AS minute_buckets
|
||||
FROM
|
||||
workspace_app_stats AS was
|
||||
filtered_app_stats AS fas
|
||||
JOIN
|
||||
workspaces AS w
|
||||
ON
|
||||
w.id = was.workspace_id
|
||||
w.id = fas.workspace_id
|
||||
-- Generate a series of minute buckets for each session for computing the
|
||||
-- mintes/bucket.
|
||||
CROSS JOIN
|
||||
generate_series(
|
||||
date_trunc('minute', was.session_started_at),
|
||||
date_trunc('minute', fas.session_started_at),
|
||||
-- Subtract 1 μs to avoid creating an extra series.
|
||||
date_trunc('minute', was.session_ended_at - '1 microsecond'::interval),
|
||||
date_trunc('minute', fas.session_ended_at - '1 microsecond'::interval),
|
||||
'1 minute'::interval
|
||||
) AS s(minute_bucket)
|
||||
WHERE
|
||||
@@ -519,7 +549,7 @@ WITH
|
||||
s.minute_bucket >= (SELECT t FROM latest_start)
|
||||
AND s.minute_bucket < NOW()
|
||||
GROUP BY
|
||||
time_bucket, w.template_id, was.user_id, was.access_method, was.slug_or_port
|
||||
time_bucket, w.template_id, fas.user_id, fas.access_method, fas.slug_or_port
|
||||
),
|
||||
agent_stats_buckets AS (
|
||||
SELECT
|
||||
|
||||
@@ -99,27 +99,32 @@ WHERE
|
||||
);
|
||||
|
||||
-- name: GetDeploymentWorkspaceAgentStats :one
|
||||
WITH agent_stats AS (
|
||||
SELECT
|
||||
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
|
||||
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
|
||||
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
|
||||
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
|
||||
FROM workspace_agent_stats
|
||||
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
|
||||
WHERE workspace_agent_stats.created_at > $1 AND connection_median_latency_ms > 0
|
||||
), latest_agent_stats AS (
|
||||
SELECT
|
||||
coalesce(SUM(session_count_vscode), 0)::bigint AS session_count_vscode,
|
||||
coalesce(SUM(session_count_ssh), 0)::bigint AS session_count_ssh,
|
||||
coalesce(SUM(session_count_jetbrains), 0)::bigint AS session_count_jetbrains,
|
||||
coalesce(SUM(session_count_reconnecting_pty), 0)::bigint AS session_count_reconnecting_pty
|
||||
FROM (
|
||||
SELECT *, ROW_NUMBER() OVER(PARTITION BY agent_id ORDER BY created_at DESC) AS rn
|
||||
FROM workspace_agent_stats WHERE created_at > $1
|
||||
) AS a WHERE a.rn = 1
|
||||
WITH stats AS (
|
||||
SELECT
|
||||
agent_id,
|
||||
created_at,
|
||||
rx_bytes,
|
||||
tx_bytes,
|
||||
connection_median_latency_ms,
|
||||
session_count_vscode,
|
||||
session_count_ssh,
|
||||
session_count_jetbrains,
|
||||
session_count_reconnecting_pty,
|
||||
ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY created_at DESC) AS rn
|
||||
FROM workspace_agent_stats
|
||||
WHERE created_at > $1
|
||||
)
|
||||
SELECT * FROM agent_stats, latest_agent_stats;
|
||||
SELECT
|
||||
coalesce(SUM(rx_bytes), 0)::bigint AS workspace_rx_bytes,
|
||||
coalesce(SUM(tx_bytes), 0)::bigint AS workspace_tx_bytes,
|
||||
-- The greater than 0 is to support legacy agents that don't report connection_median_latency_ms.
|
||||
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_50,
|
||||
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms) FILTER (WHERE connection_median_latency_ms > 0)), -1)::FLOAT AS workspace_connection_latency_95,
|
||||
coalesce(SUM(session_count_vscode) FILTER (WHERE rn = 1), 0)::bigint AS session_count_vscode,
|
||||
coalesce(SUM(session_count_ssh) FILTER (WHERE rn = 1), 0)::bigint AS session_count_ssh,
|
||||
coalesce(SUM(session_count_jetbrains) FILTER (WHERE rn = 1), 0)::bigint AS session_count_jetbrains,
|
||||
coalesce(SUM(session_count_reconnecting_pty) FILTER (WHERE rn = 1), 0)::bigint AS session_count_reconnecting_pty
|
||||
FROM stats;
|
||||
|
||||
-- name: GetDeploymentWorkspaceAgentUsageStats :one
|
||||
WITH agent_stats AS (
|
||||
|
||||
@@ -71,7 +71,8 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetWorkspaceAppStatusesByAppIDs :many
|
||||
SELECT * FROM workspace_app_statuses WHERE app_id = ANY(@ids :: uuid [ ]);
|
||||
SELECT * FROM workspace_app_statuses WHERE app_id = ANY(@ids :: uuid [ ])
|
||||
ORDER BY created_at DESC, id DESC;
|
||||
|
||||
-- name: GetLatestWorkspaceAppStatusByAppID :one
|
||||
SELECT *
|
||||
|
||||
+2
-1
@@ -41,7 +41,8 @@ const (
|
||||
// @Tags Files
|
||||
// @Param Content-Type header string true "Content-Type must be `application/x-tar` or `application/zip`" default(application/x-tar)
|
||||
// @Param file formData file true "File to be uploaded. If using tar format, file must conform to ustar (pax may cause problems)."
|
||||
// @Success 201 {object} codersdk.UploadResponse
|
||||
// @Success 200 {object} codersdk.UploadResponse "Returns existing file if duplicate"
|
||||
// @Success 201 {object} codersdk.UploadResponse "Returns newly created file"
|
||||
// @Router /files [post]
|
||||
func (api *API) postFile(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -251,13 +251,16 @@ type HTTPError struct {
|
||||
func (e HTTPError) Write(rw http.ResponseWriter, r *http.Request) {
|
||||
if e.RenderStaticPage {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: e.Code,
|
||||
HideStatus: true,
|
||||
Title: e.Msg,
|
||||
Description: e.Detail,
|
||||
RetryEnabled: false,
|
||||
DashboardURL: "/login",
|
||||
|
||||
Status: e.Code,
|
||||
HideStatus: true,
|
||||
Title: e.Msg,
|
||||
Description: e.Detail,
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: "/login",
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
RenderDescriptionMarkdown: e.RenderDetailMarkdown,
|
||||
})
|
||||
return
|
||||
|
||||
@@ -87,7 +87,9 @@ func (c *Cache) refreshTemplateBuildTimes(ctx context.Context) error {
|
||||
//nolint:gocritic // This is a system service.
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
|
||||
templates, err := c.database.GetTemplates(ctx)
|
||||
templates, err := c.database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
|
||||
Deleted: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -75,7 +75,18 @@ func ShowAuthorizePage(accessURL *url.URL) http.HandlerFunc {
|
||||
|
||||
callbackURL, err := url.Parse(app.CallbackURL)
|
||||
if err != nil {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{Status: http.StatusInternalServerError, HideStatus: false, Title: "Internal Server Error", Description: err.Error(), RetryEnabled: false, DashboardURL: accessURL.String(), Warnings: nil})
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
HideStatus: false,
|
||||
Title: "Internal Server Error",
|
||||
Description: err.Error(),
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: accessURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,7 +96,19 @@ func ShowAuthorizePage(accessURL *url.URL) http.HandlerFunc {
|
||||
for i, err := range validationErrs {
|
||||
errStr[i] = err.Detail
|
||||
}
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{Status: http.StatusBadRequest, HideStatus: false, Title: "Invalid Query Parameters", Description: "One or more query parameters are missing or invalid.", RetryEnabled: false, DashboardURL: accessURL.String(), Warnings: errStr})
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadRequest,
|
||||
HideStatus: false,
|
||||
Title: "Invalid Query Parameters",
|
||||
Description: "One or more query parameters are missing or invalid.",
|
||||
Warnings: errStr,
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: accessURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -236,3 +236,19 @@ func (z Object) WithGroupACL(groups map[string][]policy.Action) Object {
|
||||
AnyOrgOwner: z.AnyOrgOwner,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(geokat): similar to builtInRoles, this should ideally be
|
||||
// scoped to a coderd rather than a global.
|
||||
var workspaceACLDisabled bool
|
||||
|
||||
// SetWorkspaceACLDisabled disables/enables workspace sharing for the
|
||||
// deployment.
|
||||
func SetWorkspaceACLDisabled(v bool) {
|
||||
workspaceACLDisabled = v
|
||||
}
|
||||
|
||||
// WorkspaceACLDisabled returns true if workspace sharing is disabled
|
||||
// for the deployment.
|
||||
func WorkspaceACLDisabled() bool {
|
||||
return workspaceACLDisabled
|
||||
}
|
||||
|
||||
+20
-14
@@ -199,10 +199,9 @@ func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID u
|
||||
proxy := httputil.NewSingleHostReverseProxy(&tgt)
|
||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, theErr error) {
|
||||
var (
|
||||
desc = "Failed to proxy request to application: " + theErr.Error()
|
||||
additionalInfo = ""
|
||||
additionalButtonLink = ""
|
||||
additionalButtonText = ""
|
||||
desc = "Failed to proxy request to application: " + theErr.Error()
|
||||
additionalInfo = ""
|
||||
actions = []site.Action{}
|
||||
)
|
||||
|
||||
var tlsError tls.RecordHeaderError
|
||||
@@ -222,21 +221,28 @@ func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID u
|
||||
app = app.ChangePortProtocol(targetProtocol)
|
||||
|
||||
switchURL.Host = fmt.Sprintf("%s%s", app.String(), strings.TrimPrefix(wildcardHostname, "*"))
|
||||
additionalButtonLink = switchURL.String()
|
||||
additionalButtonText = fmt.Sprintf("Switch to %s", strings.ToUpper(targetProtocol))
|
||||
actions = append(actions, site.Action{
|
||||
URL: switchURL.String(),
|
||||
Text: fmt.Sprintf("Switch to %s", strings.ToUpper(targetProtocol)),
|
||||
})
|
||||
additionalInfo += fmt.Sprintf("This error seems to be due to an app protocol mismatch, try switching to %s.", strings.ToUpper(targetProtocol))
|
||||
}
|
||||
}
|
||||
|
||||
site.RenderStaticErrorPage(w, r, site.ErrorPageData{
|
||||
Status: http.StatusBadGateway,
|
||||
Title: "Bad Gateway",
|
||||
Description: desc,
|
||||
RetryEnabled: true,
|
||||
DashboardURL: dashboardURL.String(),
|
||||
AdditionalInfo: additionalInfo,
|
||||
AdditionalButtonLink: additionalButtonLink,
|
||||
AdditionalButtonText: additionalButtonText,
|
||||
Status: http.StatusBadGateway,
|
||||
Title: "Bad Gateway",
|
||||
Description: desc,
|
||||
Actions: append(actions, []site.Action{
|
||||
{
|
||||
Text: "Retry",
|
||||
},
|
||||
{
|
||||
URL: dashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
}...),
|
||||
AdditionalInfo: additionalInfo,
|
||||
})
|
||||
}
|
||||
proxy.Director = s.director(agentID, proxy.Director)
|
||||
|
||||
@@ -71,6 +71,18 @@ Prompt: "Set up CI/CD pipeline" →
|
||||
"task_name": "setup-cicd"
|
||||
}
|
||||
|
||||
Prompt: "Work on https://github.com/coder/coder/issues/1234" →
|
||||
{
|
||||
"display_name": "Work on coder/coder #1234",
|
||||
"task_name": "coder-1234"
|
||||
}
|
||||
|
||||
Prompt: "Fix https://github.com/org/repo/pull/567" →
|
||||
{
|
||||
"display_name": "Fix org/repo PR #567",
|
||||
"task_name": "repo-pr-567"
|
||||
}
|
||||
|
||||
If a suitable name cannot be created, output exactly:
|
||||
{
|
||||
"display_name": "Task Unnamed",
|
||||
|
||||
@@ -227,10 +227,11 @@ func (api *API) startAgentYamuxMonitor(ctx context.Context,
|
||||
mux *yamux.Session,
|
||||
) *agentConnectionMonitor {
|
||||
monitor := &agentConnectionMonitor{
|
||||
apiCtx: api.ctx,
|
||||
workspace: workspace,
|
||||
workspaceAgent: workspaceAgent,
|
||||
workspaceBuild: workspaceBuild,
|
||||
apiCtx: api.ctx,
|
||||
workspace: workspace,
|
||||
workspaceAgent: workspaceAgent,
|
||||
workspaceBuild: workspaceBuild,
|
||||
|
||||
conn: &yamuxPingerCloser{mux: mux},
|
||||
pingPeriod: api.AgentConnectionUpdateFrequency,
|
||||
db: api.Database,
|
||||
@@ -453,6 +454,13 @@ func (m *agentConnectionMonitor) monitor(ctx context.Context) {
|
||||
AgentID: &m.workspaceAgent.ID,
|
||||
})
|
||||
}
|
||||
|
||||
ctx, err := dbauthz.WithWorkspaceRBAC(ctx, m.workspace.RBACObject())
|
||||
if err != nil {
|
||||
// Don't error level log here, will exit the function. We want to fall back to GetWorkspaceByAgentID.
|
||||
//nolint:gocritic
|
||||
m.logger.Debug(ctx, "Cached workspace was present but RBAC object was invalid", slog.F("err", err))
|
||||
}
|
||||
err = checkBuildIsLatest(ctx, m.db, m.workspaceBuild)
|
||||
if err != nil {
|
||||
reason = err.Error()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
@@ -30,12 +31,16 @@ func WriteWorkspaceApp404(log slog.Logger, accessURL *url.URL, rw http.ResponseW
|
||||
}
|
||||
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusNotFound,
|
||||
Title: "Application Not Found",
|
||||
Description: "The application or workspace you are trying to access does not exist or you do not have permission to access it.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: accessURL.String(),
|
||||
Warnings: warnings,
|
||||
Status: http.StatusNotFound,
|
||||
Title: "Application Not Found",
|
||||
Description: "The application or workspace you are trying to access does not exist or you do not have permission to access it.",
|
||||
Warnings: warnings,
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: accessURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,11 +65,15 @@ func WriteWorkspaceApp500(log slog.Logger, accessURL *url.URL, rw http.ResponseW
|
||||
)
|
||||
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "An internal server error occurred.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: accessURL.String(),
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "An internal server error occurred.",
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: accessURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,11 +94,18 @@ func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.Respo
|
||||
}
|
||||
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadGateway,
|
||||
Title: "Application Unavailable",
|
||||
Description: msg,
|
||||
RetryEnabled: true,
|
||||
DashboardURL: accessURL.String(),
|
||||
Status: http.StatusBadGateway,
|
||||
Title: "Application Unavailable",
|
||||
Description: msg,
|
||||
Actions: []site.Action{
|
||||
{
|
||||
Text: "Retry",
|
||||
},
|
||||
{
|
||||
URL: accessURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,11 +125,26 @@ func WriteWorkspaceOffline(log slog.Logger, accessURL *url.URL, rw http.Response
|
||||
)
|
||||
}
|
||||
|
||||
actions := []site.Action{
|
||||
{
|
||||
URL: accessURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
}
|
||||
|
||||
workspaceURL, err := url.Parse(accessURL.String())
|
||||
if err == nil {
|
||||
workspaceURL.Path = path.Join(accessURL.Path, "@"+appReq.UsernameOrID, appReq.WorkspaceNameOrID)
|
||||
actions = append(actions, site.Action{
|
||||
URL: workspaceURL.String(),
|
||||
Text: "View workspace",
|
||||
})
|
||||
}
|
||||
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Workspace Offline",
|
||||
Description: fmt.Sprintf("Last workspace transition was to the %q state. Start the workspace to access its applications.", codersdk.WorkspaceTransitionStop),
|
||||
RetryEnabled: false,
|
||||
DashboardURL: accessURL.String(),
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Workspace Offline",
|
||||
Description: fmt.Sprintf("Last workspace transition was to the %q state. Start the workspace to access its applications.", codersdk.WorkspaceTransitionStop),
|
||||
Actions: actions,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -185,10 +185,14 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Bad Request",
|
||||
Description: "Could not decrypt API key. Workspace app API key smuggling is not permitted on the primary access URL. Please remove the query parameter and try again.",
|
||||
// Retry is disabled because the user needs to remove the query
|
||||
// No retry is included because the user needs to remove the query
|
||||
// parameter before they try again.
|
||||
RetryEnabled: false,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: s.DashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -204,10 +208,14 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Bad Request",
|
||||
Description: "Could not decrypt API key. Please remove the query parameter and try again.",
|
||||
// Retry is disabled because the user needs to remove the query
|
||||
// No retry is included because the user needs to remove the query
|
||||
// parameter before they try again.
|
||||
RetryEnabled: false,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: s.DashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -224,11 +232,15 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
|
||||
// startup, but we'll check anyways.
|
||||
s.Logger.Error(r.Context(), "could not split invalid app hostname", slog.F("hostname", s.Hostname))
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "The app is configured with an invalid app wildcard hostname. Please contact an administrator.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
Status: http.StatusInternalServerError,
|
||||
Title: "Internal Server Error",
|
||||
Description: "The app is configured with an invalid app wildcard hostname. Please contact an administrator.",
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: s.DashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -274,11 +286,15 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
|
||||
func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) {
|
||||
if s.DisablePathApps {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusForbidden,
|
||||
Title: "Forbidden",
|
||||
Description: "Path-based applications are disabled on this Coder deployment by the administrator.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
Status: http.StatusForbidden,
|
||||
Title: "Forbidden",
|
||||
Description: "Path-based applications are disabled on this Coder deployment by the administrator.",
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: s.DashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -287,11 +303,15 @@ func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
||||
// lookup the username from token. We used to redirect by doing this lookup.
|
||||
if chi.URLParam(r, "user") == codersdk.Me {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusNotFound,
|
||||
Title: "Application Not Found",
|
||||
Description: "Applications must be accessed with the full username, not @me.",
|
||||
RetryEnabled: false,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
Status: http.StatusNotFound,
|
||||
Title: "Application Not Found",
|
||||
Description: "Applications must be accessed with the full username, not @me.",
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: s.DashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -519,11 +539,15 @@ func (s *Server) parseHostname(rw http.ResponseWriter, r *http.Request, next htt
|
||||
app, err := appurl.ParseSubdomainAppURL(subdomain)
|
||||
if err != nil {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Invalid Application URL",
|
||||
Description: fmt.Sprintf("Could not parse subdomain application URL %q: %s", subdomain, err.Error()),
|
||||
RetryEnabled: false,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Invalid Application URL",
|
||||
Description: fmt.Sprintf("Could not parse subdomain application URL %q: %s", subdomain, err.Error()),
|
||||
Actions: []site.Action{
|
||||
{
|
||||
URL: s.DashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return appurl.ApplicationURL{}, false
|
||||
}
|
||||
@@ -547,11 +571,18 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT
|
||||
appURL, err := url.Parse(appToken.AppURL)
|
||||
if err != nil {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Bad Request",
|
||||
Description: fmt.Sprintf("Application has an invalid URL %q: %s", appToken.AppURL, err.Error()),
|
||||
RetryEnabled: true,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
Status: http.StatusBadRequest,
|
||||
Title: "Bad Request",
|
||||
Description: fmt.Sprintf("Application has an invalid URL %q: %s", appToken.AppURL, err.Error()),
|
||||
Actions: []site.Action{
|
||||
{
|
||||
Text: "Retry",
|
||||
},
|
||||
{
|
||||
URL: s.DashboardURL.String(),
|
||||
Text: "Back to site",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2598,6 +2598,13 @@ func convertWorkspace(
|
||||
failingAgents := []uuid.UUID{}
|
||||
for _, resource := range workspaceBuild.Resources {
|
||||
for _, agent := range resource.Agents {
|
||||
// Sub-agents (e.g., devcontainer agents) are excluded from the
|
||||
// workspace health calculation. Their health is managed by
|
||||
// their parent agent, and temporary disconnections during
|
||||
// devcontainer rebuilds should not affect workspace health.
|
||||
if agent.ParentID.Valid {
|
||||
continue
|
||||
}
|
||||
if !agent.Health.Healthy {
|
||||
failingAgents = append(failingAgents, agent.ID)
|
||||
}
|
||||
|
||||
@@ -346,6 +346,81 @@ func TestWorkspace(t *testing.T) {
|
||||
assert.False(t, agent2.Health.Healthy)
|
||||
assert.NotEmpty(t, agent2.Health.Reason)
|
||||
})
|
||||
|
||||
t.Run("Sub-agent excluded", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// This test verifies that sub-agents (e.g., devcontainer agents)
|
||||
// are excluded from the workspace health calculation. When a
|
||||
// devcontainer is rebuilding, the sub-agent may be temporarily
|
||||
// disconnected, but this should not make the workspace unhealthy.
|
||||
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionApply: []*proto.Response{{
|
||||
Type: &proto.Response_Apply{
|
||||
Apply: &proto.ApplyComplete{
|
||||
Resources: []*proto.Resource{{
|
||||
Name: "some",
|
||||
Type: "example",
|
||||
Agents: []*proto.Agent{{
|
||||
Id: uuid.NewString(),
|
||||
Name: "parent",
|
||||
Auth: &proto.Agent_Token{},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Get the workspace and parent agent.
|
||||
workspace, err := client.Workspace(ctx, workspace.ID)
|
||||
require.NoError(t, err)
|
||||
parentAgent := workspace.LatestBuild.Resources[0].Agents[0]
|
||||
require.True(t, parentAgent.Health.Healthy, "parent agent should be healthy initially")
|
||||
|
||||
// Create a sub-agent with a short connection timeout so it becomes
|
||||
// unhealthy quickly (simulating a devcontainer rebuild scenario).
|
||||
subAgent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ParentID: uuid.NullUUID{Valid: true, UUID: parentAgent.ID},
|
||||
ResourceID: parentAgent.ResourceID,
|
||||
Name: "subagent",
|
||||
ConnectionTimeoutSeconds: 1,
|
||||
})
|
||||
|
||||
// Wait for the sub-agent to become unhealthy due to timeout.
|
||||
var subAgentUnhealthy bool
|
||||
require.Eventually(t, func() bool {
|
||||
workspace, err = client.Workspace(ctx, workspace.ID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, res := range workspace.LatestBuild.Resources {
|
||||
for _, agent := range res.Agents {
|
||||
if agent.ID == subAgent.ID && !agent.Health.Healthy {
|
||||
subAgentUnhealthy = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, testutil.WaitShort, testutil.IntervalFast, "sub-agent should become unhealthy")
|
||||
|
||||
require.True(t, subAgentUnhealthy, "sub-agent should be unhealthy")
|
||||
|
||||
// Verify that the workspace is still healthy because sub-agents
|
||||
// are excluded from the health calculation.
|
||||
assert.True(t, workspace.Health.Healthy, "workspace should be healthy despite unhealthy sub-agent")
|
||||
assert.Empty(t, workspace.Health.FailingAgents, "failing agents should not include sub-agent")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Archived", func(t *testing.T) {
|
||||
@@ -5165,6 +5240,79 @@ func TestDeleteWorkspaceACL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// nolint:tparallel,paralleltest // Subtests modify package global.
|
||||
func TestWorkspaceSharingDisabled(t *testing.T) {
|
||||
t.Run("CanAccessWhenEnabled", func(t *testing.T) {
|
||||
var (
|
||||
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
|
||||
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)}
|
||||
// DisableWorkspaceSharing is false (default)
|
||||
}),
|
||||
})
|
||||
admin = coderdtest.CreateFirstUser(t, client)
|
||||
_, wsOwner = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
userClient, user = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
// Create workspace with ACL granting access to user
|
||||
ws := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OwnerID: wsOwner.ID,
|
||||
OrganizationID: admin.OrganizationID,
|
||||
UserACL: database.WorkspaceACL{
|
||||
user.ID.String(): database.WorkspaceACLEntry{
|
||||
Permissions: []policy.Action{
|
||||
policy.ActionRead, policy.ActionSSH, policy.ActionApplicationConnect,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Do().Workspace
|
||||
|
||||
// User SHOULD be able to access workspace when sharing is enabled
|
||||
fetchedWs, err := userClient.Workspace(ctx, ws.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ws.ID, fetchedWs.ID)
|
||||
})
|
||||
|
||||
t.Run("NoAccessWhenDisabled", func(t *testing.T) {
|
||||
var (
|
||||
client, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
|
||||
dv.Experiments = []string{string(codersdk.ExperimentWorkspaceSharing)}
|
||||
dv.DisableWorkspaceSharing = true
|
||||
}),
|
||||
})
|
||||
admin = coderdtest.CreateFirstUser(t, client)
|
||||
_, wsOwner = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
userClient, user = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||
)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
// Create workspace with ACL granting access to user directly in DB
|
||||
ws := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||
OwnerID: wsOwner.ID,
|
||||
OrganizationID: admin.OrganizationID,
|
||||
UserACL: database.WorkspaceACL{
|
||||
user.ID.String(): database.WorkspaceACLEntry{
|
||||
Permissions: []policy.Action{
|
||||
policy.ActionRead, policy.ActionSSH, policy.ActionApplicationConnect,
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Do().Workspace
|
||||
|
||||
// User should NOT be able to access workspace when sharing is disabled
|
||||
_, err := userClient.Workspace(ctx, ws.ID)
|
||||
require.Error(t, err)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceCreateWithImplicitPreset(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
|
||||
// We test the below in multiple timezones specifically
|
||||
// chosen to trigger timezone-related bugs.
|
||||
timezones := []string{
|
||||
"Asia/Kolkata", // No DST, positive fractional offset
|
||||
"Canada/Newfoundland", // DST, negative fractional offset
|
||||
"Europe/Paris", // DST, positive offset
|
||||
"US/Arizona", // No DST, negative offset
|
||||
"UTC", // Baseline
|
||||
"Asia/Kolkata", // No DST, positive fractional offset
|
||||
"America/St_Johns", // DST, negative fractional offset
|
||||
"Europe/Paris", // DST, positive offset
|
||||
"US/Arizona", // No DST, negative offset
|
||||
"UTC", // Baseline
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
|
||||
@@ -93,7 +93,7 @@ type Builder struct {
|
||||
}
|
||||
|
||||
type UsageChecker interface {
|
||||
CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion) (UsageCheckResponse, error)
|
||||
CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion, transition database.WorkspaceTransition) (UsageCheckResponse, error)
|
||||
}
|
||||
|
||||
type UsageCheckResponse struct {
|
||||
@@ -105,7 +105,7 @@ type NoopUsageChecker struct{}
|
||||
|
||||
var _ UsageChecker = NoopUsageChecker{}
|
||||
|
||||
func (NoopUsageChecker) CheckBuildUsage(_ context.Context, _ database.Store, _ *database.TemplateVersion) (UsageCheckResponse, error) {
|
||||
func (NoopUsageChecker) CheckBuildUsage(_ context.Context, _ database.Store, _ *database.TemplateVersion, _ database.WorkspaceTransition) (UsageCheckResponse, error) {
|
||||
return UsageCheckResponse{
|
||||
Permitted: true,
|
||||
}, nil
|
||||
@@ -1307,7 +1307,7 @@ func (b *Builder) checkUsage() error {
|
||||
return BuildError{http.StatusInternalServerError, "Failed to fetch template version", err}
|
||||
}
|
||||
|
||||
resp, err := b.usageChecker.CheckBuildUsage(b.ctx, b.store, templateVersion)
|
||||
resp, err := b.usageChecker.CheckBuildUsage(b.ctx, b.store, templateVersion, b.trans)
|
||||
if err != nil {
|
||||
return BuildError{http.StatusInternalServerError, "Failed to check build usage", err}
|
||||
}
|
||||
|
||||
@@ -1049,7 +1049,7 @@ func TestWorkspaceBuildUsageChecker(t *testing.T) {
|
||||
|
||||
var calls int64
|
||||
fakeUsageChecker := &fakeUsageChecker{
|
||||
checkBuildUsageFunc: func(_ context.Context, _ database.Store, templateVersion *database.TemplateVersion) (wsbuilder.UsageCheckResponse, error) {
|
||||
checkBuildUsageFunc: func(_ context.Context, _ database.Store, templateVersion *database.TemplateVersion, _ database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error) {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
return wsbuilder.UsageCheckResponse{Permitted: true}, nil
|
||||
},
|
||||
@@ -1126,7 +1126,7 @@ func TestWorkspaceBuildUsageChecker(t *testing.T) {
|
||||
|
||||
var calls int64
|
||||
fakeUsageChecker := &fakeUsageChecker{
|
||||
checkBuildUsageFunc: func(_ context.Context, _ database.Store, templateVersion *database.TemplateVersion) (wsbuilder.UsageCheckResponse, error) {
|
||||
checkBuildUsageFunc: func(_ context.Context, _ database.Store, templateVersion *database.TemplateVersion, _ database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error) {
|
||||
atomic.AddInt64(&calls, 1)
|
||||
return c.response, c.responseErr
|
||||
},
|
||||
@@ -1577,11 +1577,11 @@ func expectFindMatchingPresetID(id uuid.UUID, err error) func(mTx *dbmock.MockSt
|
||||
}
|
||||
|
||||
type fakeUsageChecker struct {
|
||||
checkBuildUsageFunc func(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion) (wsbuilder.UsageCheckResponse, error)
|
||||
checkBuildUsageFunc func(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion, transition database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error)
|
||||
}
|
||||
|
||||
func (f *fakeUsageChecker) CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion) (wsbuilder.UsageCheckResponse, error) {
|
||||
return f.checkBuildUsageFunc(ctx, store, templateVersion)
|
||||
func (f *fakeUsageChecker) CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion, transition database.WorkspaceTransition) (wsbuilder.UsageCheckResponse, error) {
|
||||
return f.checkBuildUsageFunc(ctx, store, templateVersion, transition)
|
||||
}
|
||||
|
||||
func withNoTask(mTx *dbmock.MockStore) {
|
||||
|
||||
@@ -495,6 +495,7 @@ type DeploymentValues struct {
|
||||
SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"`
|
||||
WgtunnelHost serpent.String `json:"wgtunnel_host,omitempty" typescript:",notnull"`
|
||||
DisableOwnerWorkspaceExec serpent.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"`
|
||||
DisableWorkspaceSharing serpent.Bool `json:"disable_workspace_sharing,omitempty" typescript:",notnull"`
|
||||
ProxyHealthStatusInterval serpent.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"`
|
||||
EnableTerraformDebugMode serpent.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"`
|
||||
UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"`
|
||||
@@ -2728,6 +2729,15 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
|
||||
YAML: "disableOwnerWorkspaceAccess",
|
||||
Annotations: serpent.Annotations{}.Mark(annotationExternalProxies, "true"),
|
||||
},
|
||||
{
|
||||
Name: "Disable Workspace Sharing",
|
||||
Description: `Disable workspace sharing (requires the "workspace-sharing" experiment to be enabled). Workspace ACL checking is disabled and only owners can have ssh, apps and terminal access to workspaces. Access based on the 'owner' role is also allowed unless disabled via --disable-owner-workspace-access.`,
|
||||
Flag: "disable-workspace-sharing",
|
||||
Env: "CODER_DISABLE_WORKSPACE_SHARING",
|
||||
|
||||
Value: &c.DisableWorkspaceSharing,
|
||||
YAML: "disableWorkspaceSharing",
|
||||
},
|
||||
{
|
||||
Name: "Session Duration",
|
||||
Description: "The token expiry duration for browser sessions. Sessions may last longer if they are actively making requests, but this functionality can be disabled via --disable-session-expiry-refresh.",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Redirect old offline deployments URL to new airgap URL
|
||||
/install/offline /install/airgap 301
|
||||
|
||||
# Redirect old offline anchor fragments to new airgap anchors
|
||||
/install/offline#offline-docs /install/airgap#airgap-docs 301
|
||||
/install/offline#offline-container-images /install/airgap#airgap-container-images 301
|
||||
@@ -510,9 +510,9 @@ resource "kubernetes_pod" "workspace" {
|
||||
## Get help
|
||||
|
||||
- **Examples**: Review real-world examples from the [official Coder templates](https://registry.coder.com/contributors/coder?tab=templates):
|
||||
- [AWS EC2 (Devcontainer)](https://registry.coder.com/templates/aws-devcontainer) - AWS EC2 VMs with devcontainer support
|
||||
- [Docker (Devcontainer)](https://registry.coder.com/templates/docker-devcontainer) - Envbuilder containers with dev container support
|
||||
- [Kubernetes (Devcontainer)](https://registry.coder.com/templates/kubernetes-devcontainer) - Envbuilder pods on Kubernetes
|
||||
- [AWS EC2 (Devcontainer)](https://registry.coder.com/templates/aws-devcontainer) - AWS EC2 VMs with Envbuilder
|
||||
- [Docker (Devcontainer)](https://registry.coder.com/templates/docker-devcontainer) - Docker-in-Docker with Dev Containers integration
|
||||
- [Kubernetes (Devcontainer)](https://registry.coder.com/templates/kubernetes-devcontainer) - Kubernetes pods with Envbuilder
|
||||
- [Docker Containers](https://registry.coder.com/templates/docker) - Basic Docker container workspaces
|
||||
- [AWS EC2 (Linux)](https://registry.coder.com/templates/aws-linux) - AWS EC2 VMs for Linux development
|
||||
- [Google Compute Engine (Linux)](https://registry.coder.com/templates/gcp-vm-container) - GCP VM instances
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ For any information not strictly contained in these sections, check out our
|
||||
### Development containers (dev containers)
|
||||
|
||||
- A
|
||||
[Development Container](./templates/managing-templates/devcontainers/index.md)
|
||||
[Development Container](./integrations/devcontainers/index.md)
|
||||
is an open-source specification for defining development environments (called
|
||||
dev containers). It is generally stored in VCS alongside associated source
|
||||
code. It can reference an existing base image, or a custom Dockerfile that
|
||||
|
||||
+7
-8
@@ -1,10 +1,9 @@
|
||||
# Add a dev container template to Coder
|
||||
# Add an Envbuilder template
|
||||
|
||||
A Coder administrator adds a dev container-compatible template to Coder
|
||||
(Envbuilder). This allows the template to prompt for the developer for their dev
|
||||
container repository's URL as a
|
||||
[parameter](../../extending-templates/parameters.md) when they create their
|
||||
workspace. Envbuilder clones the repo and builds a container from the
|
||||
A Coder administrator adds an Envbuilder-compatible template to Coder. This
|
||||
allows the template to prompt the developer for their dev container repository's
|
||||
URL as a [parameter](../../../templates/extending-templates/parameters.md) when they create
|
||||
their workspace. Envbuilder clones the repo and builds a container from the
|
||||
`devcontainer.json` specified in the repo.
|
||||
|
||||
You can create template files through the Coder dashboard, CLI, or you can
|
||||
@@ -128,7 +127,7 @@ their development environments:
|
||||
| [AWS EC2 dev container](https://github.com/coder/coder/tree/main/examples/templates/aws-devcontainer) | Runs a development container inside a single EC2 instance. It also mounts the Docker socket from the VM inside the container to enable Docker inside the workspace. |
|
||||
|
||||
Your template can prompt the user for a repo URL with
|
||||
[parameters](../../extending-templates/parameters.md):
|
||||
[parameters](../../../templates/extending-templates/parameters.md):
|
||||
|
||||

|
||||
|
||||
@@ -143,4 +142,4 @@ Lifecycle scripts are managed by project developers.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Dev container security and caching](./devcontainer-security-caching.md)
|
||||
- [Envbuilder security and caching](./envbuilder-security-caching.md)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Dev container releases and known issues
|
||||
# Envbuilder releases and known issues
|
||||
|
||||
## Release channels
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
# Dev container security and caching
|
||||
# Envbuilder security and caching
|
||||
|
||||
Ensure Envbuilder can only pull pre-approved images and artifacts by configuring
|
||||
it with your existing HTTP proxies, firewalls, and artifact managers.
|
||||
@@ -26,7 +26,7 @@ of caching:
|
||||
|
||||
- Caches the entire image, skipping the build process completely (except for
|
||||
post-build
|
||||
[lifecycle scripts](./add-devcontainer.md#dev-container-lifecycle-scripts)).
|
||||
[lifecycle scripts](./add-envbuilder.md#dev-container-lifecycle-scripts)).
|
||||
|
||||
Note that caching requires push access to a registry, and may require approval
|
||||
from relevant infrastructure team(s).
|
||||
@@ -62,5 +62,5 @@ You may also wish to consult a
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Dev container releases and known issues](./devcontainer-releases-known-issues.md)
|
||||
- [Envbuilder releases and known issues](./envbuilder-releases-known-issues.md)
|
||||
- [Dotfiles](../../../../user-guides/workspace-dotfiles.md)
|
||||
@@ -0,0 +1,52 @@
|
||||
# Envbuilder
|
||||
|
||||
Envbuilder is an open-source tool that builds development environments from
|
||||
[dev container](https://containers.dev/implementors/spec/) configuration files.
|
||||
Unlike the [Dev Containers integration](../integration.md),
|
||||
Envbuilder transforms the workspace image itself rather than running containers
|
||||
inside the workspace.
|
||||
|
||||
Envbuilder is well-suited for Kubernetes-native deployments without privileged
|
||||
containers, environments where Docker is unavailable or restricted, and
|
||||
workflows where administrators need infrastructure-level control over image
|
||||
builds, caching, and security scanning. For workspaces with Docker available,
|
||||
the [Dev Containers Integration](../integration.md) offers container management
|
||||
with dashboard visibility and multi-container support.
|
||||
|
||||
Dev containers provide developers with increased autonomy and control over their
|
||||
Coder cloud development environments.
|
||||
|
||||
By using dev containers, developers can customize their workspaces with tools
|
||||
pre-approved by platform teams in registries like
|
||||
[JFrog Artifactory](../../jfrog-artifactory.md). This simplifies
|
||||
workflows, reduces the need for tickets and approvals, and promotes greater
|
||||
independence for developers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
An administrator should construct or choose a base image and create a template
|
||||
that includes a `devcontainer_builder` image before a developer team configures
|
||||
dev containers.
|
||||
|
||||
## Devcontainer Features
|
||||
|
||||
[Dev container Features](https://containers.dev/implementors/features/) allow
|
||||
owners of a project to specify self-contained units of code and runtime
|
||||
configuration that can be composed together on top of an existing base image.
|
||||
This is a good place to install project-specific tools, such as
|
||||
language-specific runtimes and compilers.
|
||||
|
||||
## Coder Envbuilder
|
||||
|
||||
[Envbuilder](https://github.com/coder/envbuilder/) is an open-source project
|
||||
maintained by Coder that runs dev containers via Coder templates and your
|
||||
underlying infrastructure. Envbuilder can run on Docker or Kubernetes.
|
||||
|
||||
It is independently packaged and versioned from the centralized Coder
|
||||
open-source project. This means that Envbuilder can be used with Coder, but it
|
||||
is not required. It also means that dev container builds can scale independently
|
||||
of the Coder control plane and even run within a CI/CD pipeline.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Add an Envbuilder template](./add-envbuilder.md)
|
||||
@@ -0,0 +1,49 @@
|
||||
# Dev Containers
|
||||
|
||||
Dev containers allow developers to define their development environment
|
||||
as code using the [Dev Container specification](https://containers.dev/).
|
||||
Configuration lives in a `devcontainer.json` file alongside source code,
|
||||
enabling consistent, reproducible environments.
|
||||
|
||||
By adopting dev containers, organizations can:
|
||||
|
||||
- **Standardize environments**: Eliminate "works on my machine" issues while
|
||||
still allowing developers to customize their tools within approved boundaries.
|
||||
- **Scale efficiently**: Let developers maintain their own environment
|
||||
definitions, reducing the burden on platform teams.
|
||||
- **Improve security**: Use hardened base images and controlled package
|
||||
registries to enforce security policies while enabling developer self-service.
|
||||
|
||||
Coder supports two approaches for running dev containers. Choose based on your
|
||||
infrastructure and workflow requirements.
|
||||
|
||||
## Dev Containers Integration
|
||||
|
||||
The Dev Containers Integration uses the standard `@devcontainers/cli` and Docker
|
||||
to run containers inside your workspace. This is the recommended approach for
|
||||
most use cases.
|
||||
|
||||
**Best for:**
|
||||
|
||||
- Workspaces with Docker available (Docker-in-Docker or mounted socket)
|
||||
- Dev container management in the Coder dashboard (discovery, status, rebuild)
|
||||
- Multiple dev containers per workspace
|
||||
|
||||
[Configure Dev Containers Integration](./integration.md)
|
||||
|
||||
For user documentation, see the
|
||||
[Dev Containers user guide](../../../user-guides/devcontainers/index.md).
|
||||
|
||||
## Envbuilder
|
||||
|
||||
Envbuilder transforms the workspace image itself from a `devcontainer.json`,
|
||||
rather than running containers inside the workspace. It does not require
|
||||
a Docker daemon.
|
||||
|
||||
**Best for:**
|
||||
|
||||
- Environments where Docker is unavailable or restricted
|
||||
- Infrastructure-level control over image builds, caching, and security scanning
|
||||
- Kubernetes-native deployments without privileged containers
|
||||
|
||||
[Configure Envbuilder](./envbuilder/index.md)
|
||||
@@ -0,0 +1,259 @@
|
||||
# Configure a template for Dev Containers
|
||||
|
||||
This guide covers the Dev Containers Integration, which uses Docker. For
|
||||
environments without Docker, see [Envbuilder](./envbuilder/index.md) as an
|
||||
alternative.
|
||||
|
||||
To enable Dev Containers in workspaces, configure your template with the Dev Containers
|
||||
modules and configurations outlined in this doc.
|
||||
|
||||
Dev Containers are currently not supported in Windows or macOS workspaces.
|
||||
|
||||
## Configuration Modes
|
||||
|
||||
There are two approaches to configuring Dev Containers in Coder:
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Use the [`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer) Terraform resource to explicitly define which Dev
|
||||
Containers should be started in your workspace. This approach provides:
|
||||
|
||||
- Predictable behavior and explicit control
|
||||
- Clear template configuration
|
||||
- Easier troubleshooting
|
||||
- Better for production environments
|
||||
|
||||
This is the recommended approach for most use cases.
|
||||
|
||||
### Project Discovery
|
||||
|
||||
Alternatively, enable automatic discovery of Dev Containers in Git repositories.
|
||||
The agent scans for `devcontainer.json` files and surfaces them in the Coder UI.
|
||||
See [Environment Variables](#environment-variables) for configuration options.
|
||||
|
||||
This approach is useful when developers frequently switch between repositories
|
||||
or work with many projects, as it reduces template maintenance overhead.
|
||||
|
||||
## Install the Dev Containers CLI
|
||||
|
||||
Use the
|
||||
[devcontainers-cli](https://registry.coder.com/modules/devcontainers-cli) module
|
||||
to ensure the `@devcontainers/cli` is installed in your workspace:
|
||||
|
||||
```terraform
|
||||
module "devcontainers-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/devcontainers-cli/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, install the devcontainer CLI manually in your base image.
|
||||
|
||||
## Configure Automatic Dev Container Startup
|
||||
|
||||
The
|
||||
[`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer)
|
||||
resource automatically starts a Dev Container in your workspace, ensuring it's
|
||||
ready when you access the workspace:
|
||||
|
||||
```terraform
|
||||
resource "coder_devcontainer" "my-repository" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = coder_agent.dev.id
|
||||
workspace_folder = "/home/coder/my-repository"
|
||||
}
|
||||
```
|
||||
|
||||
The `workspace_folder` attribute must point to a valid project folder containing
|
||||
a `devcontainer.json` file. Consider using the
|
||||
[`git-clone`](https://registry.coder.com/modules/git-clone) module to ensure
|
||||
your repository is cloned and ready for automatic startup.
|
||||
|
||||
For multi-repo workspaces, define multiple `coder_devcontainer` resources, each
|
||||
pointing to a different repository. Each one runs as a separate sub-agent with
|
||||
its own terminal and apps in the dashboard.
|
||||
|
||||
## Enable Dev Containers Integration
|
||||
|
||||
Dev Containers integration is **enabled by default** in Coder 2.24.0 and later.
|
||||
You don't need to set any environment variables unless you want to change the
|
||||
default behavior.
|
||||
|
||||
If you need to explicitly disable Dev Containers, set the
|
||||
`CODER_AGENT_DEVCONTAINERS_ENABLE` environment variable to `false`:
|
||||
|
||||
```terraform
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = "codercom/oss-dogfood:latest"
|
||||
env = [
|
||||
"CODER_AGENT_DEVCONTAINERS_ENABLE=false", # Explicitly disable
|
||||
# ... Other environment variables.
|
||||
]
|
||||
# ... Other container configuration.
|
||||
}
|
||||
```
|
||||
|
||||
See the [Environment Variables](#environment-variables) section below for more
|
||||
details on available configuration options.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables control Dev Container behavior in your
|
||||
workspace. Both `CODER_AGENT_DEVCONTAINERS_ENABLE` and
|
||||
`CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE` are **enabled by default**,
|
||||
so you typically don't need to set them unless you want to explicitly disable
|
||||
the feature.
|
||||
|
||||
### CODER_AGENT_DEVCONTAINERS_ENABLE
|
||||
|
||||
**Default: `true`** • **Added in: v2.24.0**
|
||||
|
||||
Enables the Dev Containers integration in the Coder agent.
|
||||
|
||||
The Dev Containers feature is enabled by default. You can explicitly disable it
|
||||
by setting this to `false`.
|
||||
|
||||
### CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE
|
||||
|
||||
**Default: `true`** • **Added in: v2.25.0**
|
||||
|
||||
Enables automatic discovery of Dev Containers in Git repositories.
|
||||
|
||||
When enabled, the agent scans the configured working directory (set via the
|
||||
`directory` attribute in `coder_agent`, typically the user's home directory) for
|
||||
Git repositories. If the directory itself is a Git repository, it searches that
|
||||
project. Otherwise, it searches immediate subdirectories for Git repositories.
|
||||
|
||||
For each repository found, the agent looks for `devcontainer.json` files in the
|
||||
[standard locations](../../../user-guides/devcontainers/index.md#add-a-devcontainerjson)
|
||||
and surfaces discovered Dev Containers in the Coder UI. Discovery respects
|
||||
`.gitignore` patterns.
|
||||
|
||||
Set to `false` if you prefer explicit configuration via `coder_devcontainer`.
|
||||
|
||||
### CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE
|
||||
|
||||
**Default: `false`** • **Added in: v2.25.0**
|
||||
|
||||
Automatically starts Dev Containers discovered via project discovery.
|
||||
|
||||
When enabled, discovered Dev Containers will be automatically built and started
|
||||
during workspace initialization. This only applies to Dev Containers found via
|
||||
project discovery. Dev Containers defined with the `coder_devcontainer` resource
|
||||
always auto-start regardless of this setting.
|
||||
|
||||
## Per-Container Customizations
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Dev container sub-agents are created dynamically after workspace provisioning,
|
||||
> so Terraform resources like
|
||||
> [`coder_script`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script)
|
||||
> and [`coder_app`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app)
|
||||
> cannot currently be attached to them. Modules from the
|
||||
> [Coder registry](https://registry.coder.com) that depend on these resources
|
||||
> are also not currently supported for sub-agents.
|
||||
>
|
||||
> To add tools to dev containers, use
|
||||
> [dev container features](../../../user-guides/devcontainers/working-with-dev-containers.md#dev-container-features).
|
||||
> For Coder-specific apps, use the
|
||||
> [`apps` customization](../../../user-guides/devcontainers/customizing-dev-containers.md#custom-apps).
|
||||
|
||||
Developers can customize individual dev containers using the `customizations.coder`
|
||||
block in their `devcontainer.json` file. Available options include:
|
||||
|
||||
- `ignore` — Hide a dev container from Coder completely
|
||||
- `autoStart` — Control whether the container starts automatically (requires
|
||||
`CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE` to be enabled)
|
||||
- `name` — Set a custom agent name
|
||||
- `displayApps` — Control which built-in apps appear
|
||||
- `apps` — Define custom applications
|
||||
|
||||
For the full reference, see
|
||||
[Customizing dev containers](../../../user-guides/devcontainers/customizing-dev-containers.md).
|
||||
|
||||
## Complete Template Example
|
||||
|
||||
Here's a simplified template example that uses Dev Containers with manual
|
||||
configuration:
|
||||
|
||||
```terraform
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = { source = "coder/coder" }
|
||||
docker = { source = "kreuzwerker/docker" }
|
||||
}
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "dev" {
|
||||
arch = "amd64"
|
||||
os = "linux"
|
||||
startup_script_behavior = "blocking"
|
||||
startup_script = "sudo service docker start"
|
||||
shutdown_script = "sudo service docker stop"
|
||||
# ...
|
||||
}
|
||||
|
||||
module "devcontainers-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/devcontainers-cli/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
resource "coder_devcontainer" "my-repository" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = coder_agent.dev.id
|
||||
workspace_folder = "/home/coder/my-repository"
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative: Project Discovery with Autostart
|
||||
|
||||
By default, discovered containers appear in the dashboard but developers must
|
||||
manually start them. To have them start automatically, enable autostart:
|
||||
|
||||
```terraform
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = "codercom/oss-dogfood:latest"
|
||||
env = [
|
||||
# Project discovery is enabled by default, but autostart is not.
|
||||
# Enable autostart to automatically build and start discovered containers:
|
||||
"CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE=true",
|
||||
# ... Other environment variables.
|
||||
]
|
||||
# ... Other container configuration.
|
||||
}
|
||||
```
|
||||
|
||||
With autostart enabled:
|
||||
|
||||
- Discovered containers automatically build and start during workspace
|
||||
initialization
|
||||
- The `coder_devcontainer` resource is not required
|
||||
- Developers can work with multiple projects seamlessly
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> When using project discovery, you still need to install the devcontainers CLI
|
||||
> using the module or in your base image.
|
||||
|
||||
## Example Template
|
||||
|
||||
The [Docker (Dev Containers)](https://github.com/coder/coder/tree/main/examples/templates/docker-devcontainer)
|
||||
starter template demonstrates Dev Containers integration using Docker-in-Docker.
|
||||
It includes the `devcontainers-cli` module, `git-clone` module, and the
|
||||
`coder_devcontainer` resource.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Dev Containers Integration](../../../user-guides/devcontainers/index.md)
|
||||
- [Customizing Dev Containers](../../../user-guides/devcontainers/customizing-dev-containers.md)
|
||||
- [Working with Dev Containers](../../../user-guides/devcontainers/working-with-dev-containers.md)
|
||||
- [Troubleshooting Dev Containers](../../../user-guides/devcontainers/troubleshooting-dev-containers.md)
|
||||
@@ -1,280 +1,14 @@
|
||||
# Configure a template for Dev Containers
|
||||
# Dev Containers
|
||||
|
||||
To enable Dev Containers in workspaces, configure your template with the Dev Containers
|
||||
modules and configurations outlined in this doc.
|
||||
Dev containers extend your template with containerized development environments,
|
||||
allowing developers to work in consistent, reproducible setups defined by
|
||||
`devcontainer.json` files.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Dev Containers require a **Linux or macOS workspace**. Windows is not supported.
|
||||
Coder's Dev Containers Integration uses the standard `@devcontainers/cli` and
|
||||
Docker to run containers inside workspaces.
|
||||
|
||||
## Configuration Modes
|
||||
For setup instructions, see
|
||||
[Dev Containers Integration](../../integrations/devcontainers/integration.md).
|
||||
|
||||
There are two approaches to configuring Dev Containers in Coder:
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Use the [`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer) Terraform resource to explicitly define which Dev
|
||||
Containers should be started in your workspace. This approach provides:
|
||||
|
||||
- Predictable behavior and explicit control
|
||||
- Clear template configuration
|
||||
- Easier troubleshooting
|
||||
- Better for production environments
|
||||
|
||||
This is the recommended approach for most use cases.
|
||||
|
||||
### Project Discovery
|
||||
|
||||
Enable automatic discovery of Dev Containers in Git repositories. Project discovery automatically scans Git repositories for `.devcontainer/devcontainer.json` or `.devcontainer.json` files and surfaces them in the Coder UI. See the [Environment Variables](#environment-variables) section for detailed configuration options.
|
||||
|
||||
## Install the Dev Containers CLI
|
||||
|
||||
Use the
|
||||
[devcontainers-cli](https://registry.coder.com/modules/devcontainers-cli) module
|
||||
to ensure the `@devcontainers/cli` is installed in your workspace:
|
||||
|
||||
```terraform
|
||||
module "devcontainers-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/modules/devcontainers-cli/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, install the devcontainer CLI manually in your base image.
|
||||
|
||||
## Configure Automatic Dev Container Startup
|
||||
|
||||
The
|
||||
[`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer)
|
||||
resource automatically starts a Dev Container in your workspace, ensuring it's
|
||||
ready when you access the workspace:
|
||||
|
||||
```terraform
|
||||
resource "coder_devcontainer" "my-repository" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = coder_agent.dev.id
|
||||
workspace_folder = "/home/coder/my-repository"
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The `workspace_folder` attribute must specify the location of the dev
|
||||
> container's workspace and should point to a valid project folder containing a
|
||||
> `devcontainer.json` file.
|
||||
|
||||
<!-- nolint:MD028/no-blanks-blockquote -->
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Consider using the [`git-clone`](https://registry.coder.com/modules/git-clone)
|
||||
> module to ensure your repository is cloned into the workspace folder and ready
|
||||
> for automatic startup.
|
||||
|
||||
## Enable Dev Containers Integration
|
||||
|
||||
Dev Containers integration is **enabled by default** in Coder 2.24.0 and later.
|
||||
You don't need to set any environment variables unless you want to change the
|
||||
default behavior.
|
||||
|
||||
If you need to explicitly disable Dev Containers, set the
|
||||
`CODER_AGENT_DEVCONTAINERS_ENABLE` environment variable to `false`:
|
||||
|
||||
```terraform
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = "codercom/oss-dogfood:latest"
|
||||
env = [
|
||||
"CODER_AGENT_DEVCONTAINERS_ENABLE=false", # Explicitly disable
|
||||
# ... Other environment variables.
|
||||
]
|
||||
# ... Other container configuration.
|
||||
}
|
||||
```
|
||||
|
||||
See the [Environment Variables](#environment-variables) section below for more
|
||||
details on available configuration options.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables control Dev Container behavior in your
|
||||
workspace. Both `CODER_AGENT_DEVCONTAINERS_ENABLE` and
|
||||
`CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE` are **enabled by default**,
|
||||
so you typically don't need to set them unless you want to explicitly disable
|
||||
the feature.
|
||||
|
||||
### CODER_AGENT_DEVCONTAINERS_ENABLE
|
||||
|
||||
**Default: `true`** • **Added in: v2.24.0**
|
||||
|
||||
Enables the Dev Containers integration in the Coder agent.
|
||||
|
||||
The Dev Containers feature is enabled by default. You can explicitly disable it
|
||||
by setting this to `false`.
|
||||
|
||||
### CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE
|
||||
|
||||
**Default: `true`** • **Added in: v2.25.0**
|
||||
|
||||
Enables automatic discovery of Dev Containers in Git repositories.
|
||||
|
||||
When enabled, the agent will:
|
||||
|
||||
- Scan the agent directory for Git repositories
|
||||
- Look for `.devcontainer/devcontainer.json` or `.devcontainer.json` files
|
||||
- Surface discovered Dev Containers automatically in the Coder UI
|
||||
- Respect `.gitignore` patterns during discovery
|
||||
|
||||
You can disable automatic discovery by setting this to `false` if you prefer to
|
||||
use only the `coder_devcontainer` resource for explicit configuration.
|
||||
|
||||
### CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE
|
||||
|
||||
**Default: `false`** • **Added in: v2.25.0**
|
||||
|
||||
Automatically starts Dev Containers discovered via project discovery.
|
||||
|
||||
When enabled, discovered Dev Containers will be automatically built and started
|
||||
during workspace initialization. This only applies to Dev Containers found via
|
||||
project discovery. Dev Containers defined with the `coder_devcontainer` resource
|
||||
always auto-start regardless of this setting.
|
||||
|
||||
## Per-Container Customizations
|
||||
|
||||
Individual Dev Containers can be customized using the `customizations.coder` block
|
||||
in your `devcontainer.json` file. These customizations allow you to control
|
||||
container-specific behavior without modifying your template.
|
||||
|
||||
### Ignore Specific Containers
|
||||
|
||||
Use the `ignore` option to hide a Dev Container from Coder completely:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"ignore": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `ignore` is set to `true`:
|
||||
|
||||
- The Dev Container won't appear in the Coder UI
|
||||
- Coder won't manage or monitor the container
|
||||
|
||||
This is useful when you have Dev Containers in your repository that you don't
|
||||
want Coder to manage.
|
||||
|
||||
### Per-Container Auto-Start
|
||||
|
||||
Control whether individual Dev Containers should auto-start using the
|
||||
`autoStart` option:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"autoStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: The `autoStart` option only applies when global auto-start is
|
||||
enabled via `CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE=true`. If
|
||||
the global setting is disabled, containers won't auto-start regardless of this
|
||||
setting.
|
||||
|
||||
When `autoStart` is set to `true`:
|
||||
|
||||
- The Dev Container automatically builds and starts during workspace
|
||||
initialization
|
||||
- Works on a per-container basis (you can enable it for some containers but not
|
||||
others)
|
||||
|
||||
When `autoStart` is set to `false` or omitted:
|
||||
|
||||
- The Dev Container is discovered and shown in the UI
|
||||
- Users must manually start it via the UI
|
||||
|
||||
## Complete Template Example
|
||||
|
||||
Here's a simplified template example that uses Dev Containers with manual
|
||||
configuration:
|
||||
|
||||
```terraform
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = { source = "coder/coder" }
|
||||
docker = { source = "kreuzwerker/docker" }
|
||||
}
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "dev" {
|
||||
arch = "amd64"
|
||||
os = "linux"
|
||||
startup_script_behavior = "blocking"
|
||||
startup_script = "sudo service docker start"
|
||||
shutdown_script = "sudo service docker stop"
|
||||
# ...
|
||||
}
|
||||
|
||||
module "devcontainers-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/modules/devcontainers-cli/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
resource "coder_devcontainer" "my-repository" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
agent_id = coder_agent.dev.id
|
||||
workspace_folder = "/home/coder/my-repository"
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative: Project Discovery Mode
|
||||
|
||||
You can enable automatic starting of discovered Dev Containers:
|
||||
|
||||
```terraform
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = "codercom/oss-dogfood:latest"
|
||||
env = [
|
||||
# Project discovery is enabled by default, but autostart is not.
|
||||
# Enable autostart to automatically build and start discovered containers:
|
||||
"CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE=true",
|
||||
# ... Other environment variables.
|
||||
]
|
||||
# ... Other container configuration.
|
||||
}
|
||||
```
|
||||
|
||||
With this configuration:
|
||||
|
||||
- Project discovery is enabled (default behavior)
|
||||
- Discovered containers are automatically started (via the env var)
|
||||
- The `coder_devcontainer` resource is **not** required
|
||||
- Developers can work with multiple projects seamlessly
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> When using project discovery, you still need to install the devcontainers CLI
|
||||
> using the module or in your base image.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Dev Containers Integration](../../../user-guides/devcontainers/index.md)
|
||||
- [Working with Dev Containers](../../../user-guides/devcontainers/working-with-dev-containers.md)
|
||||
- [Troubleshooting Dev Containers](../../../user-guides/devcontainers/troubleshooting-dev-containers.md)
|
||||
For an alternative approach that doesn't require Docker, see
|
||||
[Envbuilder](../../integrations/devcontainers/envbuilder/index.md).
|
||||
|
||||
@@ -48,11 +48,10 @@ needs of different teams.
|
||||
|
||||
- [Image management](./managing-templates/image-management.md): Learn how to
|
||||
create and publish images for use within Coder workspaces & templates.
|
||||
- [Dev Container support](./managing-templates/devcontainers/index.md): Enable
|
||||
dev containers to allow teams to bring their own tools into Coder workspaces.
|
||||
- [Early Access Dev Containers](../../user-guides/devcontainers/index.md): Try our
|
||||
new direct devcontainers integration (distinct from Envbuilder-based
|
||||
approach).
|
||||
- [Dev Containers integration](../integrations/devcontainers/integration.md): Enable
|
||||
native dev containers support using `@devcontainers/cli` and Docker.
|
||||
- [Envbuilder](../integrations/devcontainers/envbuilder/index.md): Alternative approach
|
||||
for environments without Docker access.
|
||||
- [Template hardening](./extending-templates/resource-persistence.md#-bulletproofing):
|
||||
Configure your template to prevent certain resources from being destroyed
|
||||
(e.g. user disks).
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
# Dev containers
|
||||
|
||||
A Development Container is an
|
||||
[open-source specification](https://containers.dev/implementors/spec/) for
|
||||
defining containerized development environments which are also called
|
||||
development containers (dev containers).
|
||||
|
||||
Dev containers provide developers with increased autonomy and control over their
|
||||
Coder cloud development environments.
|
||||
|
||||
By using dev containers, developers can customize their workspaces with tools
|
||||
pre-approved by platform teams in registries like
|
||||
[JFrog Artifactory](../../../integrations/jfrog-artifactory.md). This simplifies
|
||||
workflows, reduces the need for tickets and approvals, and promotes greater
|
||||
independence for developers.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
An administrator should construct or choose a base image and create a template
|
||||
that includes a `devcontainer_builder` image before a developer team configures
|
||||
dev containers.
|
||||
|
||||
## Benefits of devcontainers
|
||||
|
||||
There are several benefits to adding a dev container-compatible template to
|
||||
Coder:
|
||||
|
||||
- Reliability through standardization
|
||||
- Scalability for growing teams
|
||||
- Improved security
|
||||
- Performance efficiency
|
||||
- Cost Optimization
|
||||
|
||||
### Reliability through standardization
|
||||
|
||||
Use dev containers to empower development teams to personalize their own
|
||||
environments while maintaining consistency and security through an approved and
|
||||
hardened base image.
|
||||
|
||||
Standardized environments ensure uniform behavior across machines and team
|
||||
members, eliminating "it works on my machine" issues and creating a stable
|
||||
foundation for development and testing. Containerized setups reduce dependency
|
||||
conflicts and misconfigurations, enhancing build stability.
|
||||
|
||||
### Scalability for growing teams
|
||||
|
||||
Dev containers allow organizations to handle multiple projects and teams
|
||||
efficiently.
|
||||
|
||||
You can leverage platforms like Kubernetes to allocate resources on demand,
|
||||
optimizing costs and ensuring fair distribution of quotas. Developer teams can
|
||||
use efficient custom images and independently configure the contents of their
|
||||
version-controlled dev containers.
|
||||
|
||||
This approach allows organizations to scale seamlessly, reducing the maintenance
|
||||
burden on the administrators that support diverse projects while allowing
|
||||
development teams to maintain their own images and onboard new users quickly.
|
||||
|
||||
### Improved security
|
||||
|
||||
Since Coder and Envbuilder run on your own infrastructure, you can use firewalls
|
||||
and cluster-level policies to ensure Envbuilder only downloads packages from
|
||||
your secure registry powered by JFrog Artifactory or Sonatype Nexus.
|
||||
Additionally, Envbuilder can be configured to push the full image back to your
|
||||
registry for additional security scanning.
|
||||
|
||||
This means that Coder admins can require hardened base images and packages,
|
||||
while still allowing developer self-service.
|
||||
|
||||
Envbuilder runs inside a small container image but does not require a Docker
|
||||
daemon in order to build a dev container. This is useful in environments where
|
||||
you may not have access to a Docker socket for security reasons, but still need
|
||||
to work with a container.
|
||||
|
||||
### Performance efficiency
|
||||
|
||||
Create a unique image for each project to reduce the dependency size of any
|
||||
given project.
|
||||
|
||||
Envbuilder has various caching modes to ensure workspaces start as fast as
|
||||
possible, such as layer caching and even full image caching and fetching via the
|
||||
[Envbuilder Terraform provider](https://registry.terraform.io/providers/coder/envbuilder/latest/docs).
|
||||
|
||||
### Cost optimization
|
||||
|
||||
By creating unique images per-project, you remove unnecessary dependencies and
|
||||
reduce the workspace size and resource consumption of any given project. Full
|
||||
image caching ensures optimal start and stop times.
|
||||
|
||||
## When to use a dev container
|
||||
|
||||
Dev containers are a good fit for developer teams who are familiar with Docker
|
||||
and are already using containerized development environments. If you have a
|
||||
large number of projects with different toolchains, dependencies, or that depend
|
||||
on a particular Linux distribution, dev containers make it easier to quickly
|
||||
switch between projects.
|
||||
|
||||
They may also be a great fit for more restricted environments where you may not
|
||||
have access to a Docker daemon since it doesn't need one to work.
|
||||
|
||||
## Devcontainer Features
|
||||
|
||||
[Dev container Features](https://containers.dev/implementors/features/) allow
|
||||
owners of a project to specify self-contained units of code and runtime
|
||||
configuration that can be composed together on top of an existing base image.
|
||||
This is a good place to install project-specific tools, such as
|
||||
language-specific runtimes and compilers.
|
||||
|
||||
## Coder Envbuilder
|
||||
|
||||
[Envbuilder](https://github.com/coder/envbuilder/) is an open-source project
|
||||
maintained by Coder that runs dev containers via Coder templates and your
|
||||
underlying infrastructure. Envbuilder can run on Docker or Kubernetes.
|
||||
|
||||
It is independently packaged and versioned from the centralized Coder
|
||||
open-source project. This means that Envbuilder can be used with Coder, but it
|
||||
is not required. It also means that dev container builds can scale independently
|
||||
of the Coder control plane and even run within a CI/CD pipeline.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Add a dev container template](./add-devcontainer.md)
|
||||
@@ -0,0 +1,14 @@
|
||||
# Envbuilder
|
||||
|
||||
Envbuilder shifts environment definition from template administrators to
|
||||
developers. Instead of baking tools into template images, developers define
|
||||
their environments via `devcontainer.json` files in their repositories.
|
||||
|
||||
Envbuilder transforms the workspace image itself from a dev container
|
||||
configuration, without requiring a Docker daemon.
|
||||
|
||||
For setup instructions, see
|
||||
[Envbuilder documentation](../../integrations/devcontainers/envbuilder/index.md).
|
||||
|
||||
For an alternative that uses Docker inside workspaces, see
|
||||
[Dev Containers Integration](../../integrations/devcontainers/integration.md).
|
||||
@@ -70,4 +70,5 @@ specific tooling for their projects. The [Dev Container](https://containers.dev)
|
||||
specification allows developers to define their projects dependencies within a
|
||||
`devcontainer.json` in their Git repository.
|
||||
|
||||
- [Learn how to integrate Dev Containers with Coder](./devcontainers/index.md)
|
||||
- [Configure a template for Dev Containers](../../integrations/devcontainers/integration.md) (recommended)
|
||||
- [Learn about Envbuilder](../../integrations/devcontainers/envbuilder/index.md) (alternative for environments without Docker)
|
||||
|
||||
@@ -96,5 +96,6 @@ coder templates delete <template-name>
|
||||
## Next steps
|
||||
|
||||
- [Image management](./image-management.md)
|
||||
- [Devcontainer templates](./devcontainers/index.md)
|
||||
- [Dev Containers integration](../../integrations/devcontainers/integration.md) (recommended)
|
||||
- [Envbuilder](../../integrations/devcontainers/envbuilder/index.md) (alternative for environments without Docker)
|
||||
- [Change management](./change-management.md)
|
||||
|
||||
@@ -60,3 +60,65 @@ needs.
|
||||
|
||||
For configuration options and details, see [Data Retention](./setup.md#data-retention)
|
||||
in the AI Bridge setup guide.
|
||||
|
||||
## Tracing
|
||||
|
||||
AI Bridge supports tracing via [OpenTelemetry](https://opentelemetry.io/),
|
||||
providing visibility into request processing, upstream API calls, and MCP server
|
||||
interactions.
|
||||
|
||||
### Enabling Tracing
|
||||
|
||||
AI Bridge tracing is enabled when tracing is enabled for the Coder server.
|
||||
To enable tracing set `CODER_TRACE_ENABLE` environment variable or
|
||||
[--trace](https://coder.com/docs/reference/cli/server#--trace) CLI flag:
|
||||
|
||||
```sh
|
||||
export CODER_TRACE_ENABLE=true
|
||||
```
|
||||
|
||||
```sh
|
||||
coder server --trace
|
||||
```
|
||||
|
||||
### What is Traced
|
||||
|
||||
AI Bridge creates spans for the following operations:
|
||||
|
||||
| Span Name | Description |
|
||||
|---------------------------------------------|------------------------------------------------------|
|
||||
| `CachedBridgePool.Acquire` | Acquiring a request bridge instance from the pool |
|
||||
| `Intercept` | Top-level span for processing an intercepted request |
|
||||
| `Intercept.CreateInterceptor` | Creating the request interceptor |
|
||||
| `Intercept.ProcessRequest` | Processing the request through the bridge |
|
||||
| `Intercept.ProcessRequest.Upstream` | Forwarding the request to the upstream AI provider |
|
||||
| `Intercept.ProcessRequest.ToolCall` | Executing a tool call requested by the AI model |
|
||||
| `Intercept.RecordInterception` | Recording creating interception record |
|
||||
| `Intercept.RecordPromptUsage` | Recording prompt/message data |
|
||||
| `Intercept.RecordTokenUsage` | Recording token consumption |
|
||||
| `Intercept.RecordToolUsage` | Recording tool/function calls |
|
||||
| `Intercept.RecordInterceptionEnded` | Recording the interception as completed |
|
||||
| `ServerProxyManager.Init` | Initializing MCP server proxy connections |
|
||||
| `StreamableHTTPServerProxy.Init` | Setting up HTTP-based MCP server proxies |
|
||||
| `StreamableHTTPServerProxy.Init.fetchTools` | Fetching available tools from MCP servers |
|
||||
|
||||
Example trace of an interception using Jaeger backend:
|
||||
|
||||

|
||||
|
||||
### Capturing Logs in Traces
|
||||
|
||||
> **Note:** Enabling log capture may generate a large volume of trace events.
|
||||
|
||||
To include log messages as trace events, enable trace log capture
|
||||
by setting `CODER_TRACE_LOGS` environment variable or using
|
||||
[--trace-logs](https://coder.com/docs/reference/cli/server#--trace-logs) flag:
|
||||
|
||||
```sh
|
||||
export CODER_TRACE_ENABLE=true
|
||||
export CODER_TRACE_LOGS=true
|
||||
```
|
||||
|
||||
```sh
|
||||
coder server --trace --trace-logs
|
||||
```
|
||||
|
||||
+22
-4
@@ -1,10 +1,10 @@
|
||||
# Coder Tasks
|
||||
|
||||
Coder Tasks is an interface for running & managing coding agents such as Claude Code and Aider, powered by Coder workspaces.
|
||||
Coder Tasks is an API-first system for automating software development workflows with any AI coding agent, like Claude Code and Sourcegraph Amp. Tasks are designed for automation scenarios like CI/CD pipelines, GitHub Actions, scheduled jobs, and issue triaging where agents can work autonomously on well-defined tasks.
|
||||
|
||||

|
||||
|
||||
Coder Tasks is best for cases where the IDE is secondary, such as prototyping or running long-running background jobs. However, tasks run inside full workspaces so developers can [connect via an IDE](../user-guides/workspace-access) to take a task to completion.
|
||||
Each task runs inside an isolated Coder workspace and can be triggered programmatically via the Coder API, CLI, or through the web UI. Tasks excel at automating repetitive development work such as addressing bugs, updating documentation, or implementing small features, whether triggered by automation systems or manually through the interface.
|
||||
|
||||
> [!NOTE]
|
||||
> Coder Tasks is free and open source. If you are a Coder Premium customer or want to run hundreds of tasks in the background, [contact us](https://coder.com/contact) for roadmap information and volume pricing.
|
||||
@@ -25,6 +25,18 @@ Each task runs inside its own Coder workspace for isolation purposes. Agents lik
|
||||
|
||||
Coder's [built-in modules for agents](https://registry.coder.com/modules?search=tag%3Atasks) will pre-install the agent alongside [AgentAPI](https://github.com/coder/agentapi). AgentAPI is an open source project developed by Coder which improves status reporting and the Chat UI, regardless of which agent you use.
|
||||
|
||||
## Common Use Cases for Task Automation
|
||||
|
||||
Tasks are designed for automation workflows where AI agents can work independently:
|
||||
|
||||
- **CI/CD Integration**: Trigger tasks from GitHub Actions, GitLab CI, or Jenkins to automatically address issues, update dependencies, or generate documentation when code changes
|
||||
- **Issue Triage**: Automatically assign agents to labeled GitHub issues to investigate bugs, create reproduction steps, or propose fixes
|
||||
- **Scheduled Maintenance**: Run periodic tasks to update documentation, refactor code, add tests, or perform code quality improvements
|
||||
- **Code Review Assistance**: Spin up tasks to analyze pull requests, suggest improvements, or verify coding standards compliance
|
||||
- **Documentation Generation**: Automatically generate or update API documentation, README files, or architectural diagrams as code evolves
|
||||
|
||||
For a complete walkthrough of GitHub Actions integration, see [Guide: Create a GitHub to Coder Tasks Workflow](./github-to-tasks.md).
|
||||
|
||||
## Getting Started with Tasks
|
||||
|
||||
### Option 1) Import and Modify Our Example Template
|
||||
@@ -141,9 +153,15 @@ Coder can automatically generate a name your tasks if you set the `ANTHROPIC_API
|
||||
|
||||
If you tried Tasks and decided you don't want to use it, you can hide the Tasks tab by starting `coder server` with the `CODER_HIDE_AI_TASKS=true` environment variable or the `--hide-ai-tasks` flag.
|
||||
|
||||
## Command Line Interface
|
||||
## Command Line Interface and API Access
|
||||
|
||||
See [Tasks CLI](./cli.md).
|
||||
Tasks can be managed programmatically through the Coder CLI or REST API:
|
||||
|
||||
- **CLI Commands**: See [Tasks CLI](./cli.md) for creating, monitoring, and managing tasks from scripts or CI/CD pipelines
|
||||
- **REST API**: See [Tasks API Reference](../reference/api/tasks.md) for HTTP endpoints to integrate task creation into your automation workflows
|
||||
- **GitHub Actions**: Use the [create-task-action](https://github.com/coder/create-task-action) to trigger tasks from GitHub workflows
|
||||
|
||||
The API-first design enables tasks to be embedded into existing development workflows, version control systems, and issue tracking platforms.
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 412 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 133 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
@@ -129,14 +129,13 @@ We support two release channels: mainline and stable - read the
|
||||
- **Mainline** Coder release:
|
||||
|
||||
- **Chart Registry**
|
||||
|
||||
<!-- autoversion(mainline): "--version [version]" -->
|
||||
|
||||
```shell
|
||||
helm install coder coder-v2/coder \
|
||||
--namespace coder \
|
||||
--values values.yaml \
|
||||
--version 2.29.0
|
||||
--version 2.29.1
|
||||
```
|
||||
|
||||
- **OCI Registry**
|
||||
@@ -147,7 +146,7 @@ We support two release channels: mainline and stable - read the
|
||||
helm install coder oci://ghcr.io/coder/chart/coder \
|
||||
--namespace coder \
|
||||
--values values.yaml \
|
||||
--version 2.29.0
|
||||
--version 2.29.1
|
||||
```
|
||||
|
||||
- **Stable** Coder release:
|
||||
@@ -160,7 +159,7 @@ We support two release channels: mainline and stable - read the
|
||||
helm install coder coder-v2/coder \
|
||||
--namespace coder \
|
||||
--values values.yaml \
|
||||
--version 2.28.5
|
||||
--version 2.28.6
|
||||
```
|
||||
|
||||
- **OCI Registry**
|
||||
@@ -171,7 +170,7 @@ We support two release channels: mainline and stable - read the
|
||||
helm install coder oci://ghcr.io/coder/chart/coder \
|
||||
--namespace coder \
|
||||
--values values.yaml \
|
||||
--version 2.28.5
|
||||
--version 2.28.6
|
||||
```
|
||||
|
||||
You can watch Coder start up by running `kubectl get pods -n coder`. Once Coder
|
||||
|
||||
@@ -134,8 +134,8 @@ kubectl create secret generic coder-db-url -n coder \
|
||||
|
||||
1. Select a Coder version:
|
||||
|
||||
- **Mainline**: `2.29.0`
|
||||
- **Stable**: `2.28.5`
|
||||
- **Mainline**: `2.29.1`
|
||||
- **Stable**: `2.28.6`
|
||||
|
||||
Learn more about release channels in the [Releases documentation](./releases/index.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Upgrading from ESR 2.24 to 2.29
|
||||
|
||||
## Guide Overview
|
||||
|
||||
Coder provides Extended Support Releases (ESR) bianually. This guide walks through upgrading from the initial Coder 2.24 ESR to our new 2.29 ESR. It will summarize key changes, highlight breaking updates, and provide a recommended upgrade process.
|
||||
|
||||
Read more about the ESR release process [here](./index.md#extended-support-release), and how Coder supports it.
|
||||
|
||||
## What's New in Coder 2.29
|
||||
|
||||
### Coder Tasks
|
||||
|
||||
Coder Tasks is an interface for running and interfacing with terminal-based coding agents like Claude Code and Codex, powered by Coder workspaces. Beginning in Coder 2.24, Tasks were introduced as an experimental feature that allowed administrators and developers to run long-lived or automated operations from templates. Over subsequent releases, Tasks matured significantly through UI refinement, improved reliability, and underlying task-status improvements in the server and database layers. By 2.29, Tasks were formally promoted to general availability, with full CLI support, a task-specific UI, and consistent visibility of task states across the dashboard. This transition establishes Tasks as a stable automation and job-execution primitive within Coder—particularly suited for long-running background operations like bug fixes, documentation generation, PR reviews, and testing/QA.For more information, read our documentation [here](https://coder.com/docs/ai-coder/tasks).
|
||||
|
||||
### AI Bridge
|
||||
|
||||
AI Bridge was introduced in 2.26, and is a smart gateway that acts as an intermediary between users' coding agents/IDEs and AI providers like OpenAI and Anthropic. It solves three key problems:
|
||||
|
||||
- Centralized authentication/authorization management (users authenticate via Coder instead of managing individual API tokens)
|
||||
- Auditing and attribution of all AI interactions (whether autonomous or human-initiated)
|
||||
- Secure communication between the Coder control plane and upstream AI APIs
|
||||
|
||||
This is a Premium/Beta feature that intercepts AI traffic to record prompts, token usage, and tool invocations. For more information, read our documentation [here](https://coder.com/docs/ai-coder/ai-bridge).
|
||||
|
||||
### Agent Boundaries
|
||||
|
||||
Agent Boundaries was introduced in 2.27 and is currently in Early Access. Agent Boundaries are process-level firewalls in Coder that restrict and audit what autonomous programs (like AI agents) can access and do within a workspace. They provide network policy enforcement—blocking specific domains and HTTP verbs to prevent data exfiltration—and write logs to the workspace for auditability. Boundaries support any terminal-based agent, including custom ones, and can be easily configured through existing Coder modules like the Claude Code module. For more information, read our documentation [here](https://coder.com/docs/ai-coder/agent-boundary).
|
||||
|
||||
### Performance Enhancements
|
||||
|
||||
Performance, particularly at scale, improved across nearly every system layer. Database queries were optimized, several new indexes were added, and expensive migrations—such as migration 371—were reworked to complete faster on large deployments. Caching was introduced for Terraform installer files and workspace/agent lookups, reducing repeated calls. Notification performance improved through more efficient connection pooling. These changes collectively enable deployments with hundreds or thousands of workspaces to operate more smoothly and with lower resource contention.
|
||||
|
||||
### Server and API Updates
|
||||
|
||||
Core server capabilities expanded significantly across the releases. Prebuild workflows gained timestamp-driven invalidation via last_invalidated_at, expired API keys began being automatically purged, and new API key-scope documentation was introduced to help administrators understand authorization boundaries. New API endpoints were added, including the ability to modify a task prompt or look up tasks by name. Template developers benefited from new Terraform directory-persistence capabilities (opt-in on a per-template basis) and improved `protobuf` configuration metadata.
|
||||
|
||||
### CLI Enhancements
|
||||
|
||||
The CLI gained substantial improvements between the two versions. Most notably, beginning in 2.29, Coder’s CLI now stores session tokens in the operating system keyring by default on macOS and Windows, enhancing credential security and reducing exposure from plaintext token storage. Users who rely on directly accessing the token file can opt out using `--use-keyring=false`. The CLI also introduced cross-platform support for keyring storage, gained support for GA Task commands, and integrated experimental functionality for the new Agent Socket API.
|
||||
|
||||
## Changes to be Aware of
|
||||
|
||||
The following are changes introduced after 2.24.X that might break workflows, or require other manual effort to address:
|
||||
|
||||
| Initial State (2.24 & before) | New State (2.25–2.29) | Change Required |
|
||||
|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Workspace updates occur in place without stopping | Workspace updates now forcibly stop workspaces before updating | Expect downtime during updates; update any scripted update flows that rely on seamless updates. See [`coder update` CLI reference](https://coder.com/docs/reference/cli/update). |
|
||||
| Connection events (SSH, port-forward, browser) logged in Audit Log | Connection events moved to Connection Log; historical entries older than 90 days pruned | Update compliance, audit, or ingestion pipelines to use the new [Connection Log](https://coder.com/docs/admin/monitoring/connection-logs) instead of [Audit Logs](https://coder.com/docs/admin/security/audit-logs) for connection events. |
|
||||
| CLI session tokens stored in plaintext file | CLI session tokens stored in OS keyring (macOS/Windows) | Update scripts, automation, or SSO flows that read/modify the token file, or use `--use-keyring=false`. See [Sessions & API Tokens](https://coder.com/docs/admin/users/sessions-tokens) and [`coder login` CLI reference](https://coder.com/docs/reference/cli/login). |
|
||||
| `task_app_id` field available in `codersdk.WorkspaceBuild` | `task_app_id` removed from `codersdk.WorkspaceBuild` | Migrate integrations to use `Task.WorkspaceAppID` instead. See [REST API reference](https://coder.com/docs/reference/api). |
|
||||
| OIDC session handling more permissive | Sessions expire when access tokens expire (typically 1 hour) unless refresh tokens are configured | Add `offline_access` to `CODER_OIDC_SCOPES` (e.g., `openid,profile,email,offline_access`); Google requires `CODER_OIDC_AUTH_URL_PARAMS='{"access_type":"offline","prompt":"consent"}'`. See [OIDC Refresh Tokens](https://coder.com/docs/admin/users/oidc-auth/refresh-tokens). |
|
||||
| Devcontainer agent selection is random when multiple agents exist | Devcontainer agent selection requires explicit choice | Update automated workflows to explicitly specify agent selection. See [Dev Containers Integration](https://coder.com/docs/user-guides/devcontainers) and [Configure a template for dev containers](https://coder.com/docs/admin/templates/extending-templates/devcontainers). |
|
||||
| Terraform execution uses clean directories per build | Terraform workflows use persistent or cached directories when enabled | Update templates that rely on clean execution directories or per-build isolation. See [External Provisioners](https://coder.com/docs/admin/provisioners) and [Template Dependencies](https://coder.com/docs/admin/templates/managing-templates/dependencies). |
|
||||
| Agent and task lifecycle behaviors more permissive | Agent and task lifecycle behaviors enforce stricter permission checks, readiness gating, and ordering | Review workflows for compatibility with stricter readiness and permission requirements. See [Workspace Lifecycle](https://coder.com/docs/user-guides/workspace-lifecycle) and [Extending Templates](https://coder.com/docs/admin/templates/extending-templates). |
|
||||
|
||||
## Upgrading
|
||||
|
||||
The following are recommendations by the Coder team when performing the upgrade:
|
||||
|
||||
- **Perform the upgrade in a staging environment first:** The cumulative changes between 2.24 and 2.29 introduce new subsystems and lifecycle behaviors, so validating templates, authentication flows, and workspace operations in staging helps avoid production issues
|
||||
- **Audit scripts or tools that rely on the CLI token file:** Since 2.29 uses the OS keyring for session tokens on macOS and Windows, update any tooling that reads the plaintext token file or plan to use `--use-keyring=false`
|
||||
- **Review templates using devcontainers or Terraform:** Explicit agent selection, optional persistent/cached Terraform directories, and updated metadata handling mean template authors should retest builds and startup behavior
|
||||
- **Check and update OIDC provider configuration:** Stricter refresh-token requirements in later releases can cause unexpected logouts or failed CLI authentication if providers are not configured according to updated docs
|
||||
- **Update integrations referencing deprecated API fields:** Code relying on `WorkspaceBuild.task_app_id` must migrate to `Task.WorkspaceAppID`, and any custom integrations built against 2.24 APIs should be validated against the new SDK
|
||||
- **Communicate audit-logging changes to security/compliance teams:** From 2.25 onward, connection events moved into the Connection Log, and older audit entries may be pruned, which can affect SIEM pipelines or compliance workflows
|
||||
- **Validate workspace lifecycle automation:** Since updates now require stopping the workspace first, confirm that automated update jobs, scripts, or scheduled tasks still function correctly in this new model
|
||||
- **Retest agent and task automation built on early experimental features:** Updates to agent readiness, permission checks, and lifecycle ordering may affect workflows developed against 2.24’s looser behaviors
|
||||
- **Monitor workspace, template, and Terraform build performance:** New caching, indexes, and DB optimizations may change build times; observing performance post-upgrade helps catch regressions early
|
||||
- **Prepare user communications around Tasks and UI changes:** Tasks are now GA and more visible in the dashboard, and many UI improvements will be new to users coming from 2.24, so a brief internal announcement can smooth the transition
|
||||
@@ -9,12 +9,14 @@ deployment.
|
||||
|
||||
## Release channels
|
||||
|
||||
We support two release channels:
|
||||
[mainline](https://github.com/coder/coder/releases/tag/v2.29.0) for the bleeding
|
||||
edge version of Coder and
|
||||
[stable](https://github.com/coder/coder/releases/latest) for those with lower
|
||||
tolerance for fault. We field our mainline releases publicly for one month
|
||||
before promoting them to stable. The version prior to stable receives patches
|
||||
We support four release channels:
|
||||
|
||||
- **Mainline:** The bleeding edge version of Coder
|
||||
- **Stable:** N-1 of the mainline release
|
||||
- **Security Support:** N-2 of the mainline release
|
||||
- **Extended Support Release:** Biannually released version of Coder
|
||||
|
||||
We field our mainline releases publicly for one month before promoting them to stable. The security support version, so n-2 from mainline, receives patches
|
||||
only for security issues or CVEs.
|
||||
|
||||
### Mainline releases
|
||||
@@ -37,6 +39,16 @@ only for security issues or CVEs.
|
||||
For more information on feature rollout, see our
|
||||
[feature stages documentation](../releases/feature-stages.md).
|
||||
|
||||
### Extended Support Release
|
||||
|
||||
- Designed for organizations that prioritize long-term stability
|
||||
- Receives only critical bugfixes and security patches
|
||||
- Ideal for regulated environments or large deployments with strict upgrade cycles
|
||||
|
||||
ESR releases will be updated with critical bugfixes and security patches that are available to paying customers. This extended support model provides predictable, long-term maintenance for organizations that require enhanced stability. Because ESR forgoes new features in favor of maintenance and stability, it is best suited for teams with strict upgrade constraints. The latest ESR version is [Coder 2.29](https://github.com/coder/coder/releases/tag/v2.29.0).
|
||||
|
||||
For more information, see the [Coder ESR announcement](https://coder.com/blog/esr) or our [ESR Upgrade Guide](./esr-2.24-2.29-upgrade.md).
|
||||
|
||||
## Installing stable
|
||||
|
||||
When installing Coder, we generally advise specifying the desired version from
|
||||
@@ -55,15 +67,15 @@ pages.
|
||||
## Release schedule
|
||||
<!-- Autogenerated release calendar from scripts/update-release-calendar.sh -->
|
||||
<!-- RELEASE_CALENDAR_START -->
|
||||
| Release name | Release Date | Status | Latest Release |
|
||||
|------------------------------------------------|--------------------|------------------|----------------------------------------------------------------|
|
||||
| [2.24](https://coder.com/changelog/coder-2-24) | July 01, 2025 | Not Supported | [v2.24.4](https://github.com/coder/coder/releases/tag/v2.24.4) |
|
||||
| [2.25](https://coder.com/changelog/coder-2-25) | August 05, 2025 | Not Supported | [v2.25.3](https://github.com/coder/coder/releases/tag/v2.25.3) |
|
||||
| [2.26](https://coder.com/changelog/coder-2-26) | September 03, 2025 | Not Supported | [v2.26.6](https://github.com/coder/coder/releases/tag/v2.26.6) |
|
||||
| [2.27](https://coder.com/changelog/coder-2-27) | October 02, 2025 | Security Support | [v2.27.8](https://github.com/coder/coder/releases/tag/v2.27.8) |
|
||||
| [2.28](https://coder.com/changelog/coder-2-28) | November 04, 2025 | Stable | [v2.28.5](https://github.com/coder/coder/releases/tag/v2.28.5) |
|
||||
| [2.29](https://coder.com/changelog/coder-2-29) | December 02, 2025 | Mainline | [v2.29.0](https://github.com/coder/coder/releases/tag/v2.29.0) |
|
||||
| 2.30 | | Not Released | N/A |
|
||||
| Release name | Release Date | Status | Latest Release |
|
||||
|------------------------------------------------|--------------------|--------------------------|----------------------------------------------------------------|
|
||||
| [2.24](https://coder.com/changelog/coder-2-24) | July 01, 2025 | Extended Support Release | [v2.24.4](https://github.com/coder/coder/releases/tag/v2.24.4) |
|
||||
| [2.25](https://coder.com/changelog/coder-2-25) | August 05, 2025 | Not Supported | [v2.25.3](https://github.com/coder/coder/releases/tag/v2.25.3) |
|
||||
| [2.26](https://coder.com/changelog/coder-2-26) | September 03, 2025 | Not Supported | [v2.26.6](https://github.com/coder/coder/releases/tag/v2.26.6) |
|
||||
| [2.27](https://coder.com/changelog/coder-2-27) | October 02, 2025 | Security Support | [v2.27.9](https://github.com/coder/coder/releases/tag/v2.27.9) |
|
||||
| [2.28](https://coder.com/changelog/coder-2-28) | November 04, 2025 | Stable | [v2.28.6](https://github.com/coder/coder/releases/tag/v2.28.6) |
|
||||
| [2.29](https://coder.com/changelog/coder-2-29) | December 02, 2025 | Mainline + ESR | [v2.29.1](https://github.com/coder/coder/releases/tag/v2.29.1) |
|
||||
| 2.30 | | Not Released | N/A |
|
||||
<!-- RELEASE_CALENDAR_END -->
|
||||
|
||||
> [!TIP]
|
||||
@@ -75,6 +87,6 @@ pages.
|
||||
>
|
||||
> The `preview` image is not intended for production use.
|
||||
|
||||
### A note about January releases
|
||||
### January Releases
|
||||
|
||||
As of January, 2025 we skip the January release each year because most of our engineering team is out for the December holiday period.
|
||||
Releases on the first Tuesday of January **are not guaranteed to occur** because most of our team is out for the December holiday period. That being said, an ad-hoc release might still occur. We advise not relying on a January release, or reaching out to Coder directly to determine if one will be occurring closer to the release date.
|
||||
|
||||
+51
-24
@@ -187,6 +187,11 @@
|
||||
"title": "Feature stages",
|
||||
"description": "Information about pre-GA stages.",
|
||||
"path": "./install/releases/feature-stages.md"
|
||||
},
|
||||
{
|
||||
"title": "Upgrading from ESR 2.24 to 2.29",
|
||||
"description": "Upgrade Guide for ESR Releases",
|
||||
"path": "./install/releases/esr-2.24-2.29-upgrade.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -316,7 +321,7 @@
|
||||
"icon_path": "./images/icons/circle-dot.svg"
|
||||
},
|
||||
{
|
||||
"title": "Dev Containers Integration",
|
||||
"title": "Dev Containers",
|
||||
"description": "Run containerized development environments in your Coder workspace using the dev containers specification.",
|
||||
"path": "./user-guides/devcontainers/index.md",
|
||||
"icon_path": "./images/icons/container.svg",
|
||||
@@ -326,6 +331,11 @@
|
||||
"description": "Access dev containers via SSH, your IDE, or web terminal.",
|
||||
"path": "./user-guides/devcontainers/working-with-dev-containers.md"
|
||||
},
|
||||
{
|
||||
"title": "Customizing dev containers",
|
||||
"description": "Configure custom agent names, apps, and display options in devcontainer.json.",
|
||||
"path": "./user-guides/devcontainers/customizing-dev-containers.md"
|
||||
},
|
||||
{
|
||||
"title": "Troubleshooting dev containers",
|
||||
"description": "Diagnose and resolve common issues with dev containers in your Coder workspace.",
|
||||
@@ -522,26 +532,9 @@
|
||||
"path": "./admin/templates/managing-templates/change-management.md"
|
||||
},
|
||||
{
|
||||
"title": "Dev containers",
|
||||
"description": "Learn about using development containers in templates",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/index.md",
|
||||
"children": [
|
||||
{
|
||||
"title": "Add a dev container template",
|
||||
"description": "How to add a dev container template to Coder",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/add-devcontainer.md"
|
||||
},
|
||||
{
|
||||
"title": "Dev container security and caching",
|
||||
"description": "Configure dev container authentication and caching",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/devcontainer-security-caching.md"
|
||||
},
|
||||
{
|
||||
"title": "Dev container releases and known issues",
|
||||
"description": "Dev container releases and known issues",
|
||||
"path": "./admin/templates/managing-templates/devcontainers/devcontainer-releases-known-issues.md"
|
||||
}
|
||||
]
|
||||
"title": "Envbuilder",
|
||||
"description": "Shift environment definition to repositories",
|
||||
"path": "./admin/templates/managing-templates/envbuilder.md"
|
||||
},
|
||||
{
|
||||
"title": "Template Dependencies",
|
||||
@@ -653,8 +646,8 @@
|
||||
"path": "./admin/templates/extending-templates/provider-authentication.md"
|
||||
},
|
||||
{
|
||||
"title": "Configure a template for dev containers",
|
||||
"description": "How to use configure your template for dev containers",
|
||||
"title": "Dev Containers",
|
||||
"description": "Extend templates with containerized dev environments",
|
||||
"path": "./admin/templates/extending-templates/devcontainers.md"
|
||||
},
|
||||
{
|
||||
@@ -754,6 +747,40 @@
|
||||
"title": "OAuth2 Provider",
|
||||
"description": "Use Coder as an OAuth2 provider",
|
||||
"path": "./admin/integrations/oauth2-provider.md"
|
||||
},
|
||||
{
|
||||
"title": "Dev Containers",
|
||||
"description": "Configure dev container support using Docker or Envbuilder",
|
||||
"path": "./admin/integrations/devcontainers/index.md",
|
||||
"children": [
|
||||
{
|
||||
"title": "Dev Containers Integration",
|
||||
"description": "Configure native dev containers with Docker",
|
||||
"path": "./admin/integrations/devcontainers/integration.md"
|
||||
},
|
||||
{
|
||||
"title": "Envbuilder",
|
||||
"description": "Build dev containers without Docker",
|
||||
"path": "./admin/integrations/devcontainers/envbuilder/index.md",
|
||||
"children": [
|
||||
{
|
||||
"title": "Add an Envbuilder template",
|
||||
"description": "How to add an Envbuilder template",
|
||||
"path": "./admin/integrations/devcontainers/envbuilder/add-envbuilder.md"
|
||||
},
|
||||
{
|
||||
"title": "Security and caching",
|
||||
"description": "Configure authentication and caching",
|
||||
"path": "./admin/integrations/devcontainers/envbuilder/envbuilder-security-caching.md"
|
||||
},
|
||||
{
|
||||
"title": "Releases and known issues",
|
||||
"description": "Release channels and known issues",
|
||||
"path": "./admin/integrations/devcontainers/envbuilder/envbuilder-releases-known-issues.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -938,7 +965,7 @@
|
||||
},
|
||||
{
|
||||
"title": "AI Bridge",
|
||||
"description": "Centralized LLM and MCP proxy for platform teams",
|
||||
"description": "AI Gateway for Enterprise Governance \u0026 Observability",
|
||||
"path": "./ai-coder/ai-bridge/index.md",
|
||||
"icon_path": "./images/icons/api.svg",
|
||||
"state": ["premium", "beta"],
|
||||
|
||||
Generated
+87
@@ -727,6 +727,93 @@ Status Code **200**
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Add new license
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"license": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------------------------------------------------------------------|----------|---------------------|
|
||||
| `body` | body | [codersdk.AddLicenseRequest](schemas.md#codersdkaddlicenserequest) | true | Add license request |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"claims": {},
|
||||
"id": 0,
|
||||
"uploaded_at": "2019-08-24T14:15:22Z",
|
||||
"uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.License](schemas.md#codersdklicense) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update license entitlements
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses/refresh-entitlements`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "string",
|
||||
"message": "string",
|
||||
"validations": [
|
||||
{
|
||||
"detail": "string",
|
||||
"field": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|--------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Delete license
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+5
-4
@@ -31,7 +31,7 @@ file: string
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -41,9 +41,10 @@ file: string
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|--------------------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.UploadResponse](schemas.md#codersdkuploadresponse) |
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|------------------------------------|--------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | Returns existing file if duplicate | [codersdk.UploadResponse](schemas.md#codersdkuploadresponse) |
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Returns newly created file | [codersdk.UploadResponse](schemas.md#codersdkuploadresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
||||
Generated
+1
@@ -233,6 +233,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
||||
"disable_owner_workspace_exec": true,
|
||||
"disable_password_auth": true,
|
||||
"disable_path_apps": true,
|
||||
"disable_workspace_sharing": true,
|
||||
"docs_url": {
|
||||
"forceQuery": true,
|
||||
"fragment": "string",
|
||||
|
||||
Generated
-87
@@ -1,92 +1,5 @@
|
||||
# Organizations
|
||||
|
||||
## Add new license
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"license": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------------------------------------------------------------------|----------|---------------------|
|
||||
| `body` | body | [codersdk.AddLicenseRequest](schemas.md#codersdkaddlicenserequest) | true | Add license request |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"claims": {},
|
||||
"id": 0,
|
||||
"uploaded_at": "2019-08-24T14:15:22Z",
|
||||
"uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f"
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.License](schemas.md#codersdklicense) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update license entitlements
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/licenses/refresh-entitlements \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /licenses/refresh-entitlements`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 201 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "string",
|
||||
"message": "string",
|
||||
"validations": [
|
||||
{
|
||||
"detail": "string",
|
||||
"field": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|--------------------------------------------------------------|-------------|--------------------------------------------------|
|
||||
| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Response](schemas.md#codersdkresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get organizations
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+3
@@ -2917,6 +2917,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"disable_owner_workspace_exec": true,
|
||||
"disable_password_auth": true,
|
||||
"disable_path_apps": true,
|
||||
"disable_workspace_sharing": true,
|
||||
"docs_url": {
|
||||
"forceQuery": true,
|
||||
"fragment": "string",
|
||||
@@ -3439,6 +3440,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"disable_owner_workspace_exec": true,
|
||||
"disable_password_auth": true,
|
||||
"disable_path_apps": true,
|
||||
"disable_workspace_sharing": true,
|
||||
"docs_url": {
|
||||
"forceQuery": true,
|
||||
"fragment": "string",
|
||||
@@ -3793,6 +3795,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `disable_owner_workspace_exec` | boolean | false | | |
|
||||
| `disable_password_auth` | boolean | false | | |
|
||||
| `disable_path_apps` | boolean | false | | |
|
||||
| `disable_workspace_sharing` | boolean | false | | |
|
||||
| `docs_url` | [serpent.URL](#serpenturl) | false | | |
|
||||
| `enable_authz_recording` | boolean | false | | |
|
||||
| `enable_terraform_debug_mode` | boolean | false | | |
|
||||
|
||||
Generated
+10
@@ -1115,6 +1115,16 @@ Disable workspace apps that are not served from subdomains. Path-based apps can
|
||||
|
||||
Remove the permission for the 'owner' role to have workspace execution on all workspaces. This prevents the 'owner' from ssh, apps, and terminal access based on the 'owner' role. They still have their user permissions to access their own workspaces.
|
||||
|
||||
### --disable-workspace-sharing
|
||||
|
||||
| | |
|
||||
|-------------|-----------------------------------------------|
|
||||
| Type | <code>bool</code> |
|
||||
| Environment | <code>$CODER_DISABLE_WORKSPACE_SHARING</code> |
|
||||
| YAML | <code>disableWorkspaceSharing</code> |
|
||||
|
||||
Disable workspace sharing (requires the "workspace-sharing" experiment to be enabled). Workspace ACL checking is disabled and only owners can have ssh, apps and terminal access to workspaces. Access based on the 'owner' role is also allowed unless disabled via --disable-owner-workspace-access.
|
||||
|
||||
### --session-duration
|
||||
|
||||
| | |
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
# Customizing dev containers
|
||||
|
||||
Coder supports custom configuration in your `devcontainer.json` file through the
|
||||
`customizations.coder` block. These options let you control how Coder interacts
|
||||
with your dev container without requiring template changes.
|
||||
|
||||
## Ignore a dev container
|
||||
|
||||
Use the `ignore` option to hide a dev container from Coder completely:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"ignore": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `ignore` is set to `true`:
|
||||
|
||||
- The dev container won't appear in the Coder UI
|
||||
- Coder won't manage or monitor the container
|
||||
|
||||
This is useful for dev containers in your repository that you don't want Coder
|
||||
to manage.
|
||||
|
||||
## Auto-start
|
||||
|
||||
Control whether your dev container should auto-start using the `autoStart`
|
||||
option:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"autoStart": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `autoStart` is set to `true`, the dev container automatically builds and
|
||||
starts during workspace initialization.
|
||||
|
||||
When `autoStart` is set to `false` or omitted, the dev container is discovered
|
||||
and shown in the UI, but users must manually start it.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The `autoStart` option only takes effect when your template administrator has
|
||||
> enabled [`CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE`](../../admin/integrations/devcontainers/integration.md#coder_agent_devcontainers_discovery_autostart_enable).
|
||||
> If this setting is disabled at the template level, containers won't auto-start
|
||||
> regardless of this option.
|
||||
|
||||
## Custom agent name
|
||||
|
||||
Each dev container gets an agent name derived from the workspace folder path by
|
||||
default. You can set a custom name using the `name` option:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"name": "my-custom-agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The name must contain only lowercase letters, numbers, and hyphens. This name
|
||||
appears in `coder ssh` commands and the dashboard (e.g.,
|
||||
`coder ssh my-workspace.my-custom-agent`).
|
||||
|
||||
## Display apps
|
||||
|
||||
Control which built-in Coder apps appear for your dev container using
|
||||
`displayApps`:
|
||||
|
||||
_Disable built-in apps to reduce clutter or guide developers toward preferred tools_
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"displayApps": {
|
||||
"web_terminal": true,
|
||||
"ssh_helper": true,
|
||||
"port_forwarding_helper": true,
|
||||
"vscode": true,
|
||||
"vscode_insiders": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Available display apps:
|
||||
|
||||
| App | Description | Default |
|
||||
|--------------------------|------------------------------|---------|
|
||||
| `web_terminal` | Web-based terminal access | `true` |
|
||||
| `ssh_helper` | SSH connection helper | `true` |
|
||||
| `port_forwarding_helper` | Port forwarding interface | `true` |
|
||||
| `vscode` | VS Code Desktop integration | `true` |
|
||||
| `vscode_insiders` | VS Code Insiders integration | `false` |
|
||||
|
||||
## Custom apps
|
||||
|
||||
Define custom applications for your dev container using the `apps` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "zed",
|
||||
"displayName": "Zed Editor",
|
||||
"url": "zed://ssh/${localEnv:CODER_WORKSPACE_AGENT_NAME}.${localEnv:CODER_WORKSPACE_NAME}.${localEnv:CODER_WORKSPACE_OWNER_NAME}.coder${containerWorkspaceFolder}",
|
||||
"external": true,
|
||||
"icon": "/icon/zed.svg",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This example adds a Zed Editor button that opens the dev container directly in
|
||||
the Zed desktop app via its SSH remote feature.
|
||||
|
||||
Each app supports the following properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
|---------------|---------|---------------------------------------------------------------|
|
||||
| `slug` | string | Unique identifier for the app (required) |
|
||||
| `displayName` | string | Human-readable name shown in the UI |
|
||||
| `url` | string | URL to open (supports variable interpolation) |
|
||||
| `command` | string | Command to run instead of opening a URL |
|
||||
| `icon` | string | Path to an icon (e.g., `/icon/code.svg`) |
|
||||
| `openIn` | string | `"tab"` or `"slim-window"` (default: `"slim-window"`) |
|
||||
| `share` | string | `"owner"`, `"authenticated"`, `"organization"`, or `"public"` |
|
||||
| `external` | boolean | Open as external URL (e.g., for desktop apps) |
|
||||
| `group` | string | Group name for organizing apps in the UI |
|
||||
| `order` | number | Sort order for display |
|
||||
| `hidden` | boolean | Hide the app from the UI |
|
||||
| `subdomain` | boolean | Use subdomain-based access |
|
||||
| `healthCheck` | object | Health check configuration (see below) |
|
||||
|
||||
### Health checks
|
||||
|
||||
Configure health checks to monitor app availability:
|
||||
|
||||
```json
|
||||
{
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "web-server",
|
||||
"displayName": "Web Server",
|
||||
"url": "http://localhost:8080",
|
||||
"healthCheck": {
|
||||
"url": "http://localhost:8080/healthz",
|
||||
"interval": 5,
|
||||
"threshold": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Health check properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
|-------------|--------|-------------------------------------------------|
|
||||
| `url` | string | URL to check for health status |
|
||||
| `interval` | number | Seconds between health checks |
|
||||
| `threshold` | number | Number of failures before marking app unhealthy |
|
||||
|
||||
## Variable interpolation
|
||||
|
||||
App URLs and other string values support variable interpolation for dynamic
|
||||
configuration.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Use `${localEnv:VAR_NAME}` to reference environment variables, with optional
|
||||
default values:
|
||||
|
||||
```json
|
||||
{
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "my-app",
|
||||
"url": "http://${localEnv:HOST:127.0.0.1}:${localEnv:PORT:8080}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Coder-provided variables
|
||||
|
||||
Coder provides these environment variables automatically:
|
||||
|
||||
| Variable | Description |
|
||||
|-------------------------------------|------------------------------------|
|
||||
| `CODER_WORKSPACE_NAME` | Name of the workspace |
|
||||
| `CODER_WORKSPACE_OWNER_NAME` | Username of the workspace owner |
|
||||
| `CODER_WORKSPACE_AGENT_NAME` | Name of the dev container agent |
|
||||
| `CODER_WORKSPACE_PARENT_AGENT_NAME` | Name of the parent workspace agent |
|
||||
| `CODER_URL` | URL of the Coder deployment |
|
||||
| `CONTAINER_ID` | Docker container ID |
|
||||
|
||||
### Dev container variables
|
||||
|
||||
Standard dev container variables are also available:
|
||||
|
||||
| Variable | Description |
|
||||
|-------------------------------|--------------------------------------------|
|
||||
| `${containerWorkspaceFolder}` | Workspace folder path inside the container |
|
||||
| `${localWorkspaceFolder}` | Workspace folder path on the host |
|
||||
|
||||
### Session token
|
||||
|
||||
Use `$SESSION_TOKEN` in external app URLs to include the user's session token:
|
||||
|
||||
```json
|
||||
{
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "custom-ide",
|
||||
"displayName": "Custom IDE",
|
||||
"url": "custom-ide://open?token=$SESSION_TOKEN&folder=${containerWorkspaceFolder}",
|
||||
"external": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Feature options as environment variables
|
||||
|
||||
When your dev container uses features, Coder exposes feature options as
|
||||
environment variables. The format is `FEATURE_<FEATURE_NAME>_OPTION_<OPTION_NAME>`.
|
||||
|
||||
For example, with this feature configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 9090
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Coder creates `FEATURE_CODE_SERVER_OPTION_PORT=9090`, which you can reference in
|
||||
your apps:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 9090
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"coder": {
|
||||
"apps": [
|
||||
{
|
||||
"slug": "code-server",
|
||||
"displayName": "Code Server",
|
||||
"url": "http://localhost:${localEnv:FEATURE_CODE_SERVER_OPTION_PORT:8080}",
|
||||
"icon": "/icon/code.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Working with dev containers](./working-with-dev-containers.md) — SSH, IDE
|
||||
integration, and port forwarding
|
||||
- [Troubleshooting dev containers](./troubleshooting-dev-containers.md) —
|
||||
Diagnose common issues
|
||||
@@ -1,87 +1,142 @@
|
||||
# Dev Containers Integration
|
||||
# Dev Containers
|
||||
|
||||
The Dev Containers integration enables seamless creation and management of Dev
|
||||
Containers in Coder workspaces. This feature leverages the
|
||||
[`@devcontainers/cli`](https://github.com/devcontainers/cli) and
|
||||
[Docker](https://www.docker.com) to provide a streamlined development
|
||||
experience.
|
||||
[Dev containers](https://containers.dev/) define your development environment
|
||||
as code using a `devcontainer.json` file. Coder's Dev Containers integration
|
||||
uses the [`@devcontainers/cli`](https://github.com/devcontainers/cli) and
|
||||
[Docker](https://www.docker.com) to seamlessly build and run these containers,
|
||||
with management in your dashboard.
|
||||
|
||||
This implementation is different from the existing
|
||||
[Envbuilder-based Dev Containers](../../admin/templates/managing-templates/devcontainers/index.md)
|
||||
offering.
|
||||
This guide covers the Dev Containers integration. For workspaces without Docker,
|
||||
administrators can configure
|
||||
[Envbuilder](../../admin/integrations/devcontainers/envbuilder/index.md) instead,
|
||||
which builds the workspace image itself from your dev container configuration.
|
||||
|
||||
_Dev containers appear as sub-agents with their own apps, SSH access, and port forwarding_
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Coder version 2.24.0 or later
|
||||
- Coder CLI version 2.24.0 or later
|
||||
- **Linux or macOS workspace**, Dev Containers are not supported on Windows
|
||||
- A template with:
|
||||
- Dev Containers integration enabled
|
||||
- A Docker-compatible workspace image
|
||||
- Appropriate permissions to execute Docker commands inside your workspace
|
||||
- Docker available inside your workspace
|
||||
- The `@devcontainers/cli` installed in your workspace
|
||||
|
||||
## How It Works
|
||||
|
||||
The Dev Containers integration utilizes the `devcontainer` command from
|
||||
[`@devcontainers/cli`](https://github.com/devcontainers/cli) to manage Dev
|
||||
Containers within your Coder workspace.
|
||||
This command provides comprehensive functionality for creating, starting, and managing Dev Containers.
|
||||
|
||||
Dev environments are configured through a standard `devcontainer.json` file,
|
||||
which allows for extensive customization of your development setup.
|
||||
|
||||
When a workspace with the Dev Containers integration starts:
|
||||
|
||||
1. The workspace initializes the Docker environment.
|
||||
1. The integration detects repositories with a `.devcontainer` directory or a
|
||||
`devcontainer.json` file.
|
||||
1. The integration builds and starts the Dev Container based on the
|
||||
configuration.
|
||||
1. Your workspace automatically detects the running Dev Container.
|
||||
Dev Containers integration is enabled by default. Your workspace needs Docker
|
||||
(via Docker-in-Docker or a mounted socket) and the devcontainers CLI. Most
|
||||
templates with Dev Containers support include both. See
|
||||
[Configure a template for dev containers](../../admin/integrations/devcontainers/integration.md)
|
||||
for setup details.
|
||||
|
||||
## Features
|
||||
|
||||
### Available Now
|
||||
- Automatic dev container detection from repositories
|
||||
- Seamless container startup during workspace initialization
|
||||
- Change detection with outdated status indicator
|
||||
- On-demand container rebuild via dashboard button
|
||||
- Integrated IDE experience with VS Code
|
||||
- Direct SSH access to containers
|
||||
- Automatic port detection
|
||||
|
||||
- Automatic Dev Container detection from repositories
|
||||
- Seamless Dev Container startup during workspace initialization
|
||||
- Dev Container change detection and dirty state indicators
|
||||
- On-demand Dev Container recreation via rebuild button
|
||||
- Integrated IDE experience in Dev Containers with VS Code
|
||||
- Direct service access in Dev Containers
|
||||
- SSH access to Dev Containers
|
||||
- Automatic port detection for container ports
|
||||
## Getting started
|
||||
|
||||
### Add a devcontainer.json
|
||||
|
||||
Add a `devcontainer.json` file to your repository. This file defines your
|
||||
development environment. You can place it in:
|
||||
|
||||
- `.devcontainer/devcontainer.json` (recommended)
|
||||
- `.devcontainer.json` (root of repository)
|
||||
- `.devcontainer/<folder>/devcontainer.json` (for multiple configurations)
|
||||
|
||||
The third option allows monorepos to define multiple dev container
|
||||
configurations in separate sub-folders. See the
|
||||
[Dev Container specification](https://containers.dev/implementors/spec/#devcontainerjson)
|
||||
for details.
|
||||
|
||||
Here's a minimal example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu"
|
||||
}
|
||||
```
|
||||
|
||||
For more configuration options, see the
|
||||
[Dev Container specification](https://containers.dev/).
|
||||
|
||||
### Start your dev container
|
||||
|
||||
Coder automatically discovers dev container configurations in your repositories
|
||||
and displays them in your workspace dashboard. From there, you can start a dev
|
||||
container with a single click.
|
||||
|
||||
_Coder detects dev container configurations and displays them with a Start button_
|
||||
|
||||
If your template administrator has configured automatic startup (via the
|
||||
`coder_devcontainer` Terraform resource or autostart settings), your dev
|
||||
container will build and start automatically when the workspace starts.
|
||||
|
||||
### Connect to your dev container
|
||||
|
||||
Once running, your dev container appears as a sub-agent in your workspace
|
||||
dashboard. You can connect via:
|
||||
|
||||
- **Web terminal** in the Coder dashboard
|
||||
- **SSH** using `coder ssh <workspace>.<agent>`
|
||||
- **VS Code** using the "Open in VS Code Desktop" button
|
||||
|
||||
See [Working with dev containers](./working-with-dev-containers.md) for detailed
|
||||
connection instructions.
|
||||
|
||||
## How it works
|
||||
|
||||
The Dev Containers integration uses the `devcontainer` command from
|
||||
[`@devcontainers/cli`](https://github.com/devcontainers/cli) to manage
|
||||
containers within your Coder workspace.
|
||||
|
||||
When a workspace with Dev Containers integration starts:
|
||||
|
||||
1. The workspace initializes the Docker environment.
|
||||
1. The integration detects repositories with dev container configurations.
|
||||
1. Detected dev containers appear in the Coder dashboard.
|
||||
1. If auto-start is configured (via `coder_devcontainer` or autostart settings),
|
||||
the integration builds and starts the dev container automatically.
|
||||
1. Coder creates a sub-agent for the running container, enabling direct access.
|
||||
|
||||
Without auto-start, users can manually start discovered dev containers from the
|
||||
dashboard.
|
||||
|
||||
### Agent naming
|
||||
|
||||
Each dev container gets its own agent name, derived from the workspace folder
|
||||
path. For example, a dev container with workspace folder `/home/coder/my-app`
|
||||
will have an agent named `my-app`.
|
||||
|
||||
Agent names are sanitized to contain only lowercase alphanumeric characters and
|
||||
hyphens. You can also set a
|
||||
[custom agent name](./customizing-dev-containers.md#custom-agent-name)
|
||||
in your `devcontainer.json`.
|
||||
|
||||
## Limitations
|
||||
|
||||
The Dev Containers integration has the following limitations:
|
||||
- **Linux only**: Dev Containers are currently not supported in Windows or
|
||||
macOS workspaces
|
||||
- Changes to `devcontainer.json` require manual rebuild using the dashboard
|
||||
button
|
||||
- The `forwardPorts` property in `devcontainer.json` with `host:port` syntax
|
||||
(e.g., `"db:5432"`) for Docker Compose sidecar containers is not yet
|
||||
supported. For single-container dev containers, use `coder port-forward` to
|
||||
access ports directly on the sub-agent.
|
||||
- Some advanced dev container features may have limited support
|
||||
|
||||
- **Not supported on Windows**
|
||||
- Changes to the `devcontainer.json` file require manual container recreation
|
||||
using the rebuild button
|
||||
- Some Dev Container features may not work as expected
|
||||
## Next steps
|
||||
|
||||
## Comparison with Envbuilder-based Dev Containers
|
||||
|
||||
| Feature | Dev Containers Integration | Envbuilder Dev Containers |
|
||||
|----------------|----------------------------------------|----------------------------------------------|
|
||||
| Implementation | Direct `@devcontainers/cli` and Docker | Coder's Envbuilder |
|
||||
| Target users | Individual developers | Platform teams and administrators |
|
||||
| Configuration | Standard `devcontainer.json` | Terraform templates with Envbuilder |
|
||||
| Management | User-controlled | Admin-controlled |
|
||||
| Requirements | Docker access in workspace | Compatible with more restricted environments |
|
||||
|
||||
Choose the appropriate solution based on your team's needs and infrastructure
|
||||
constraints. For additional details on Envbuilder's Dev Container support, see
|
||||
the
|
||||
[Envbuilder Dev Container spec support documentation](https://github.com/coder/envbuilder/blob/main/docs/devcontainer-spec-support.md).
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Explore the [Dev Container specification](https://containers.dev/) to learn
|
||||
more about advanced configuration options
|
||||
- Read about [Dev Container features](https://containers.dev/features) to
|
||||
enhance your development environment
|
||||
- Check the
|
||||
[VS Code dev containers documentation](https://code.visualstudio.com/docs/devcontainers/containers)
|
||||
for IDE-specific features
|
||||
- [Working with dev containers](./working-with-dev-containers.md) — SSH, IDE
|
||||
integration, and port forwarding
|
||||
- [Customizing dev containers](./customizing-dev-containers.md) — Custom agent
|
||||
names, apps, and display options
|
||||
- [Troubleshooting dev containers](./troubleshooting-dev-containers.md) —
|
||||
Diagnose common issues
|
||||
- [Dev Container specification](https://containers.dev/) — Advanced
|
||||
configuration options
|
||||
- [Dev Container features](https://containers.dev/features) — Enhance your
|
||||
environment with pre-built tools
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Troubleshooting dev containers
|
||||
|
||||
## Dev Container Not Starting
|
||||
## Dev container not starting
|
||||
|
||||
If your dev container fails to start:
|
||||
|
||||
@@ -10,7 +10,108 @@ If your dev container fails to start:
|
||||
- `/tmp/coder-startup-script.log`
|
||||
- `/tmp/coder-script-[script_id].log`
|
||||
|
||||
1. Verify that Docker is running in your workspace.
|
||||
1. Ensure the `devcontainer.json` file is valid.
|
||||
1. Verify Docker is available in your workspace (see below).
|
||||
1. Ensure the `devcontainer.json` file is valid JSON.
|
||||
1. Check that the repository has been cloned correctly.
|
||||
1. Verify the resource limits in your workspace are sufficient.
|
||||
|
||||
## Docker not available
|
||||
|
||||
Dev containers require Docker, either via a running daemon (Docker-in-Docker) or
|
||||
a mounted socket from the host. Your template determines which approach is used.
|
||||
|
||||
**If using Docker-in-Docker**, check that the daemon is running:
|
||||
|
||||
```console
|
||||
sudo service docker status
|
||||
sudo service docker start # if not running
|
||||
```
|
||||
|
||||
**If using a mounted socket**, verify the socket exists and is accessible:
|
||||
|
||||
```console
|
||||
ls -la /var/run/docker.sock
|
||||
docker ps # test access
|
||||
```
|
||||
|
||||
If you get permission errors, your user may need to be in the `docker` group.
|
||||
|
||||
## Finding your dev container agent
|
||||
|
||||
Use `coder show` to list all agents in your workspace, including dev container
|
||||
sub-agents:
|
||||
|
||||
```console
|
||||
coder show <workspace>
|
||||
```
|
||||
|
||||
The agent name is derived from the workspace folder path. For details on how
|
||||
names are generated, see [Agent naming](./index.md#agent-naming).
|
||||
|
||||
## SSH connection issues
|
||||
|
||||
If `coder ssh <workspace>.<agent>` fails:
|
||||
|
||||
1. Verify the agent name using `coder show <workspace>`.
|
||||
1. Check that the dev container is running:
|
||||
|
||||
```console
|
||||
docker ps
|
||||
```
|
||||
|
||||
1. Check the workspace agent logs for container-related errors:
|
||||
|
||||
```console
|
||||
grep -i container /tmp/coder-agent.log
|
||||
```
|
||||
|
||||
## VS Code connection issues
|
||||
|
||||
VS Code connects to dev containers through the Coder extension. The extension
|
||||
uses the sub-agent information to route connections through the parent workspace
|
||||
agent to the dev container. If VS Code fails to connect:
|
||||
|
||||
1. Ensure you have the latest Coder VS Code extension.
|
||||
1. Verify the dev container is running in the Coder dashboard.
|
||||
1. Check the parent workspace agent is healthy.
|
||||
1. Try restarting the dev container from the dashboard.
|
||||
|
||||
## Dev container features not working
|
||||
|
||||
If features from your `devcontainer.json` aren't being applied:
|
||||
|
||||
1. Rebuild the container to ensure features are installed fresh.
|
||||
1. Check the container build output for feature installation errors.
|
||||
1. Verify the feature reference format is correct:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Slow container startup
|
||||
|
||||
If your dev container takes a long time to start:
|
||||
|
||||
1. **Use a pre-built image** instead of building from a Dockerfile. This avoids
|
||||
the image build step, though features and lifecycle scripts still run.
|
||||
1. **Minimize features**. Each feature executes as a separate Docker layer
|
||||
during the image build, which is typically the slowest part. Changing
|
||||
`devcontainer.json` invalidates the layer cache, causing features to
|
||||
reinstall on rebuild.
|
||||
1. **Check lifecycle scripts**. Commands in `postStartCommand` run on every
|
||||
container start. Commands in `postCreateCommand` run once per build, so
|
||||
they execute again after each rebuild.
|
||||
|
||||
## Getting more help
|
||||
|
||||
If you continue to experience issues:
|
||||
|
||||
1. Collect logs from `/tmp/coder-agent.log` (both workspace and container).
|
||||
1. Note the exact error messages.
|
||||
1. Check [Coder GitHub issues](https://github.com/coder/coder/issues) for
|
||||
similar problems.
|
||||
1. Contact your Coder administrator for template-specific issues.
|
||||
|
||||
@@ -3,95 +3,155 @@
|
||||
The dev container integration appears in your Coder dashboard, providing a
|
||||
visual representation of the running environment:
|
||||
|
||||

|
||||
_Dev containers appear as sub-agents with their own apps, SSH access, and port forwarding_
|
||||
|
||||
## SSH Access
|
||||
## SSH access
|
||||
|
||||
You can SSH into your dev container directly using the Coder CLI:
|
||||
Each dev container has its own agent name, derived from the workspace folder
|
||||
(e.g., `/home/coder/my-project` becomes `my-project`). You can find agent names
|
||||
in your workspace dashboard, or see
|
||||
[Agent naming](./index.md#agent-naming) for details on how names are generated.
|
||||
|
||||
### Using the Coder CLI
|
||||
|
||||
The simplest way to SSH into a dev container is using `coder ssh` with the
|
||||
workspace and agent name:
|
||||
|
||||
```console
|
||||
coder ssh --container keen_dijkstra my-workspace
|
||||
coder ssh <workspace>.<agent>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> SSH access is not yet compatible with the `coder config-ssh` command for use
|
||||
> with OpenSSH. You would need to manually modify your SSH config to include the
|
||||
> `--container` flag in the `ProxyCommand`.
|
||||
For example, to connect to a dev container with agent name `my-project` in
|
||||
workspace `my-workspace`:
|
||||
|
||||
## Web Terminal Access
|
||||
```console
|
||||
coder ssh my-workspace.my-project
|
||||
```
|
||||
|
||||
To SSH into the main workspace agent instead of the dev container:
|
||||
|
||||
```console
|
||||
coder ssh my-workspace
|
||||
```
|
||||
|
||||
### Using OpenSSH (config-ssh)
|
||||
|
||||
You can also use standard OpenSSH tools after generating SSH config entries with
|
||||
`coder config-ssh`:
|
||||
|
||||
```console
|
||||
coder config-ssh
|
||||
```
|
||||
|
||||
This creates a wildcard SSH host entry that matches all your workspaces and
|
||||
their agents, including dev container sub-agents. You can then connect using:
|
||||
|
||||
```console
|
||||
ssh my-project.my-workspace.me.coder
|
||||
```
|
||||
|
||||
The default hostname suffix is `.coder`. If your organization uses a different
|
||||
suffix, adjust the hostname accordingly. The suffix can be configured via
|
||||
[`coder config-ssh --hostname-suffix`](../../reference/cli/config-ssh.md) or
|
||||
by your deployment administrator.
|
||||
|
||||
This method works with any SSH client, IDE remote extensions, `rsync`, `scp`,
|
||||
and other tools that use SSH.
|
||||
|
||||
## Web terminal access
|
||||
|
||||
Once your workspace and dev container are running, you can use the web terminal
|
||||
in the Coder interface to execute commands directly inside the dev container.
|
||||
|
||||

|
||||
|
||||
## IDE Integration (VS Code)
|
||||
## IDE integration (VS Code)
|
||||
|
||||
You can open your dev container directly in VS Code by:
|
||||
|
||||
1. Selecting "Open in VS Code Desktop" from the Coder web interface
|
||||
2. Using the Coder CLI with the container flag:
|
||||
1. Selecting **Open in VS Code Desktop** from the dev container agent in the
|
||||
Coder web interface.
|
||||
1. Using the Coder CLI:
|
||||
|
||||
```console
|
||||
coder open vscode --container keen_dijkstra my-workspace
|
||||
```
|
||||
```console
|
||||
coder open vscode <workspace>.<agent>
|
||||
```
|
||||
|
||||
While optimized for VS Code, other IDEs with dev containers support may also
|
||||
For example:
|
||||
|
||||
```console
|
||||
coder open vscode my-workspace.my-project
|
||||
```
|
||||
|
||||
VS Code will automatically detect the dev container environment and connect
|
||||
appropriately.
|
||||
|
||||
While optimized for VS Code, other IDEs with dev container support may also
|
||||
work.
|
||||
|
||||
## Port Forwarding
|
||||
## Port forwarding
|
||||
|
||||
During the early access phase, port forwarding is limited to ports defined via
|
||||
Since dev containers run as sub-agents, you can forward ports directly to them
|
||||
using standard Coder port forwarding:
|
||||
|
||||
```console
|
||||
coder port-forward <workspace>.<agent> --tcp 8080
|
||||
```
|
||||
|
||||
For example, to forward port 8080 from a dev container with agent name
|
||||
`my-project`:
|
||||
|
||||
```console
|
||||
coder port-forward my-workspace.my-project --tcp 8080
|
||||
```
|
||||
|
||||
This forwards port 8080 on your local machine directly to port 8080 in the dev
|
||||
container. Coder also automatically detects ports opened inside the container.
|
||||
|
||||
### Exposing ports on the parent workspace
|
||||
|
||||
If you need to expose dev container ports through the parent workspace agent
|
||||
(rather than the sub-agent), you can use the
|
||||
[`appPort`](https://containers.dev/implementors/json_reference/#image-specific)
|
||||
in your `devcontainer.json` file.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Support for automatic port forwarding via the `forwardPorts` property in
|
||||
> `devcontainer.json` is planned for a future release.
|
||||
|
||||
For example, with this `devcontainer.json` configuration:
|
||||
property in your `devcontainer.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"appPort": ["8080:8080", "4000:3000"]
|
||||
"appPort": ["8080:8080", "4000:3000"]
|
||||
}
|
||||
```
|
||||
|
||||
You can forward these ports to your local machine using:
|
||||
This maps container ports to the parent workspace, which can then be forwarded
|
||||
using the main workspace agent.
|
||||
|
||||
```console
|
||||
coder port-forward my-workspace --tcp 8080,4000
|
||||
```
|
||||
## Dev container features
|
||||
|
||||
This forwards port 8080 (local) -> 8080 (agent) -> 8080 (dev container) and port
|
||||
4000 (local) -> 4000 (agent) -> 3000 (dev container).
|
||||
|
||||
## Dev Container Features
|
||||
|
||||
You can use standard dev container features in your `devcontainer.json` file.
|
||||
Coder also maintains a
|
||||
You can use standard [dev container features](https://containers.dev/features)
|
||||
in your `devcontainer.json` file. Coder also maintains a
|
||||
[repository of features](https://github.com/coder/devcontainer-features) to
|
||||
enhance your development experience.
|
||||
|
||||
Currently available features include [code-server](https://github.com/coder/devcontainer-features/blob/main/src/code-server).
|
||||
|
||||
To use the code-server feature, add the following to your `devcontainer.json`:
|
||||
For example, the
|
||||
[code-server](https://github.com/coder/devcontainer-features/blob/main/src/code-server)
|
||||
feature from the [Coder features repository](https://github.com/coder/devcontainer-features):
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 13337,
|
||||
"host": "0.0.0.0"
|
||||
}
|
||||
},
|
||||
"appPort": ["13337:13337"]
|
||||
"features": {
|
||||
"ghcr.io/coder/devcontainer-features/code-server:1": {
|
||||
"port": 13337,
|
||||
"host": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Remember to include the port in the `appPort` section to ensure proper port
|
||||
> forwarding.
|
||||
## Rebuilding dev containers
|
||||
|
||||
When you modify your `devcontainer.json`, you need to rebuild the container for
|
||||
changes to take effect. Coder detects changes and shows an **Outdated** status
|
||||
next to the dev container.
|
||||
|
||||
_The Outdated indicator appears when changes to devcontainer.json are detected_
|
||||
|
||||
Click **Rebuild** to recreate your dev container with the updated configuration.
|
||||
|
||||
@@ -7,7 +7,7 @@ These are intended for end-user flows only. If you are an administrator, please
|
||||
refer to our docs on configuring [templates](../admin/index.md) or the
|
||||
[control plane](../admin/index.md).
|
||||
|
||||
Check out our [early access features](../install/releases/feature-stages.md) for upcoming
|
||||
functionality, including [Dev Containers integration](../user-guides/devcontainers/index.md).
|
||||
Check out [Dev Containers integration](./devcontainers/index.md) for running
|
||||
containerized development environments in your Coder workspace.
|
||||
|
||||
<children></children>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
## JetBrains Gateway
|
||||
|
||||
> [! WARNING]
|
||||
> Using Coder through JetBrains Gateway is not recommended at this time. Instead, we suggest using [JetBrains Toolbox](https://coder.com/docs/user-guides/workspace-access/jetbrains/toolbox) for stability and performance benefits. If you are currently using Gateway, we recommend [migration](https://www.jetbrains.com/help/toolbox-app/jetbrains-gateway-migrations-guide.html).
|
||||
|
||||
JetBrains Gateway is a compact desktop app that allows you to work remotely with
|
||||
a JetBrains IDE without downloading one. Visit the
|
||||
[JetBrains Gateway website](https://www.jetbrains.com/remote-development/gateway/)
|
||||
|
||||
@@ -104,14 +104,14 @@ data "coder_workspace_owner" "me" {}
|
||||
|
||||
module "slackme" {
|
||||
source = "dev.registry.coder.com/coder/slackme/coder"
|
||||
version = "1.0.32"
|
||||
version = "1.0.33"
|
||||
agent_id = coder_agent.dev.id
|
||||
auth_provider_id = "slack"
|
||||
}
|
||||
|
||||
module "dotfiles" {
|
||||
source = "dev.registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ RUN sed -i 's|http://archive.ubuntu.com/ubuntu/|http://mirrors.edge.kernel.org/u
|
||||
|
||||
# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.12.2.
|
||||
# Installing the same version here to match.
|
||||
RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.13.4/terraform_1.13.4_linux_amd64.zip" && \
|
||||
RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.14.1/terraform_1.14.1_linux_amd64.zip" && \
|
||||
unzip /tmp/terraform.zip -d /usr/local/bin && \
|
||||
rm -f /tmp/terraform.zip && \
|
||||
chmod +x /usr/local/bin/terraform && \
|
||||
|
||||
+22
-7
@@ -333,7 +333,7 @@ data "coder_parameter" "vscode_channel" {
|
||||
module "slackme" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/coder/slackme/coder"
|
||||
version = "1.0.32"
|
||||
version = "1.0.33"
|
||||
agent_id = coder_agent.dev.id
|
||||
auth_provider_id = "slack"
|
||||
}
|
||||
@@ -341,7 +341,7 @@ module "slackme" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ module "git-config" {
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "dev.registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
agent_id = coder_agent.dev.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = local.repo_base_dir
|
||||
@@ -373,7 +373,7 @@ module "personalize" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
agent_id = coder_agent.dev.id
|
||||
subdomain = true
|
||||
}
|
||||
@@ -391,7 +391,7 @@ module "code-server" {
|
||||
module "vscode-web" {
|
||||
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "vscode-web") ? data.coder_workspace.me.start_count : 0
|
||||
source = "dev.registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
agent_id = coder_agent.dev.id
|
||||
folder = local.repo_dir
|
||||
extensions = ["github.copilot"]
|
||||
@@ -429,7 +429,7 @@ module "coder-login" {
|
||||
module "cursor" {
|
||||
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "cursor") ? data.coder_workspace.me.start_count : 0
|
||||
source = "dev.registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.dev.id
|
||||
folder = local.repo_dir
|
||||
}
|
||||
@@ -437,7 +437,7 @@ module "cursor" {
|
||||
module "windsurf" {
|
||||
count = contains(jsondecode(data.coder_parameter.ide_choices.value), "windsurf") ? data.coder_workspace.me.start_count : 0
|
||||
source = "dev.registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.dev.id
|
||||
folder = local.repo_dir
|
||||
}
|
||||
@@ -596,6 +596,14 @@ resource "coder_agent" "dev" {
|
||||
# Allow synchronization between scripts.
|
||||
trap 'touch /tmp/.coder-startup-script.done' EXIT
|
||||
|
||||
# Authenticate GitHub CLI
|
||||
if ! gh auth status >/dev/null 2>&1; then
|
||||
echo "Logging into GitHub CLI…"
|
||||
coder external-auth access-token github | gh auth login --hostname github.com --with-token
|
||||
else
|
||||
echo "Already logged into GitHub CLI."
|
||||
fi
|
||||
|
||||
# Increase the shutdown timeout of the docker service for improved cleanup.
|
||||
# The 240 was picked as it's lower than the 300 seconds we set for the
|
||||
# container shutdown grace period.
|
||||
@@ -831,6 +839,13 @@ locals {
|
||||
- Built-in tools - use for everything else:
|
||||
(file operations, git commands, builds & installs, one-off shell commands)
|
||||
|
||||
-- Workflow --
|
||||
When starting new work:
|
||||
1. If given a GitHub issue URL, use the `gh` CLI to read the full issue details with `gh issue view <issue-number>`.
|
||||
2. Create a feature branch for the work using a descriptive name based on the issue or task.
|
||||
Example: `git checkout -b fix/issue-123-oauth-error` or `git checkout -b feat/add-dark-mode`
|
||||
3. Proceed with implementation following the CLAUDE.md guidelines.
|
||||
|
||||
-- Context --
|
||||
There is an existing application in the current directory.
|
||||
Be sure to read CLAUDE.md before making any changes.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
@@ -33,6 +34,7 @@ type Server struct {
|
||||
requestBridgePool Pooler
|
||||
|
||||
logger slog.Logger
|
||||
tracer trace.Tracer
|
||||
wg sync.WaitGroup
|
||||
|
||||
// initConnectionCh will receive when the daemon connects to coderd for the
|
||||
@@ -48,7 +50,7 @@ type Server struct {
|
||||
shutdownOnce sync.Once
|
||||
}
|
||||
|
||||
func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger) (*Server, error) {
|
||||
func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger, tracer trace.Tracer) (*Server, error) {
|
||||
if rpcDialer == nil {
|
||||
return nil, xerrors.Errorf("nil rpcDialer given")
|
||||
}
|
||||
@@ -56,6 +58,7 @@ func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
daemon := &Server{
|
||||
logger: logger,
|
||||
tracer: tracer,
|
||||
clientDialer: rpcDialer,
|
||||
clientCh: make(chan DRPCClient),
|
||||
lifecycleCtx: ctx,
|
||||
@@ -143,7 +146,7 @@ func (s *Server) GetRequestHandler(ctx context.Context, req Request) (http.Handl
|
||||
return nil, xerrors.New("nil requestBridgePool")
|
||||
}
|
||||
|
||||
reqBridge, err := s.requestBridgePool.Acquire(ctx, req, s.Client, NewMCPProxyFactory(s.logger, s.Client))
|
||||
reqBridge, err := s.requestBridgePool.Acquire(ctx, req, s.Client, NewMCPProxyFactory(s.logger, s.tracer, s.Client))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("acquire request bridge: %w", err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -13,8 +14,13 @@ import (
|
||||
promtest "github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
|
||||
"github.com/coder/aibridge"
|
||||
aibtracing "github.com/coder/aibridge/tracing"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
@@ -28,6 +34,8 @@ import (
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
var testTracer = otel.Tracer("aibridged_test")
|
||||
|
||||
// TestIntegration is not an exhaustive test against the upstream AI providers' SDKs (see coder/aibridge for those).
|
||||
// This test validates that:
|
||||
// - intercepted requests can be authenticated/authorized
|
||||
@@ -35,11 +43,17 @@ import (
|
||||
// - responses can be returned as expected
|
||||
// - interceptions are logged, as well as their related prompt, token, and tool calls
|
||||
// - MCP server configurations are returned as expected
|
||||
// - tracing spans are properly recorded
|
||||
func TestIntegration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
sr := tracetest.NewSpanRecorder()
|
||||
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
|
||||
tracer := tp.Tracer(t.Name())
|
||||
defer func() { _ = tp.Shutdown(t.Context()) }()
|
||||
|
||||
// Create mock MCP server.
|
||||
var mcpTokenReceived string
|
||||
mockMCPServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -169,13 +183,13 @@ func TestIntegration(t *testing.T) {
|
||||
|
||||
logger := testutil.Logger(t)
|
||||
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})}
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, nil, logger)
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, nil, tracer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Given: aibridged is started.
|
||||
srv, err := aibridged.New(t.Context(), pool, func(ctx context.Context) (aibridged.DRPCClient, error) {
|
||||
return aiBridgeClient, nil
|
||||
}, logger)
|
||||
}, logger, tracer)
|
||||
require.NoError(t, err, "create new aibridged")
|
||||
t.Cleanup(func() {
|
||||
_ = srv.Shutdown(ctx)
|
||||
@@ -256,6 +270,44 @@ func TestIntegration(t *testing.T) {
|
||||
|
||||
// Then: the MCP server was initialized.
|
||||
require.Contains(t, mcpTokenReceived, authLink.OAuthAccessToken, "mock MCP server not requested")
|
||||
|
||||
// Then: verify tracing spans were recorded.
|
||||
spans := sr.Ended()
|
||||
require.NotEmpty(t, spans)
|
||||
i := slices.IndexFunc(spans, func(s sdktrace.ReadOnlySpan) bool { return s.Name() == "CachedBridgePool.Acquire" })
|
||||
require.NotEqual(t, -1, i, "span named 'CachedBridgePool.Acquire' not found")
|
||||
|
||||
expectAttrs := []attribute.KeyValue{
|
||||
attribute.String(aibtracing.InitiatorID, user.ID.String()),
|
||||
attribute.String(aibtracing.APIKeyID, keyID),
|
||||
}
|
||||
require.Equal(t, spans[i].Attributes(), expectAttrs)
|
||||
|
||||
// Check for aibridge spans.
|
||||
spanNames := make(map[string]bool)
|
||||
for _, span := range spans {
|
||||
spanNames[span.Name()] = true
|
||||
}
|
||||
|
||||
expectedAibridgeSpans := []string{
|
||||
"CachedBridgePool.Acquire",
|
||||
"ServerProxyManager.Init",
|
||||
"StreamableHTTPServerProxy.Init",
|
||||
"StreamableHTTPServerProxy.Init.fetchTools",
|
||||
"Intercept",
|
||||
"Intercept.CreateInterceptor",
|
||||
"Intercept.RecordInterception",
|
||||
"Intercept.ProcessRequest",
|
||||
"Intercept.ProcessRequest.Upstream",
|
||||
"Intercept.RecordPromptUsage",
|
||||
"Intercept.RecordTokenUsage",
|
||||
"Intercept.RecordToolUsage",
|
||||
"Intercept.RecordInterceptionEnded",
|
||||
}
|
||||
|
||||
for _, expectedSpan := range expectedAibridgeSpans {
|
||||
require.Contains(t, spanNames, expectedSpan)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationWithMetrics validates that Prometheus metrics are correctly incremented
|
||||
@@ -324,13 +376,13 @@ func TestIntegrationWithMetrics(t *testing.T) {
|
||||
providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})}
|
||||
|
||||
// Create pool with metrics.
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, metrics, logger)
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, metrics, testTracer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Given: aibridged is started.
|
||||
srv, err := aibridged.New(ctx, pool, func(ctx context.Context) (aibridged.DRPCClient, error) {
|
||||
return aiBridgeClient, nil
|
||||
}, logger)
|
||||
}, logger, testTracer)
|
||||
require.NoError(t, err, "create new aibridged")
|
||||
t.Cleanup(func() {
|
||||
_ = srv.Shutdown(ctx)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user