Merge branch 'main' into jetbrains
This commit is contained in:
commit
c977142180
194
CONTRIBUTING.md
194
CONTRIBUTING.md
@ -4,12 +4,14 @@ Welcome! This guide covers how to contribute to the Coder Registry, whether you'
|
|||||||
|
|
||||||
## What is the Coder Registry?
|
## What is the Coder Registry?
|
||||||
|
|
||||||
The Coder Registry is a collection of Terraform modules that extend Coder workspaces with development tools like VS Code, Cursor, JetBrains IDEs, and more.
|
The Coder Registry is a collection of Terraform modules and templates for Coder workspaces. Modules provide IDEs, authentication integrations, development tools, and other workspace functionality. Templates provide complete workspace configurations for different platforms and use cases that appear as community templates on the registry website.
|
||||||
|
|
||||||
## Types of Contributions
|
## Types of Contributions
|
||||||
|
|
||||||
- **[New Modules](#creating-a-new-module)** - Add support for a new tool or functionality
|
- **[New Modules](#creating-a-new-module)** - Add support for a new tool or functionality
|
||||||
|
- **[New Templates](#creating-a-new-template)** - Create complete workspace configurations
|
||||||
- **[Existing Modules](#contributing-to-existing-modules)** - Fix bugs, add features, or improve documentation
|
- **[Existing Modules](#contributing-to-existing-modules)** - Fix bugs, add features, or improve documentation
|
||||||
|
- **[Existing Templates](#contributing-to-existing-templates)** - Improve workspace templates
|
||||||
- **[Bug Reports](#reporting-issues)** - Report problems or request features
|
- **[Bug Reports](#reporting-issues)** - Report problems or request features
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
@ -36,7 +38,15 @@ bun install
|
|||||||
|
|
||||||
### Understanding Namespaces
|
### Understanding Namespaces
|
||||||
|
|
||||||
All modules are organized under `/registry/[namespace]/modules/`. Each contributor gets their own namespace (e.g., `/registry/your-username/modules/`). If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
|
All modules and templates are organized under `/registry/[namespace]/`. Each contributor gets their own namespace with both modules and templates directories:
|
||||||
|
|
||||||
|
```
|
||||||
|
registry/[namespace]/
|
||||||
|
├── modules/ # Individual components and tools
|
||||||
|
└── templates/ # Complete workspace configurations
|
||||||
|
```
|
||||||
|
|
||||||
|
For example: `/registry/your-username/modules/` and `/registry/your-username/templates/`. If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
|
||||||
|
|
||||||
### Images and Icons
|
### Images and Icons
|
||||||
|
|
||||||
@ -136,15 +146,171 @@ git push origin your-branch
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing to Existing Modules
|
## Creating a New Template
|
||||||
|
|
||||||
### 1. Find the Module
|
Templates are complete Coder workspace configurations that users can deploy directly. Unlike modules (which are components), templates provide full infrastructure definitions for specific platforms or use cases.
|
||||||
|
|
||||||
```bash
|
### Template Structure
|
||||||
find registry -name "*[module-name]*" -type d
|
|
||||||
|
Templates follow the same namespace structure as modules but are located in the `templates` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
registry/[your-username]/templates/[template-name]/
|
||||||
|
├── main.tf # Complete Terraform configuration
|
||||||
|
├── README.md # Documentation with frontmatter
|
||||||
|
├── [additional files] # Scripts, configs, etc.
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Make Your Changes
|
### 1. Create Your Template Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p registry/[your-username]/templates/[template-name]
|
||||||
|
cd registry/[your-username]/templates/[template-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Template Files
|
||||||
|
|
||||||
|
#### main.tf
|
||||||
|
|
||||||
|
Your `main.tf` should be a complete Coder template configuration including:
|
||||||
|
|
||||||
|
- Required providers (coder, and your infrastructure provider)
|
||||||
|
- Coder agent configuration
|
||||||
|
- Infrastructure resources (containers, VMs, etc.)
|
||||||
|
- Registry modules for IDEs, tools, and integrations
|
||||||
|
|
||||||
|
Example structure:
|
||||||
|
|
||||||
|
```terraform
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
}
|
||||||
|
# Add your infrastructure provider (docker, aws, etc.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Coder data sources
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
# Coder agent
|
||||||
|
resource "coder_agent" "main" {
|
||||||
|
arch = "amd64"
|
||||||
|
os = "linux"
|
||||||
|
startup_script = <<-EOT
|
||||||
|
# Startup commands here
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Registry modules for IDEs, tools, and integrations
|
||||||
|
module "code-server" {
|
||||||
|
source = "registry.coder.com/coder/code-server/coder"
|
||||||
|
version = "~> 1.0"
|
||||||
|
agent_id = coder_agent.main.id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Your infrastructure resources
|
||||||
|
# (Docker containers, AWS instances, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### README.md
|
||||||
|
|
||||||
|
Create documentation with proper frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
display_name: "Template Name"
|
||||||
|
description: "Brief description of what this template provides"
|
||||||
|
icon: "../../../../.icons/platform.svg"
|
||||||
|
verified: false
|
||||||
|
tags: ["platform", "use-case", "tools"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Template Name
|
||||||
|
|
||||||
|
Describe what the template provides and how to use it.
|
||||||
|
|
||||||
|
Include any setup requirements, resource information, or usage notes that users need to know.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Your Template
|
||||||
|
|
||||||
|
Templates should be tested to ensure they work correctly. Test with Coder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd registry/[your-username]/templates/[template-name]
|
||||||
|
coder templates push [template-name] -d .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Template Best Practices
|
||||||
|
|
||||||
|
- **Use registry modules**: Leverage existing modules for IDEs, tools, and integrations
|
||||||
|
- **Provide sensible defaults**: Make the template work out-of-the-box
|
||||||
|
- **Include metadata**: Add useful workspace metadata (CPU, memory, disk usage)
|
||||||
|
- **Document prerequisites**: Clearly explain infrastructure requirements
|
||||||
|
- **Use variables**: Allow customization of common settings
|
||||||
|
- **Follow naming conventions**: Use descriptive, consistent naming
|
||||||
|
|
||||||
|
### 5. Template Guidelines
|
||||||
|
|
||||||
|
- Templates appear as "Community Templates" on the registry website
|
||||||
|
- Include proper error handling and validation
|
||||||
|
- Test with Coder before submitting
|
||||||
|
- Document any required permissions or setup steps
|
||||||
|
- Use semantic versioning in your README frontmatter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing to Existing Templates
|
||||||
|
|
||||||
|
### 1. Types of Template Improvements
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
|
||||||
|
- Fix infrastructure provisioning issues
|
||||||
|
- Resolve agent connectivity problems
|
||||||
|
- Correct resource naming or tagging
|
||||||
|
|
||||||
|
**Feature additions:**
|
||||||
|
|
||||||
|
- Add new registry modules for additional functionality
|
||||||
|
- Include additional infrastructure options
|
||||||
|
- Improve startup scripts or automation
|
||||||
|
|
||||||
|
**Platform updates:**
|
||||||
|
|
||||||
|
- Update base images or AMIs
|
||||||
|
- Adapt to new platform features
|
||||||
|
- Improve security configurations
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
|
||||||
|
- Clarify prerequisites and setup steps
|
||||||
|
- Add troubleshooting guides
|
||||||
|
- Improve usage examples
|
||||||
|
|
||||||
|
### 2. Testing Template Changes
|
||||||
|
|
||||||
|
Testing template modifications thoroughly is necessary. Test with Coder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
coder templates push test-[template-name] -d .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Maintain Compatibility
|
||||||
|
|
||||||
|
- Don't remove existing variables without clear migration path
|
||||||
|
- Preserve backward compatibility when possible
|
||||||
|
- Test that existing workspaces still function
|
||||||
|
- Document any breaking changes clearly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing to Existing Modules
|
||||||
|
|
||||||
|
### 1. Make Your Changes
|
||||||
|
|
||||||
**For bug fixes:**
|
**For bug fixes:**
|
||||||
|
|
||||||
@ -166,7 +332,7 @@ find registry -name "*[module-name]*" -type d
|
|||||||
- Add missing variable documentation
|
- Add missing variable documentation
|
||||||
- Improve usage examples
|
- Improve usage examples
|
||||||
|
|
||||||
### 3. Test Your Changes
|
### 2. Test Your Changes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Test a specific module
|
# Test a specific module
|
||||||
@ -176,7 +342,7 @@ bun test -t 'module-name'
|
|||||||
bun test
|
bun test
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Maintain Backward Compatibility
|
### 3. Maintain Backward Compatibility
|
||||||
|
|
||||||
- New variables should have default values
|
- New variables should have default values
|
||||||
- Don't break existing functionality
|
- Don't break existing functionality
|
||||||
@ -208,6 +374,7 @@ bun test
|
|||||||
We have different PR templates for different types of contributions. GitHub will show you options to choose from, or you can manually select:
|
We have different PR templates for different types of contributions. GitHub will show you options to choose from, or you can manually select:
|
||||||
|
|
||||||
- **New Module**: Use `?template=new_module.md`
|
- **New Module**: Use `?template=new_module.md`
|
||||||
|
- **New Template**: Use `?template=new_template.md`
|
||||||
- **Bug Fix**: Use `?template=bug_fix.md`
|
- **Bug Fix**: Use `?template=bug_fix.md`
|
||||||
- **Feature**: Use `?template=feature.md`
|
- **Feature**: Use `?template=feature.md`
|
||||||
- **Documentation**: Use `?template=documentation.md`
|
- **Documentation**: Use `?template=documentation.md`
|
||||||
@ -224,6 +391,13 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
|
|||||||
- `main.test.ts` - Working tests
|
- `main.test.ts` - Working tests
|
||||||
- `README.md` - Documentation with frontmatter
|
- `README.md` - Documentation with frontmatter
|
||||||
|
|
||||||
|
### Every Template Must Have
|
||||||
|
|
||||||
|
- `main.tf` - Complete Terraform configuration
|
||||||
|
- `README.md` - Documentation with frontmatter
|
||||||
|
|
||||||
|
Templates don't require test files like modules do, but should be manually tested before submission.
|
||||||
|
|
||||||
### README Frontmatter
|
### README Frontmatter
|
||||||
|
|
||||||
Module README frontmatter must include:
|
Module README frontmatter must include:
|
||||||
@ -304,7 +478,7 @@ When reporting bugs, include:
|
|||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
- **Examples**: Check `/registry/coder/modules/` for well-structured modules
|
- **Examples**: Check `/registry/coder/modules/` for well-structured modules and `/registry/coder/templates/` for complete templates
|
||||||
- **Issues**: Open an issue for technical problems
|
- **Issues**: Open an issue for technical problems
|
||||||
- **Community**: Reach out to the Coder community for questions
|
- **Community**: Reach out to the Coder community for questions
|
||||||
|
|
||||||
|
|||||||
6
go.mod
6
go.mod
@ -20,7 +20,7 @@ require (
|
|||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
golang.org/x/crypto v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/term v0.10.0 // indirect
|
golang.org/x/term v0.29.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
20
go.sum
20
go.sum
@ -51,17 +51,17 @@ go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiM
|
|||||||
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
||||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
|
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
|
||||||
|
|||||||
54
registry/coder/modules/agentapi/README.md
Normal file
54
registry/coder/modules/agentapi/README.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
display_name: AgentAPI
|
||||||
|
description: Building block for modules that need to run an agentapi server
|
||||||
|
icon: ../../../../.icons/coder.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [internal]
|
||||||
|
---
|
||||||
|
|
||||||
|
# AgentAPI
|
||||||
|
|
||||||
|
The AgentAPI module is a building block for modules that need to run an agentapi server. It is intended primarily for internal use by Coder to create modules compatible with Tasks.
|
||||||
|
|
||||||
|
We do not recommend using this module directly. Instead, please consider using one of our [Tasks-compatible AI agent modules](https://registry.coder.com/modules?search=tag%3Atasks).
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "agentapi" {
|
||||||
|
source = "registry.coder.com/coder/agentapi/coder"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
agent_id = var.agent_id
|
||||||
|
web_app_slug = local.app_slug
|
||||||
|
web_app_order = var.order
|
||||||
|
web_app_group = var.group
|
||||||
|
web_app_icon = var.icon
|
||||||
|
web_app_display_name = "Goose"
|
||||||
|
cli_app_slug = "goose-cli"
|
||||||
|
cli_app_display_name = "Goose CLI"
|
||||||
|
module_dir_name = local.module_dir_name
|
||||||
|
install_agentapi = var.install_agentapi
|
||||||
|
pre_install_script = var.pre_install_script
|
||||||
|
post_install_script = var.post_install_script
|
||||||
|
start_script = local.start_script
|
||||||
|
install_script = <<-EOT
|
||||||
|
#!/bin/bash
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||||
|
chmod +x /tmp/install.sh
|
||||||
|
|
||||||
|
ARG_PROVIDER='${var.goose_provider}' \
|
||||||
|
ARG_MODEL='${var.goose_model}' \
|
||||||
|
ARG_GOOSE_CONFIG="$(echo -n '${base64encode(local.combined_extensions)}' | base64 -d)" \
|
||||||
|
ARG_INSTALL='${var.install_goose}' \
|
||||||
|
ARG_GOOSE_VERSION='${var.goose_version}' \
|
||||||
|
/tmp/install.sh
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## For module developers
|
||||||
|
|
||||||
|
For a complete example of how to use this module, see the [goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
|
||||||
151
registry/coder/modules/agentapi/main.test.ts
Normal file
151
registry/coder/modules/agentapi/main.test.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
test,
|
||||||
|
afterEach,
|
||||||
|
expect,
|
||||||
|
describe,
|
||||||
|
setDefaultTimeout,
|
||||||
|
beforeAll,
|
||||||
|
} from "bun:test";
|
||||||
|
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||||
|
import {
|
||||||
|
loadTestFile,
|
||||||
|
writeExecutable,
|
||||||
|
setup as setupUtil,
|
||||||
|
execModuleScript,
|
||||||
|
expectAgentAPIStarted,
|
||||||
|
} from "./test-util";
|
||||||
|
|
||||||
|
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||||
|
cleanupFunctions.push(cleanup);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup logic depends on the fact that bun's built-in test runner
|
||||||
|
// runs tests sequentially.
|
||||||
|
// https://bun.sh/docs/test/discovery#execution-order
|
||||||
|
// Weird things would happen if tried to run tests in parallel.
|
||||||
|
// One test could clean up resources that another test was still using.
|
||||||
|
afterEach(async () => {
|
||||||
|
// reverse the cleanup functions so that they are run in the correct order
|
||||||
|
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||||
|
cleanupFunctions = [];
|
||||||
|
for (const cleanup of cleanupFnsCopy) {
|
||||||
|
try {
|
||||||
|
await cleanup();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during cleanup:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SetupProps {
|
||||||
|
skipAgentAPIMock?: boolean;
|
||||||
|
moduleVariables?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleDirName = ".agentapi-module";
|
||||||
|
|
||||||
|
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||||
|
const projectDir = "/home/coder/project";
|
||||||
|
const { id } = await setupUtil({
|
||||||
|
moduleVariables: {
|
||||||
|
experiment_report_tasks: "true",
|
||||||
|
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||||
|
web_app_display_name: "AgentAPI Web",
|
||||||
|
web_app_slug: "agentapi-web",
|
||||||
|
web_app_icon: "/icon/coder.svg",
|
||||||
|
cli_app_display_name: "AgentAPI CLI",
|
||||||
|
cli_app_slug: "agentapi-cli",
|
||||||
|
agentapi_version: "latest",
|
||||||
|
module_dir_name: moduleDirName,
|
||||||
|
start_script: await loadTestFile(import.meta.dir, "agentapi-start.sh"),
|
||||||
|
folder: projectDir,
|
||||||
|
...props?.moduleVariables,
|
||||||
|
},
|
||||||
|
registerCleanup,
|
||||||
|
projectDir,
|
||||||
|
skipAgentAPIMock: props?.skipAgentAPIMock,
|
||||||
|
moduleDir: import.meta.dir,
|
||||||
|
});
|
||||||
|
await writeExecutable({
|
||||||
|
containerId: id,
|
||||||
|
filePath: "/usr/bin/aiagent",
|
||||||
|
content: await loadTestFile(import.meta.dir, "ai-agent-mock.js"),
|
||||||
|
});
|
||||||
|
return { id };
|
||||||
|
};
|
||||||
|
|
||||||
|
// increase the default timeout to 60 seconds
|
||||||
|
setDefaultTimeout(60 * 1000);
|
||||||
|
|
||||||
|
// we don't run these tests in CI because they take too long and make network
|
||||||
|
// calls. they are dedicated for local development.
|
||||||
|
describe("agentapi", async () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("happy-path", async () => {
|
||||||
|
const { id } = await setup();
|
||||||
|
|
||||||
|
await execModuleScript(id);
|
||||||
|
|
||||||
|
await expectAgentAPIStarted(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("custom-port", async () => {
|
||||||
|
const { id } = await setup({
|
||||||
|
moduleVariables: {
|
||||||
|
agentapi_port: "3827",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await execModuleScript(id);
|
||||||
|
await expectAgentAPIStarted(id, 3827);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pre-post-install-scripts", async () => {
|
||||||
|
const { id } = await setup({
|
||||||
|
moduleVariables: {
|
||||||
|
pre_install_script: `#!/bin/bash\necho "pre-install"`,
|
||||||
|
install_script: `#!/bin/bash\necho "install"`,
|
||||||
|
post_install_script: `#!/bin/bash\necho "post-install"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await execModuleScript(id);
|
||||||
|
await expectAgentAPIStarted(id);
|
||||||
|
|
||||||
|
const preInstallLog = await readFileContainer(
|
||||||
|
id,
|
||||||
|
`/home/coder/${moduleDirName}/pre_install.log`,
|
||||||
|
);
|
||||||
|
const installLog = await readFileContainer(
|
||||||
|
id,
|
||||||
|
`/home/coder/${moduleDirName}/install.log`,
|
||||||
|
);
|
||||||
|
const postInstallLog = await readFileContainer(
|
||||||
|
id,
|
||||||
|
`/home/coder/${moduleDirName}/post_install.log`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(preInstallLog).toContain("pre-install");
|
||||||
|
expect(installLog).toContain("install");
|
||||||
|
expect(postInstallLog).toContain("post-install");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("install-agentapi", async () => {
|
||||||
|
const { id } = await setup({ skipAgentAPIMock: true });
|
||||||
|
|
||||||
|
const respModuleScript = await execModuleScript(id);
|
||||||
|
expect(respModuleScript.exitCode).toBe(0);
|
||||||
|
|
||||||
|
await expectAgentAPIStarted(id);
|
||||||
|
const respAgentAPI = await execContainer(id, [
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
"agentapi --version",
|
||||||
|
]);
|
||||||
|
expect(respAgentAPI.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
213
registry/coder/modules/agentapi/main.tf
Normal file
213
registry/coder/modules/agentapi/main.tf
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 2.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
variable "web_app_order" {
|
||||||
|
type = number
|
||||||
|
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "web_app_group" {
|
||||||
|
type = string
|
||||||
|
description = "The name of a group that this app belongs to."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "web_app_icon" {
|
||||||
|
type = string
|
||||||
|
description = "The icon to use for the app."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "web_app_display_name" {
|
||||||
|
type = string
|
||||||
|
description = "The display name of the web app."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "web_app_slug" {
|
||||||
|
type = string
|
||||||
|
description = "The slug of the web app."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "folder" {
|
||||||
|
type = string
|
||||||
|
description = "The folder to run AgentAPI in."
|
||||||
|
default = "/home/coder"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cli_app" {
|
||||||
|
type = bool
|
||||||
|
description = "Whether to create the CLI workspace app."
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cli_app_order" {
|
||||||
|
type = number
|
||||||
|
description = "The order of the CLI workspace app."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cli_app_group" {
|
||||||
|
type = string
|
||||||
|
description = "The group of the CLI workspace app."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cli_app_icon" {
|
||||||
|
type = string
|
||||||
|
description = "The icon to use for the app."
|
||||||
|
default = "/icon/claude.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cli_app_display_name" {
|
||||||
|
type = string
|
||||||
|
description = "The display name of the CLI workspace app."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cli_app_slug" {
|
||||||
|
type = string
|
||||||
|
description = "The slug of the CLI workspace app."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "pre_install_script" {
|
||||||
|
type = string
|
||||||
|
description = "Custom script to run before installing the agent used by AgentAPI."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "install_script" {
|
||||||
|
type = string
|
||||||
|
description = "Script to install the agent used by AgentAPI."
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "post_install_script" {
|
||||||
|
type = string
|
||||||
|
description = "Custom script to run after installing the agent used by AgentAPI."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "start_script" {
|
||||||
|
type = string
|
||||||
|
description = "Script that starts AgentAPI."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "install_agentapi" {
|
||||||
|
type = bool
|
||||||
|
description = "Whether to install AgentAPI."
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agentapi_version" {
|
||||||
|
type = string
|
||||||
|
description = "The version of AgentAPI to install."
|
||||||
|
default = "v0.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agentapi_port" {
|
||||||
|
type = number
|
||||||
|
description = "The port used by AgentAPI."
|
||||||
|
default = 3284
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "module_dir_name" {
|
||||||
|
type = string
|
||||||
|
description = "Name of the subdirectory in the home directory for module files."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# we always trim the slash for consistency
|
||||||
|
workdir = trimsuffix(var.folder, "/")
|
||||||
|
encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : ""
|
||||||
|
encoded_install_script = var.install_script != null ? base64encode(var.install_script) : ""
|
||||||
|
encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : ""
|
||||||
|
agentapi_start_script_b64 = base64encode(var.start_script)
|
||||||
|
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
|
||||||
|
main_script = file("${path.module}/scripts/main.sh")
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "agentapi" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "Install and start AgentAPI"
|
||||||
|
icon = var.web_app_icon
|
||||||
|
script = <<-EOT
|
||||||
|
#!/bin/bash
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
echo -n '${base64encode(local.main_script)}' | base64 -d > /tmp/main.sh
|
||||||
|
chmod +x /tmp/main.sh
|
||||||
|
|
||||||
|
ARG_MODULE_DIR_NAME='${var.module_dir_name}' \
|
||||||
|
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
|
||||||
|
ARG_PRE_INSTALL_SCRIPT="$(echo -n '${local.encoded_pre_install_script}' | base64 -d)" \
|
||||||
|
ARG_INSTALL_SCRIPT="$(echo -n '${local.encoded_install_script}' | base64 -d)" \
|
||||||
|
ARG_INSTALL_AGENTAPI='${var.install_agentapi}' \
|
||||||
|
ARG_AGENTAPI_VERSION='${var.agentapi_version}' \
|
||||||
|
ARG_START_SCRIPT="$(echo -n '${local.agentapi_start_script_b64}' | base64 -d)" \
|
||||||
|
ARG_WAIT_FOR_START_SCRIPT="$(echo -n '${local.agentapi_wait_for_start_script_b64}' | base64 -d)" \
|
||||||
|
ARG_POST_INSTALL_SCRIPT="$(echo -n '${local.encoded_post_install_script}' | base64 -d)" \
|
||||||
|
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
|
||||||
|
/tmp/main.sh
|
||||||
|
EOT
|
||||||
|
run_on_start = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "agentapi_web" {
|
||||||
|
slug = var.web_app_slug
|
||||||
|
display_name = var.web_app_display_name
|
||||||
|
agent_id = var.agent_id
|
||||||
|
url = "http://localhost:${var.agentapi_port}/"
|
||||||
|
icon = var.web_app_icon
|
||||||
|
order = var.web_app_order
|
||||||
|
group = var.web_app_group
|
||||||
|
subdomain = true
|
||||||
|
healthcheck {
|
||||||
|
url = "http://localhost:${var.agentapi_port}/status"
|
||||||
|
interval = 3
|
||||||
|
threshold = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "agentapi_cli" {
|
||||||
|
count = var.cli_app ? 1 : 0
|
||||||
|
|
||||||
|
slug = var.cli_app_slug
|
||||||
|
display_name = var.cli_app_display_name
|
||||||
|
agent_id = var.agent_id
|
||||||
|
command = <<-EOT
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
export LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
|
agentapi attach
|
||||||
|
EOT
|
||||||
|
icon = var.cli_app_icon
|
||||||
|
order = var.cli_app_order
|
||||||
|
group = var.cli_app_group
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_ai_task" "agentapi" {
|
||||||
|
sidebar_app {
|
||||||
|
id = coder_app.agentapi_web.id
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
port=${1:-3284}
|
||||||
|
|
||||||
|
# This script waits for the agentapi server to start on port 3284.
|
||||||
|
# It considers the server started after 3 consecutive successful responses.
|
||||||
|
|
||||||
|
agentapi_started=false
|
||||||
|
|
||||||
|
echo "Waiting for agentapi server to start on port $port..."
|
||||||
|
for i in $(seq 1 150); do
|
||||||
|
for j in $(seq 1 3); do
|
||||||
|
sleep 0.1
|
||||||
|
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||||
|
echo "agentapi response received ($j/3)"
|
||||||
|
else
|
||||||
|
echo "agentapi server not responding ($i/15)"
|
||||||
|
continue 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
agentapi_started=true
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$agentapi_started" != "true" ]; then
|
||||||
|
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "agentapi server started on port $port."
|
||||||
96
registry/coder/modules/agentapi/scripts/main.sh
Normal file
96
registry/coder/modules/agentapi/scripts/main.sh
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
|
||||||
|
set -o nounset
|
||||||
|
MODULE_DIR_NAME="$ARG_MODULE_DIR_NAME"
|
||||||
|
WORKDIR="$ARG_WORKDIR"
|
||||||
|
PRE_INSTALL_SCRIPT="$ARG_PRE_INSTALL_SCRIPT"
|
||||||
|
INSTALL_SCRIPT="$ARG_INSTALL_SCRIPT"
|
||||||
|
INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI"
|
||||||
|
AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION"
|
||||||
|
START_SCRIPT="$ARG_START_SCRIPT"
|
||||||
|
WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT"
|
||||||
|
POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT"
|
||||||
|
AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
|
||||||
|
set +o nounset
|
||||||
|
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
module_path="$HOME/${MODULE_DIR_NAME}"
|
||||||
|
mkdir -p "$module_path/scripts"
|
||||||
|
|
||||||
|
if [ ! -d "${WORKDIR}" ]; then
|
||||||
|
echo "Warning: The specified folder '${WORKDIR}' does not exist."
|
||||||
|
echo "Creating the folder..."
|
||||||
|
mkdir -p "${WORKDIR}"
|
||||||
|
echo "Folder created successfully."
|
||||||
|
fi
|
||||||
|
if [ -n "${PRE_INSTALL_SCRIPT}" ]; then
|
||||||
|
echo "Running pre-install script..."
|
||||||
|
echo -n "${PRE_INSTALL_SCRIPT}" >"$module_path/pre_install.sh"
|
||||||
|
chmod +x "$module_path/pre_install.sh"
|
||||||
|
"$module_path/pre_install.sh" 2>&1 | tee "$module_path/pre_install.log"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Running install script..."
|
||||||
|
echo -n "${INSTALL_SCRIPT}" >"$module_path/install.sh"
|
||||||
|
chmod +x "$module_path/install.sh"
|
||||||
|
"$module_path/install.sh" 2>&1 | tee "$module_path/install.log"
|
||||||
|
|
||||||
|
# Install AgentAPI if enabled
|
||||||
|
if [ "${INSTALL_AGENTAPI}" = "true" ]; then
|
||||||
|
echo "Installing AgentAPI..."
|
||||||
|
arch=$(uname -m)
|
||||||
|
if [ "$arch" = "x86_64" ]; then
|
||||||
|
binary_name="agentapi-linux-amd64"
|
||||||
|
elif [ "$arch" = "aarch64" ]; then
|
||||||
|
binary_name="agentapi-linux-arm64"
|
||||||
|
else
|
||||||
|
echo "Error: Unsupported architecture: $arch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "${AGENTAPI_VERSION}" = "latest" ]; then
|
||||||
|
# for the latest release the download URL pattern is different than for tagged releases
|
||||||
|
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
|
||||||
|
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
|
||||||
|
else
|
||||||
|
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name"
|
||||||
|
fi
|
||||||
|
curl \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 5 \
|
||||||
|
--fail \
|
||||||
|
--retry-all-errors \
|
||||||
|
-L \
|
||||||
|
-C - \
|
||||||
|
-o agentapi \
|
||||||
|
"$download_url"
|
||||||
|
chmod +x agentapi
|
||||||
|
sudo mv agentapi /usr/local/bin/agentapi
|
||||||
|
fi
|
||||||
|
if ! command_exists agentapi; then
|
||||||
|
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -n "${START_SCRIPT}" >"$module_path/scripts/agentapi-start.sh"
|
||||||
|
echo -n "${WAIT_FOR_START_SCRIPT}" >"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||||
|
chmod +x "$module_path/scripts/agentapi-start.sh"
|
||||||
|
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
|
||||||
|
|
||||||
|
if [ -n "${POST_INSTALL_SCRIPT}" ]; then
|
||||||
|
echo "Running post-install script..."
|
||||||
|
echo -n "${POST_INSTALL_SCRIPT}" >"$module_path/post_install.sh"
|
||||||
|
chmod +x "$module_path/post_install.sh"
|
||||||
|
"$module_path/post_install.sh" 2>&1 | tee "$module_path/post_install.log"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
export LC_ALL=en_US.UTF-8
|
||||||
|
|
||||||
|
cd "${WORKDIR}"
|
||||||
|
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &>"$module_path/agentapi-start.log" &
|
||||||
|
"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}"
|
||||||
130
registry/coder/modules/agentapi/test-util.ts
Normal file
130
registry/coder/modules/agentapi/test-util.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
execContainer,
|
||||||
|
findResourceInstance,
|
||||||
|
removeContainer,
|
||||||
|
runContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
writeFileContainer,
|
||||||
|
} from "~test";
|
||||||
|
import path from "path";
|
||||||
|
import { expect } from "bun:test";
|
||||||
|
|
||||||
|
export const setupContainer = async ({
|
||||||
|
moduleDir,
|
||||||
|
image,
|
||||||
|
vars,
|
||||||
|
}: {
|
||||||
|
moduleDir: string;
|
||||||
|
image?: string;
|
||||||
|
vars?: Record<string, string>;
|
||||||
|
}) => {
|
||||||
|
const state = await runTerraformApply(moduleDir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
...vars,
|
||||||
|
});
|
||||||
|
const coderScript = findResourceInstance(state, "coder_script");
|
||||||
|
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||||
|
return { id, coderScript, cleanup: () => removeContainer(id) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadTestFile = async (
|
||||||
|
moduleDir: string,
|
||||||
|
...relativePath: [string, ...string[]]
|
||||||
|
) => {
|
||||||
|
return await Bun.file(
|
||||||
|
path.join(moduleDir, "testdata", ...relativePath),
|
||||||
|
).text();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const writeExecutable = async ({
|
||||||
|
containerId,
|
||||||
|
filePath,
|
||||||
|
content,
|
||||||
|
}: {
|
||||||
|
containerId: string;
|
||||||
|
filePath: string;
|
||||||
|
content: string;
|
||||||
|
}) => {
|
||||||
|
await writeFileContainer(containerId, filePath, content, {
|
||||||
|
user: "root",
|
||||||
|
});
|
||||||
|
await execContainer(
|
||||||
|
containerId,
|
||||||
|
["bash", "-c", `chmod 755 ${filePath}`],
|
||||||
|
["--user", "root"],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SetupProps {
|
||||||
|
skipAgentAPIMock?: boolean;
|
||||||
|
moduleDir: string;
|
||||||
|
moduleVariables: Record<string, string>;
|
||||||
|
projectDir?: string;
|
||||||
|
registerCleanup: (cleanup: () => Promise<void>) => void;
|
||||||
|
agentapiMockScript?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||||
|
const projectDir = props.projectDir ?? "/home/coder/project";
|
||||||
|
const { id, coderScript, cleanup } = await setupContainer({
|
||||||
|
moduleDir: props.moduleDir,
|
||||||
|
vars: props.moduleVariables,
|
||||||
|
});
|
||||||
|
props.registerCleanup(cleanup);
|
||||||
|
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
|
||||||
|
if (!props?.skipAgentAPIMock) {
|
||||||
|
await writeExecutable({
|
||||||
|
containerId: id,
|
||||||
|
filePath: "/usr/bin/agentapi",
|
||||||
|
content:
|
||||||
|
props.agentapiMockScript ??
|
||||||
|
(await loadTestFile(import.meta.dir, "agentapi-mock.js")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await writeExecutable({
|
||||||
|
containerId: id,
|
||||||
|
filePath: "/home/coder/script.sh",
|
||||||
|
content: coderScript.script,
|
||||||
|
});
|
||||||
|
return { id };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expectAgentAPIStarted = async (
|
||||||
|
id: string,
|
||||||
|
port: number = 3284,
|
||||||
|
) => {
|
||||||
|
const resp = await execContainer(id, [
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
`curl -fs -o /dev/null "http://localhost:${port}/status"`,
|
||||||
|
]);
|
||||||
|
if (resp.exitCode !== 0) {
|
||||||
|
console.log("agentapi not started");
|
||||||
|
console.log(resp.stdout);
|
||||||
|
console.log(resp.stderr);
|
||||||
|
}
|
||||||
|
expect(resp.exitCode).toBe(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const execModuleScript = async (
|
||||||
|
id: string,
|
||||||
|
env?: Record<string, string>,
|
||||||
|
) => {
|
||||||
|
const envArgs = Object.entries(env ?? {})
|
||||||
|
.map(([key, value]) => ["--env", `${key}=${value}`])
|
||||||
|
.flat();
|
||||||
|
const resp = await execContainer(
|
||||||
|
id,
|
||||||
|
[
|
||||||
|
"bash",
|
||||||
|
"-c",
|
||||||
|
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||||
|
],
|
||||||
|
envArgs,
|
||||||
|
);
|
||||||
|
if (resp.exitCode !== 0) {
|
||||||
|
console.log(resp.stdout);
|
||||||
|
console.log(resp.stderr);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
19
registry/coder/modules/agentapi/testdata/agentapi-mock.js
vendored
Normal file
19
registry/coder/modules/agentapi/testdata/agentapi-mock.js
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const http = require("http");
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const portIdx = args.findIndex((arg) => arg === "--port") + 1;
|
||||||
|
const port = portIdx ? args[portIdx] : 3284;
|
||||||
|
|
||||||
|
console.log(`starting server on port ${port}`);
|
||||||
|
|
||||||
|
http
|
||||||
|
.createServer(function (_request, response) {
|
||||||
|
response.writeHead(200);
|
||||||
|
response.end(
|
||||||
|
JSON.stringify({
|
||||||
|
status: "stable",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.listen(port);
|
||||||
16
registry/coder/modules/agentapi/testdata/agentapi-start.sh
vendored
Normal file
16
registry/coder/modules/agentapi/testdata/agentapi-start.sh
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
use_prompt=${1:-false}
|
||||||
|
port=${2:-3284}
|
||||||
|
|
||||||
|
module_path="$HOME/.agentapi-module"
|
||||||
|
log_file_path="$module_path/agentapi.log"
|
||||||
|
|
||||||
|
echo "using prompt: $use_prompt" >>/home/coder/test-agentapi-start.log
|
||||||
|
echo "using port: $port" >>/home/coder/test-agentapi-start.log
|
||||||
|
|
||||||
|
agentapi server --port "$port" --term-width 67 --term-height 1190 -- \
|
||||||
|
bash -c aiagent \
|
||||||
|
>"$log_file_path" 2>&1
|
||||||
9
registry/coder/modules/agentapi/testdata/ai-agent-mock.js
vendored
Normal file
9
registry/coder/modules/agentapi/testdata/ai-agent-mock.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
console.log("mocking an ai agent");
|
||||||
|
// sleep for 30 minutes
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
10
test/test.ts
10
test/test.ts
@ -324,3 +324,13 @@ export const writeFileContainer = async (
|
|||||||
}
|
}
|
||||||
expect(proc.exitCode).toBe(0);
|
expect(proc.exitCode).toBe(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const readFileContainer = async (id: string, path: string) => {
|
||||||
|
const proc = await execContainer(id, ["cat", path], ["--user", "root"]);
|
||||||
|
if (proc.exitCode !== 0) {
|
||||||
|
console.log(proc.stderr);
|
||||||
|
console.log(proc.stdout);
|
||||||
|
}
|
||||||
|
expect(proc.exitCode).toBe(0);
|
||||||
|
return proc.stdout;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user