mirror of
https://github.com/coder/registry.git
synced 2026-06-03 13:08:14 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83a82345de |
@@ -1,24 +0,0 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: v2.1
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustruct
|
||||
- forcetypeassert
|
||||
- gocognit
|
||||
- gocritic
|
||||
- godot
|
||||
- gomodguard
|
||||
- gosec
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- makezero
|
||||
- misspell
|
||||
- nestif
|
||||
- nilnil
|
||||
# - noctx
|
||||
# - paralleltest
|
||||
- revive
|
||||
- staticcheck
|
||||
# - tparallel
|
||||
- unconvert
|
||||
- unused
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 412
|
||||
godot:
|
||||
scope: all
|
||||
capital: true
|
||||
exhaustruct:
|
||||
include:
|
||||
- httpmw\.\w+
|
||||
- github.com/coder/coder/v2/coderd/database\.[^G][^e][^t]\w+Params
|
||||
gocognit:
|
||||
min-complexity: 300
|
||||
goconst:
|
||||
min-len: 4
|
||||
min-occurrences: 3
|
||||
gocritic:
|
||||
enabled-checks:
|
||||
- badLock
|
||||
- badRegexp
|
||||
- boolExprSimplify
|
||||
- builtinShadow
|
||||
- builtinShadowDecl
|
||||
- commentedOutImport
|
||||
- deferUnlambda
|
||||
- dupImport
|
||||
- dynamicFmtString
|
||||
- emptyDecl
|
||||
- emptyFallthrough
|
||||
- emptyStringTest
|
||||
- evalOrder
|
||||
- externalErrorReassign
|
||||
- filepathJoin
|
||||
- hexLiteral
|
||||
- httpNoBody
|
||||
- importShadow
|
||||
- indexAlloc
|
||||
- initClause
|
||||
- methodExprCall
|
||||
- nestingReduce
|
||||
- nilValReturn
|
||||
- preferFilepathJoin
|
||||
- rangeAppendAll
|
||||
- regexpPattern
|
||||
- redundantSprint
|
||||
- regexpSimplify
|
||||
- ruleguard
|
||||
- sliceClear
|
||||
- sortSlice
|
||||
- sprintfQuotedString
|
||||
- sqlQuery
|
||||
- stringConcatSimplify
|
||||
- stringXbytes
|
||||
- todoCommentWithoutDetail
|
||||
- tooManyResultsChecker
|
||||
- truncateCmp
|
||||
- typeAssertChain
|
||||
- typeDefFirst
|
||||
- unlabelStmt
|
||||
- weakCond
|
||||
- whyNoLint
|
||||
settings:
|
||||
ruleguard:
|
||||
failOn: all
|
||||
rules: ${base-path}/scripts/rules.go
|
||||
gosec:
|
||||
excludes:
|
||||
- G601
|
||||
govet:
|
||||
disable:
|
||||
- loopclosure
|
||||
importas:
|
||||
no-unaliased: true
|
||||
misspell:
|
||||
locale: US
|
||||
ignore-rules:
|
||||
- trialer
|
||||
nestif:
|
||||
min-complexity: 20
|
||||
revive:
|
||||
severity: warning
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: bare-return
|
||||
- name: blank-imports
|
||||
- name: bool-literal-in-expr
|
||||
- name: call-to-gc
|
||||
- name: confusing-results
|
||||
- name: constant-logical-expr
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
# - name: deep-exit
|
||||
- name: defer
|
||||
- name: dot-imports
|
||||
- name: duplicated-imports
|
||||
- name: early-return
|
||||
- name: empty-block
|
||||
- name: empty-lines
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: errorf
|
||||
- name: exported
|
||||
- name: flag-parameter
|
||||
- name: get-return
|
||||
- name: identical-branches
|
||||
- name: if-return
|
||||
- name: import-shadowing
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: modifies-value-receiver
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-of-int
|
||||
- name: struct-tag
|
||||
- name: superfluous-else
|
||||
- name: time-naming
|
||||
- name: unconditional-recursion
|
||||
- name: unexported-naming
|
||||
- name: unexported-return
|
||||
- name: unhandled-error
|
||||
- name: unnecessary-stmt
|
||||
- name: unreachable-code
|
||||
- name: unused-parameter
|
||||
- name: unused-receiver
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
- name: waitgroup-by-value
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- SA4006 # Detects redundant assignments
|
||||
- SA4009 # Detects redundant variable declarations
|
||||
- SA1019
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- errcheck
|
||||
- exhaustruct
|
||||
- forcetypeassert
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- exhaustruct
|
||||
path: scripts/*
|
||||
- linters:
|
||||
- ALL
|
||||
path: scripts/rules.go
|
||||
paths:
|
||||
- scripts/rules.go
|
||||
- coderd/database/dbmem
|
||||
- node_modules
|
||||
- .git
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
fix: true
|
||||
formatters:
|
||||
enable:
|
||||
- goimports
|
||||
- gofmt
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- scripts/rules.go
|
||||
- coderd/database/dbmem
|
||||
- node_modules
|
||||
- .git
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+3
-3
@@ -179,11 +179,11 @@ The following criteria exists for two reasons:
|
||||
- When increasing the level of a header, the header's level must be incremented by one each time.
|
||||
- Any `.hcl` code snippets must be labeled as `.tf` snippets instead
|
||||
|
||||
````txt
|
||||
```tf
|
||||
```txt
|
||||
\`\`\`tf
|
||||
Content
|
||||
\`\`\`
|
||||
```
|
||||
````
|
||||
|
||||
#### Namespace (contributor profile) criteria
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ type coderResourceFrontmatter struct {
|
||||
|
||||
// coderResourceReadme represents a README describing a Terraform resource used
|
||||
// to help create Coder workspaces. As of 2025-04-15, this encapsulates both
|
||||
// Coder Modules and Coder Templates.
|
||||
// Coder Modules and Coder Templates
|
||||
type coderResourceReadme struct {
|
||||
resourceType string
|
||||
filePath string
|
||||
@@ -37,14 +37,14 @@ type coderResourceReadme struct {
|
||||
|
||||
func validateCoderResourceDisplayName(displayName *string) error {
|
||||
if displayName != nil && *displayName == "" {
|
||||
return xerrors.New("if defined, display_name must not be empty string")
|
||||
return errors.New("if defined, display_name must not be empty string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCoderResourceDescription(description string) error {
|
||||
if description == "" {
|
||||
return xerrors.New("frontmatter description cannot be empty")
|
||||
return errors.New("frontmatter description cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -53,29 +53,29 @@ func validateCoderResourceIconURL(iconURL string) []error {
|
||||
problems := []error{}
|
||||
|
||||
if iconURL == "" {
|
||||
problems = append(problems, xerrors.New("icon URL cannot be empty"))
|
||||
problems = append(problems, errors.New("icon URL cannot be empty"))
|
||||
return problems
|
||||
}
|
||||
|
||||
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/")
|
||||
if isAbsoluteURL {
|
||||
if _, err := url.ParseRequestURI(iconURL); err != nil {
|
||||
problems = append(problems, xerrors.New("absolute icon URL is not correctly formatted"))
|
||||
problems = append(problems, errors.New("absolute icon URL is not correctly formatted"))
|
||||
}
|
||||
if strings.Contains(iconURL, "?") {
|
||||
problems = append(problems, xerrors.New("icon URLs cannot contain query parameters"))
|
||||
problems = append(problems, errors.New("icon URLs cannot contain query parameters"))
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// Would normally be skittish about having relative paths like this, but it
|
||||
// should be safe because we have guarantees about the structure of the
|
||||
// repo, and where this logic will run.
|
||||
// repo, and where this logic will run
|
||||
isPermittedRelativeURL := strings.HasPrefix(iconURL, "./") ||
|
||||
strings.HasPrefix(iconURL, "/") ||
|
||||
strings.HasPrefix(iconURL, "../../../../.icons")
|
||||
if !isPermittedRelativeURL {
|
||||
problems = append(problems, xerrors.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL))
|
||||
problems = append(problems, fmt.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL))
|
||||
}
|
||||
|
||||
return problems
|
||||
@@ -83,7 +83,7 @@ func validateCoderResourceIconURL(iconURL string) []error {
|
||||
|
||||
func validateCoderResourceTags(tags []string) error {
|
||||
if tags == nil {
|
||||
return xerrors.New("provided tags array is nil")
|
||||
return errors.New("provided tags array is nil")
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
@@ -91,7 +91,7 @@ func validateCoderResourceTags(tags []string) error {
|
||||
|
||||
// All of these tags are used for the module/template filter controls in the
|
||||
// Registry site. Need to make sure they can all be placed in the browser
|
||||
// URL without issue.
|
||||
// URL without issue
|
||||
invalidTags := []string{}
|
||||
for _, t := range tags {
|
||||
if t != url.QueryEscape(t) {
|
||||
@@ -100,7 +100,7 @@ func validateCoderResourceTags(tags []string) error {
|
||||
}
|
||||
|
||||
if len(invalidTags) != 0 {
|
||||
return xerrors.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
|
||||
return fmt.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func validateCoderResourceTags(tags []string) error {
|
||||
// parse any Terraform code snippets, and make some deeper guarantees about how
|
||||
// it's structured. Just validating whether it *can* be parsed as Terraform
|
||||
// would be a big improvement.
|
||||
var terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
|
||||
var terraformVersionRe = regexp.MustCompile("^\\s*\\bversion\\s+=")
|
||||
|
||||
func validateCoderResourceReadmeBody(body string) []error {
|
||||
trimmed := strings.TrimSpace(body)
|
||||
@@ -132,7 +132,7 @@ func validateCoderResourceReadmeBody(body string) []error {
|
||||
|
||||
// Code assumes that invalid headers would've already been handled by
|
||||
// the base validation function, so we don't need to check deeper if the
|
||||
// first line isn't an h1.
|
||||
// first line isn't an h1
|
||||
if lineNum == 1 {
|
||||
if !strings.HasPrefix(nextLine, "# ") {
|
||||
break
|
||||
@@ -147,7 +147,7 @@ func validateCoderResourceReadmeBody(body string) []error {
|
||||
terraformCodeBlockCount++
|
||||
}
|
||||
if strings.HasPrefix(nextLine, "```hcl") {
|
||||
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
|
||||
errs = append(errs, errors.New("all .hcl language references must be converted to .tf"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -160,34 +160,34 @@ func validateCoderResourceReadmeBody(body string) []error {
|
||||
}
|
||||
|
||||
// Code assumes that we can treat this case as the end of the "h1
|
||||
// section" and don't need to process any further lines.
|
||||
// section" and don't need to process any further lines
|
||||
if lineNum > 1 && strings.HasPrefix(nextLine, "#") {
|
||||
break
|
||||
}
|
||||
|
||||
// Code assumes that if we've reached this point, the only other options
|
||||
// are: (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset
|
||||
// references made via [] syntax.
|
||||
// references made via [] syntax
|
||||
trimmedLine := strings.TrimSpace(nextLine)
|
||||
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
|
||||
foundParagraph = foundParagraph || isParagraph
|
||||
}
|
||||
|
||||
if terraformCodeBlockCount == 0 {
|
||||
errs = append(errs, xerrors.New("did not find Terraform code block within h1 section"))
|
||||
errs = append(errs, errors.New("did not find Terraform code block within h1 section"))
|
||||
} else {
|
||||
if terraformCodeBlockCount > 1 {
|
||||
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
|
||||
errs = append(errs, errors.New("cannot have more than one Terraform code block in h1 section"))
|
||||
}
|
||||
if !foundTerraformVersionRef {
|
||||
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
|
||||
errs = append(errs, errors.New("did not find Terraform code block that specifies 'version' field"))
|
||||
}
|
||||
}
|
||||
if !foundParagraph {
|
||||
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
|
||||
errs = append(errs, errors.New("did not find paragraph within h1 section"))
|
||||
}
|
||||
if isInsideCodeBlock {
|
||||
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
|
||||
errs = append(errs, errors.New("code blocks inside h1 section do not all terminate before end of file"))
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -220,12 +220,12 @@ func validateCoderResourceReadme(rm coderResourceReadme) []error {
|
||||
func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceReadme, error) {
|
||||
fm, body, err := separateFrontmatter(rm.rawText)
|
||||
if err != nil {
|
||||
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
yml := coderResourceFrontmatter{}
|
||||
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
|
||||
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
return coderResourceReadme{
|
||||
@@ -257,9 +257,9 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
|
||||
|
||||
yamlValidationErrors := []error{}
|
||||
for _, readme := range resources {
|
||||
errs := validateCoderResourceReadme(readme)
|
||||
if len(errs) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errs...)
|
||||
errors := validateCoderResourceReadme(readme)
|
||||
if len(errors) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
||||
}
|
||||
}
|
||||
if len(yamlValidationErrors) != 0 {
|
||||
@@ -273,8 +273,8 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
|
||||
}
|
||||
|
||||
// Todo: Need to beef up this function by grabbing each image/video URL from
|
||||
// the body's AST.
|
||||
func validateCoderResourceRelativeUrls(_ map[string]coderResourceReadme) error {
|
||||
// the body's AST
|
||||
func validateCoderResourceRelativeUrls(resources map[string]coderResourceReadme) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
||||
|
||||
func validateAllCoderResourceFilesOfType(resourceType string) error {
|
||||
if !slices.Contains(supportedResourceTypes, resourceType) {
|
||||
return xerrors.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
|
||||
return fmt.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
|
||||
}
|
||||
|
||||
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -8,20 +10,21 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var validContributorStatuses = []string{"official", "partner", "community"}
|
||||
|
||||
type contributorProfileFrontmatter struct {
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Bio string `yaml:"bio"`
|
||||
ContributorStatus string `yaml:"status"`
|
||||
AvatarURL *string `yaml:"avatar"`
|
||||
LinkedinURL *string `yaml:"linkedin"`
|
||||
WebsiteURL *string `yaml:"website"`
|
||||
SupportEmail *string `yaml:"support_email"`
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Bio string `yaml:"bio"`
|
||||
ContributorStatus string `yaml:"status"`
|
||||
// Script assumes that if avatar URL is nil, the Registry site build step
|
||||
// will backfill the value with the user's GitHub avatar URL
|
||||
AvatarURL *string `yaml:"avatar"`
|
||||
LinkedinURL *string `yaml:"linkedin"`
|
||||
WebsiteURL *string `yaml:"website"`
|
||||
SupportEmail *string `yaml:"support_email"`
|
||||
}
|
||||
|
||||
type contributorProfileReadme struct {
|
||||
@@ -32,7 +35,7 @@ type contributorProfileReadme struct {
|
||||
|
||||
func validateContributorDisplayName(displayName string) error {
|
||||
if displayName == "" {
|
||||
return xerrors.New("missing display_name")
|
||||
return fmt.Errorf("missing display_name")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -44,7 +47,7 @@ func validateContributorLinkedinURL(linkedinURL *string) error {
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(*linkedinURL); err != nil {
|
||||
return xerrors.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
|
||||
return fmt.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -60,31 +63,31 @@ func validateContributorSupportEmail(email *string) []error {
|
||||
// Can't 100% validate that this is correct without actually sending
|
||||
// an email, and especially with some contributors being individual
|
||||
// developers, we don't want to do that on every single run of the CI
|
||||
// pipeline. Best we can do is verify the general structure.
|
||||
// pipeline. Best we can do is verify the general structure
|
||||
username, server, ok := strings.Cut(*email, "@")
|
||||
if !ok {
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing @ symbol", *email))
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing @ symbol", *email))
|
||||
return errs
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing username", *email))
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing username", *email))
|
||||
}
|
||||
|
||||
domain, tld, ok := strings.Cut(server, ".")
|
||||
if !ok {
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing period for server segment", *email))
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing period for server segment", *email))
|
||||
return errs
|
||||
}
|
||||
|
||||
if domain == "" {
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing domain", *email))
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing domain", *email))
|
||||
}
|
||||
if tld == "" {
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing top-level domain", *email))
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing top-level domain", *email))
|
||||
}
|
||||
if strings.Contains(*email, "?") {
|
||||
errs = append(errs, xerrors.New("email is not allowed to contain query parameters"))
|
||||
errs = append(errs, errors.New("email is not allowed to contain query parameters"))
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -96,7 +99,7 @@ func validateContributorWebsite(websiteURL *string) error {
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(*websiteURL); err != nil {
|
||||
return xerrors.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
|
||||
return fmt.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -104,14 +107,14 @@ func validateContributorWebsite(websiteURL *string) error {
|
||||
|
||||
func validateContributorStatus(status string) error {
|
||||
if !slices.Contains(validContributorStatuses, status) {
|
||||
return xerrors.Errorf("contributor status %q is not valid", status)
|
||||
return fmt.Errorf("contributor status %q is not valid", status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Can't validate the image actually leads to a valid resource in a pure
|
||||
// function, but can at least catch obvious problems.
|
||||
// function, but can at least catch obvious problems
|
||||
func validateContributorAvatarURL(avatarURL *string) []error {
|
||||
if avatarURL == nil {
|
||||
return nil
|
||||
@@ -119,17 +122,17 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
||||
|
||||
errs := []error{}
|
||||
if *avatarURL == "" {
|
||||
errs = append(errs, xerrors.New("avatar URL must be omitted or non-empty string"))
|
||||
errs = append(errs, errors.New("avatar URL must be omitted or non-empty string"))
|
||||
return errs
|
||||
}
|
||||
|
||||
// Have to use .Parse instead of .ParseRequestURI because this is the
|
||||
// one field that's allowed to be a relative URL.
|
||||
// one field that's allowed to be a relative URL
|
||||
if _, err := url.Parse(*avatarURL); err != nil {
|
||||
errs = append(errs, xerrors.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
|
||||
errs = append(errs, fmt.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
|
||||
}
|
||||
if strings.Contains(*avatarURL, "?") {
|
||||
errs = append(errs, xerrors.New("avatar URL is not allowed to contain search parameters"))
|
||||
errs = append(errs, errors.New("avatar URL is not allowed to contain search parameters"))
|
||||
}
|
||||
|
||||
matched := false
|
||||
@@ -142,7 +145,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
||||
if !matched {
|
||||
segments := strings.Split(*avatarURL, ".")
|
||||
fileExtension := segments[len(segments)-1]
|
||||
errs = append(errs, xerrors.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
|
||||
errs = append(errs, fmt.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -177,12 +180,12 @@ func validateContributorReadme(rm contributorProfileReadme) []error {
|
||||
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
|
||||
fm, _, err := separateFrontmatter(rm.rawText)
|
||||
if err != nil {
|
||||
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
yml := contributorProfileFrontmatter{}
|
||||
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
|
||||
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
return contributorProfileReadme{
|
||||
@@ -203,7 +206,7 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
|
||||
}
|
||||
|
||||
if prev, alreadyExists := profilesByNamespace[p.namespace]; alreadyExists {
|
||||
yamlParsingErrors = append(yamlParsingErrors, xerrors.Errorf("%q: namespace %q conflicts with namespace from %q", p.filePath, p.namespace, prev.filePath))
|
||||
yamlParsingErrors = append(yamlParsingErrors, fmt.Errorf("%q: namespace %q conflicts with namespace from %q", p.filePath, p.namespace, prev.filePath))
|
||||
continue
|
||||
}
|
||||
profilesByNamespace[p.namespace] = p
|
||||
@@ -271,9 +274,12 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
||||
|
||||
func validateContributorRelativeUrls(contributors map[string]contributorProfileReadme) error {
|
||||
// This function only validates relative avatar URLs for now, but it can be
|
||||
// beefed up to validate more in the future.
|
||||
var errs []error
|
||||
// beefed up to validate more in the future
|
||||
errs := []error{}
|
||||
|
||||
for _, con := range contributors {
|
||||
// If the avatar URL is missing, we'll just assume that the Registry
|
||||
// site build step will take care of filling in the data properly
|
||||
if con.frontmatter.AvatarURL == nil {
|
||||
continue
|
||||
}
|
||||
@@ -284,18 +290,16 @@ func validateContributorRelativeUrls(contributors map[string]contributorProfileR
|
||||
continue
|
||||
}
|
||||
|
||||
isAvatarInApprovedSpot := strings.HasPrefix(*con.frontmatter.AvatarURL, "./.images/") ||
|
||||
strings.HasPrefix(*con.frontmatter.AvatarURL, ".images/")
|
||||
if !isAvatarInApprovedSpot {
|
||||
errs = append(errs, xerrors.Errorf("%q: relative avatar URLs cannot be placed outside a user's namespaced directory", con.filePath))
|
||||
if strings.HasPrefix(*con.frontmatter.AvatarURL, "..") {
|
||||
errs = append(errs, fmt.Errorf("%q: relative avatar URLs cannot be placed outside a user's namespaced directory", con.filePath))
|
||||
continue
|
||||
}
|
||||
|
||||
absolutePath := strings.TrimSuffix(con.filePath, "README.md") +
|
||||
*con.frontmatter.AvatarURL
|
||||
_, err := os.Stat(absolutePath)
|
||||
_, err := os.ReadFile(absolutePath)
|
||||
if err != nil {
|
||||
errs = append(errs, xerrors.Errorf("%q: relative avatar path %q does not point to image in file system", con.filePath, absolutePath))
|
||||
errs = append(errs, fmt.Errorf("%q: relative avatar path %q does not point to image in file system", con.filePath, *con.frontmatter.AvatarURL))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
// validationPhaseError represents an error that occurred during a specific
|
||||
// phase of README validation. It should be used to collect ALL validation
|
||||
@@ -28,5 +24,5 @@ func (vpe validationPhaseError) Error() string {
|
||||
}
|
||||
|
||||
func addFilePathToError(filePath string, err error) error {
|
||||
return xerrors.Errorf("%q: %v", filePath, err)
|
||||
return fmt.Errorf("%q: %v", filePath, err)
|
||||
}
|
||||
|
||||
@@ -6,30 +6,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
)
|
||||
|
||||
var logger = slog.Make(sloghuman.Sink(os.Stdout))
|
||||
|
||||
func main() {
|
||||
logger.Info(context.Background(), "starting README validation")
|
||||
log.Println("Starting README validation")
|
||||
|
||||
// If there are fundamental problems with how the repo is structured, we
|
||||
// can't make any guarantees that any further validations will be relevant
|
||||
// or accurate.
|
||||
err := validateRepoStructure()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
// or accurate
|
||||
repoErr := validateRepoStructure()
|
||||
if repoErr != nil {
|
||||
log.Println(repoErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
err = validateAllContributorFiles()
|
||||
err := validateAllContributorFiles()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@@ -39,11 +34,11 @@ func main() {
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
|
||||
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
for _, err := range errs {
|
||||
logger.Error(context.Background(), err.Error())
|
||||
fmt.Println(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const rootRegistryPath = "./registry"
|
||||
@@ -23,9 +23,9 @@ type readme struct {
|
||||
// from the main README body, returning both values in that order. It does not
|
||||
// validate whether the structure of the frontmatter is valid (i.e., that it's
|
||||
// structured as YAML).
|
||||
func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBody string, err error) {
|
||||
func separateFrontmatter(readmeText string) (string, string, error) {
|
||||
if readmeText == "" {
|
||||
return "", "", xerrors.New("README is empty")
|
||||
return "", "", errors.New("README is empty")
|
||||
}
|
||||
|
||||
const fence = "---"
|
||||
@@ -41,7 +41,7 @@ func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBod
|
||||
continue
|
||||
}
|
||||
// Break early if the very first line wasn't a fence, because then we
|
||||
// know for certain that the README has problems.
|
||||
// know for certain that the README has problems
|
||||
if fenceCount == 0 {
|
||||
break
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBod
|
||||
// It should be safe to trim each line of the frontmatter on a per-line
|
||||
// basis, because there shouldn't be any extra meaning attached to the
|
||||
// indentation. The same does NOT apply to the README; best we can do is
|
||||
// gather all the lines, and then trim around it.
|
||||
// gather all the lines, and then trim around it
|
||||
if inReadmeBody := fenceCount >= 2; inReadmeBody {
|
||||
body += nextLine + "\n"
|
||||
} else {
|
||||
@@ -57,31 +57,31 @@ func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBod
|
||||
}
|
||||
}
|
||||
if fenceCount < 2 {
|
||||
return "", "", xerrors.New("README does not have two sets of frontmatter fences")
|
||||
return "", "", errors.New("README does not have two sets of frontmatter fences")
|
||||
}
|
||||
if fm == "" {
|
||||
return "", "", xerrors.New("readme has frontmatter fences but no frontmatter content")
|
||||
return "", "", errors.New("readme has frontmatter fences but no frontmatter content")
|
||||
}
|
||||
|
||||
return fm, strings.TrimSpace(body), nil
|
||||
}
|
||||
|
||||
var readmeHeaderRe = regexp.MustCompile(`^(#+)(\s*)`)
|
||||
var readmeHeaderRe = regexp.MustCompile("^(#{1,})(\\s*)")
|
||||
|
||||
// Todo: This seems to work okay for now, but the really proper way of doing
|
||||
// this is by parsing this as an AST, and then checking the resulting nodes.
|
||||
// this is by parsing this as an AST, and then checking the resulting nodes
|
||||
func validateReadmeBody(body string) []error {
|
||||
trimmed := strings.TrimSpace(body)
|
||||
|
||||
if trimmed == "" {
|
||||
return []error{xerrors.New("README body is empty")}
|
||||
return []error{errors.New("README body is empty")}
|
||||
}
|
||||
|
||||
// If the very first line of the README, there's a risk that the rest of the
|
||||
// validation logic will break, since we don't have many guarantees about
|
||||
// how the README is actually structured.
|
||||
// how the README is actually structured
|
||||
if !strings.HasPrefix(trimmed, "# ") {
|
||||
return []error{xerrors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
|
||||
return []error{errors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
@@ -95,7 +95,7 @@ func validateReadmeBody(body string) []error {
|
||||
|
||||
// Have to check this because a lot of programming languages support #
|
||||
// comments (including Terraform), and without any context, there's no
|
||||
// way to tell the difference between a markdown header and code comment.
|
||||
// way to tell the difference between a markdown header and code comment
|
||||
if strings.HasPrefix(nextLine, "```") {
|
||||
isInCodeBlock = !isInCodeBlock
|
||||
continue
|
||||
@@ -111,7 +111,7 @@ func validateReadmeBody(body string) []error {
|
||||
|
||||
spaceAfterHeader := headerGroups[2]
|
||||
if spaceAfterHeader == "" {
|
||||
errs = append(errs, xerrors.New("header does not have space between header characters and main header text"))
|
||||
errs = append(errs, errors.New("header does not have space between header characters and main header text"))
|
||||
}
|
||||
|
||||
nextHeaderLevel := len(headerGroups[1])
|
||||
@@ -122,26 +122,26 @@ func validateReadmeBody(body string) []error {
|
||||
}
|
||||
|
||||
// If we have obviously invalid headers, it's not really safe to keep
|
||||
// proceeding with the rest of the content.
|
||||
// proceeding with the rest of the content
|
||||
if nextHeaderLevel == 1 {
|
||||
errs = append(errs, xerrors.New("READMEs cannot contain more than h1 header"))
|
||||
errs = append(errs, errors.New("READMEs cannot contain more than h1 header"))
|
||||
break
|
||||
}
|
||||
if nextHeaderLevel > 6 {
|
||||
errs = append(errs, xerrors.Errorf("README/HTML files cannot have headers exceed level 6 (found level %d)", nextHeaderLevel))
|
||||
errs = append(errs, fmt.Errorf("README/HTML files cannot have headers exceed level 6 (found level %d)", nextHeaderLevel))
|
||||
break
|
||||
}
|
||||
|
||||
// This is something we need to enforce for accessibility, not just for
|
||||
// the Registry website, but also when users are viewing the README
|
||||
// files in the GitHub web view.
|
||||
// files in the GitHub web view
|
||||
if nextHeaderLevel > latestHeaderLevel && nextHeaderLevel != (latestHeaderLevel+1) {
|
||||
errs = append(errs, xerrors.New("headers are not allowed to increase more than 1 level at a time"))
|
||||
errs = append(errs, fmt.Errorf("headers are not allowed to increase more than 1 level at a time"))
|
||||
continue
|
||||
}
|
||||
|
||||
// As long as the above condition passes, there's no problems with
|
||||
// going up a header level or going down 1+ header levels.
|
||||
// going up a header level or going down 1+ header levels
|
||||
latestHeaderLevel = nextHeaderLevel
|
||||
}
|
||||
|
||||
@@ -154,20 +154,24 @@ func validateReadmeBody(body string) []error {
|
||||
type validationPhase string
|
||||
|
||||
const (
|
||||
// ValidationPhaseFileStructureValidation indicates when the entire Registry
|
||||
// validationPhaseFileStructureValidation indicates when the entire Registry
|
||||
// directory is being verified for having all files be placed in the file
|
||||
// system as expected.
|
||||
validationPhaseFileStructureValidation validationPhase = "File structure validation"
|
||||
|
||||
// ValidationPhaseFileLoad indicates when README files are being read from
|
||||
// the file system.
|
||||
// validationPhaseFileLoad indicates when README files are being read from
|
||||
// the file system
|
||||
validationPhaseFileLoad = "Filesystem reading"
|
||||
|
||||
// ValidationPhaseReadmeParsing indicates when a README's frontmatter is
|
||||
// validationPhaseReadmeParsing indicates when a README's frontmatter is
|
||||
// being parsed as YAML. This phase does not include YAML validation.
|
||||
validationPhaseReadmeParsing = "README parsing"
|
||||
|
||||
// ValidationPhaseAssetCrossReference indicates when a README's frontmatter
|
||||
// validationPhaseReadmeValidation indicates when a README's frontmatter is
|
||||
// being validated as proper YAML with expected keys.
|
||||
validationPhaseReadmeValidation = "README validation"
|
||||
|
||||
// validationPhaseAssetCrossReference indicates when a README's frontmatter
|
||||
// is having all its relative URLs be validated for whether they point to
|
||||
// valid resources.
|
||||
validationPhaseAssetCrossReference = "Cross-referencing relative asset URLs"
|
||||
|
||||
@@ -2,15 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".icons", ".images")
|
||||
var supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
|
||||
|
||||
func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
errs := []error{}
|
||||
@@ -18,7 +17,7 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
subDir, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
// It's valid for a specific resource directory not to exist. It's just
|
||||
// that if it does exist, it must follow specific rules.
|
||||
// that if it does exist, it must follow specific rules
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, addFilePathToError(dirPath, err))
|
||||
}
|
||||
@@ -26,7 +25,7 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
}
|
||||
|
||||
if !subDir.IsDir() {
|
||||
errs = append(errs, xerrors.Errorf("%q: path is not a directory", dirPath))
|
||||
errs = append(errs, fmt.Errorf("%q: path is not a directory", dirPath))
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -39,7 +38,7 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
// The .coder subdirectories are sometimes generated as part of Bun
|
||||
// tests. These subdirectories will never be committed to the repo, but
|
||||
// in the off chance that they don't get cleaned up properly, we want to
|
||||
// skip over them.
|
||||
// skip over them
|
||||
if !f.IsDir() || f.Name() == ".coder" {
|
||||
continue
|
||||
}
|
||||
@@ -48,7 +47,7 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
_, err := os.Stat(resourceReadmePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, xerrors.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
|
||||
errs = append(errs, fmt.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
|
||||
} else {
|
||||
errs = append(errs, addFilePathToError(resourceReadmePath, err))
|
||||
}
|
||||
@@ -58,11 +57,12 @@ func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
_, err = os.Stat(mainTerraformPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, xerrors.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
|
||||
errs = append(errs, fmt.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
|
||||
} else {
|
||||
errs = append(errs, addFilePathToError(mainTerraformPath, err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -78,7 +78,7 @@ func validateRegistryDirectory() []error {
|
||||
for _, d := range userDirs {
|
||||
dirPath := path.Join(rootRegistryPath, d.Name())
|
||||
if !d.IsDir() {
|
||||
allErrs = append(allErrs, xerrors.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
|
||||
allErrs = append(allErrs, fmt.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func validateRegistryDirectory() []error {
|
||||
|
||||
for _, f := range files {
|
||||
// Todo: Decide if there's anything more formal that we want to
|
||||
// ensure about non-directories scoped to user namespaces.
|
||||
// ensure about non-directories scoped to user namespaces
|
||||
if !f.IsDir() {
|
||||
continue
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func validateRegistryDirectory() []error {
|
||||
filePath := path.Join(dirPath, segment)
|
||||
|
||||
if !slices.Contains(supportedUserNameSpaceDirectories, segment) {
|
||||
allErrs = append(allErrs, xerrors.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
|
||||
allErrs = append(allErrs, fmt.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func validateRepoStructure() error {
|
||||
|
||||
_, err := os.Stat("./.icons")
|
||||
if err != nil {
|
||||
problems = append(problems, xerrors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
|
||||
problems = append(problems, errors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
|
||||
}
|
||||
|
||||
if len(problems) != 0 {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type terraformLifecycleCondition struct {
|
||||
// Any terraform expression that evaluates to a boolean
|
||||
expression string
|
||||
errorMessage *string
|
||||
}
|
||||
|
||||
type terraformLifecycle struct {
|
||||
createBeforeDestroy bool
|
||||
preventDestroy bool
|
||||
ignoreChanges []string
|
||||
replaceTriggeredBy []string
|
||||
preCondition *terraformLifecycleCondition
|
||||
postCondition *terraformLifecycleCondition
|
||||
}
|
||||
|
||||
type terraformForEachKind string
|
||||
|
||||
const (
|
||||
terraformForEachKindMap terraformForEachKind = "map"
|
||||
terraformForEachKindSet terraformForEachKind = "set"
|
||||
)
|
||||
|
||||
type terraformForEach struct {
|
||||
kind terraformForEachKind
|
||||
// If the kind is "map", all values should be guaranteed to be a definite
|
||||
// string for each key. If it's "set", all values should be nil
|
||||
values map[string]*string
|
||||
}
|
||||
|
||||
type terraformVariableKind string
|
||||
|
||||
const (
|
||||
terraformVariableKindString terraformVariableKind = "string"
|
||||
terraformVariableKindNumber terraformVariableKind = "number"
|
||||
terraformVariableKindBool terraformVariableKind = "bool"
|
||||
terraformVariableKindList terraformVariableKind = "list"
|
||||
terraformVariableKindMap terraformVariableKind = "map"
|
||||
terraformVariableKindSet terraformVariableKind = "set"
|
||||
// This value kind is incredibly rare. It corresponds to any variable with
|
||||
// a null value but no defined type
|
||||
terraformVariableKindUnknown terraformVariableKind = "unknown"
|
||||
)
|
||||
|
||||
type terraformVariable struct {
|
||||
kind terraformVariableKind
|
||||
value any
|
||||
}
|
||||
|
||||
// coderTerraformModule represents the values that can be parsed from a
|
||||
// Terraform module that are relevant to a Coder deployment. Most of the fields
|
||||
// are derived from the main Terraform spec, but some additional Coder-specific
|
||||
// fields are appended, too, for convenience
|
||||
type coderTerraformModule struct {
|
||||
// Corresponds to the optional `lifecycle` metadata field available to all
|
||||
// resource types
|
||||
Lifecycle *terraformLifecycle `json:"lifecycle"`
|
||||
// Corresponds to the optional `for_each` metadata field available to all
|
||||
// resource types
|
||||
ForEach *terraformForEach `json:"for_each"`
|
||||
// Corresponds to the `source` field in a module block
|
||||
ModuleSource string `json:"module_source"`
|
||||
// Corresponds to the `version` field in Terraform module blocks. Note that
|
||||
// while the Terraform spec marks this field as optional, Coder requires
|
||||
// that one always be defined.
|
||||
Version string `json:"version"`
|
||||
// Corresponds to the optional `provider` field in a module block
|
||||
Provider *string `json:"provider"`
|
||||
// Corresponds to optional `depends_on` field for module blocks
|
||||
DependsOn []string `json:"depends_on"`
|
||||
// Corresponds to the `count` field for any Terraform resource type. It
|
||||
// defines the number of resource instances to create when using Terraform
|
||||
// Apply.
|
||||
InstanceCount int `json:"instance_count"`
|
||||
// Corresponds to `agent_id`` field in a module block. Terraform doesn't
|
||||
// have any built-in concept of an agent_id, but it's needed to make a
|
||||
// module work with a Coder deployment
|
||||
AgentID string `json:"agent_id"`
|
||||
// Captures all other arbitrary values defined for a Terraform module block.
|
||||
// Note that while Terraform itself has you define all other fields at the
|
||||
// same level as the well-known/official fields, they've been isolated into
|
||||
// a map for the Go struct definition to improve type-safety
|
||||
Values map[string]terraformVariable `json:"values"`
|
||||
// The raw Terraform snippet used to derive the coderTerraformModule struct
|
||||
SourceCode string `json:"source_code"`
|
||||
}
|
||||
|
||||
var _ json.Marshaler = &coderTerraformModule{}
|
||||
|
||||
func (ctmCopy coderTerraformModule) MarshalJSON() ([]byte, error) {
|
||||
if ctmCopy.Lifecycle != nil {
|
||||
lCopy := &terraformLifecycle{
|
||||
createBeforeDestroy: ctmCopy.Lifecycle.createBeforeDestroy,
|
||||
preventDestroy: ctmCopy.Lifecycle.preventDestroy,
|
||||
ignoreChanges: ctmCopy.Lifecycle.ignoreChanges,
|
||||
replaceTriggeredBy: ctmCopy.Lifecycle.replaceTriggeredBy,
|
||||
preCondition: ctmCopy.Lifecycle.preCondition,
|
||||
postCondition: ctmCopy.Lifecycle.postCondition,
|
||||
}
|
||||
// Make sure that both slices always get serialized as JSON null if
|
||||
// they're empty. Serializing as an empty JSON array has no extra
|
||||
// semantics
|
||||
if len(lCopy.ignoreChanges) == 0 {
|
||||
lCopy.ignoreChanges = nil
|
||||
}
|
||||
if len(lCopy.replaceTriggeredBy) == 0 {
|
||||
lCopy.replaceTriggeredBy = nil
|
||||
}
|
||||
ctmCopy.Lifecycle = lCopy
|
||||
}
|
||||
|
||||
if ctmCopy.ForEach != nil {
|
||||
feCopy := &terraformForEach{
|
||||
kind: ctmCopy.ForEach.kind,
|
||||
values: ctmCopy.ForEach.values,
|
||||
}
|
||||
// Make sure that the values map is NEVER serialized as JSON null
|
||||
if feCopy.values == nil {
|
||||
feCopy.values = make(map[string]*string)
|
||||
}
|
||||
ctmCopy.ForEach = feCopy
|
||||
}
|
||||
|
||||
if len(ctmCopy.DependsOn) == 0 {
|
||||
ctmCopy.DependsOn = nil
|
||||
}
|
||||
|
||||
if ctmCopy.Values == nil {
|
||||
ctmCopy.Values = make(map[string]terraformVariable)
|
||||
}
|
||||
for k, tfValue := range ctmCopy.Values {
|
||||
switch tfValue.kind {
|
||||
case terraformVariableKindString, terraformVariableKindBool, terraformVariableKindNumber, terraformVariableKindUnknown:
|
||||
continue
|
||||
case terraformVariableKindSet, terraformVariableKindList:
|
||||
recast, ok := tfValue.value.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to process terraform variable %q of kind %q as set/list", k, tfValue.kind)
|
||||
}
|
||||
if recast == nil {
|
||||
ctmCopy.Values[k] = terraformVariable{
|
||||
kind: tfValue.kind,
|
||||
value: []any{},
|
||||
}
|
||||
}
|
||||
case terraformVariableKindMap:
|
||||
recast, ok := tfValue.value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to process terraform variable %q of kind %q as map", k, tfValue.kind)
|
||||
}
|
||||
if recast == nil {
|
||||
ctmCopy.Values[k] = terraformVariable{
|
||||
kind: tfValue.kind,
|
||||
value: make(map[string]any),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(ctmCopy)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ tags: [helper]
|
||||
module "MODULE_NAME" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,7 +31,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
||||
module "MODULE_NAME" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = [
|
||||
"dracula-theme.theme-dracula"
|
||||
@@ -49,7 +49,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
module "MODULE_NAME" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -65,7 +65,7 @@ Run code-server in the background, don't fetch it from GitHub:
|
||||
```tf
|
||||
module "MODULE_NAME" {
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,3 +105,4 @@ data "coder_parameter" "MODULE_NAME" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,25 +2,4 @@ module coder.com/coder-registry
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
cdr.dev/slog v1.6.1
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.7.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
)
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
@@ -1,79 +1,3 @@
|
||||
cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4=
|
||||
cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI=
|
||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
|
||||
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
|
||||
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
|
||||
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
|
||||
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
|
||||
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
|
||||
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/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
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/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/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
|
||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -2,7 +2,6 @@
|
||||
display_name: Coder
|
||||
bio: Coder provisions cloud development environments via Terraform, supporting Linux, macOS, Windows, X86, ARM, Kubernetes and more.
|
||||
github: coder
|
||||
avatar: ./.images/avatar.png
|
||||
linkedin: https://www.linkedin.com/company/coderhq
|
||||
website: https://www.coder.com
|
||||
status: official
|
||||
|
||||
@@ -14,7 +14,7 @@ Run [Aider](https://aider.chat) AI pair programming in your workspace. This modu
|
||||
```tf
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -69,7 +69,7 @@ variable "anthropic_api_key" {
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
}
|
||||
@@ -94,7 +94,7 @@ variable "openai_api_key" {
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
use_tmux = true
|
||||
ai_provider = "openai"
|
||||
@@ -115,7 +115,7 @@ variable "custom_api_key" {
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_provider = "custom"
|
||||
custom_env_var_name = "MY_CUSTOM_API_KEY"
|
||||
@@ -132,7 +132,7 @@ You can extend Aider's capabilities by adding custom extensions:
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
|
||||
@@ -211,7 +211,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
@@ -230,17 +224,17 @@ resource "coder_script" "aider" {
|
||||
}
|
||||
|
||||
echo "Setting up Aider AI pair programming..."
|
||||
|
||||
|
||||
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
|
||||
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
mkdir -p "${var.folder}"
|
||||
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
echo "Checking dependencies for Linux..."
|
||||
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if ! command_exists tmux; then
|
||||
echo "Installing tmux for persistent sessions..."
|
||||
@@ -302,7 +296,7 @@ resource "coder_script" "aider" {
|
||||
|
||||
if [ "${var.install_aider}" = "true" ]; then
|
||||
echo "Installing Aider..."
|
||||
|
||||
|
||||
if ! command_exists python3 || ! command_exists pip3; then
|
||||
echo "Installing Python dependencies required for Aider..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
@@ -325,37 +319,37 @@ resource "coder_script" "aider" {
|
||||
else
|
||||
echo "Python is already installed, skipping installation."
|
||||
fi
|
||||
|
||||
|
||||
if ! command_exists aider; then
|
||||
curl -LsSf https://aider.chat/install.sh | sh
|
||||
fi
|
||||
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
if [ -f "$HOME/.zshrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
fi
|
||||
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Aider to report tasks via Coder MCP..."
|
||||
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
|
||||
cat > "$HOME/.config/aider/config.yml" << EOL
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
@@ -363,29 +357,29 @@ EOL
|
||||
fi
|
||||
|
||||
echo "Starting persistent Aider session..."
|
||||
|
||||
|
||||
touch "$HOME/.aider.log"
|
||||
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in tmux session..."
|
||||
|
||||
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
|
||||
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
|
||||
@@ -395,12 +389,12 @@ EOL
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
|
||||
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
@@ -408,12 +402,12 @@ EOL
|
||||
else
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in screen session..."
|
||||
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
@@ -423,7 +417,7 @@ EOL
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
@@ -432,15 +426,15 @@ EOL
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
|
||||
|
||||
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
@@ -450,7 +444,7 @@ EOL
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
@@ -462,7 +456,7 @@ EOL
|
||||
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
echo "Aider setup complete!"
|
||||
EOT
|
||||
run_on_start = true
|
||||
@@ -477,12 +471,12 @@ resource "coder_app" "aider_cli" {
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
|
||||
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
|
||||
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Aider tmux session..."
|
||||
@@ -505,5 +499,4 @@ resource "coder_app" "aider_cli" {
|
||||
fi
|
||||
EOT
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ Enable DCV Server and Web Client on Windows workspaces.
|
||||
module "dcv" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/amazon-dcv-windows/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.24"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,11 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "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 "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
@@ -57,8 +45,6 @@ resource "coder_app" "web-dcv" {
|
||||
url = "https://localhost:${var.port}${local.web_url_path}?username=${local.admin_username}&password=${var.admin_password}"
|
||||
icon = "/icon/dcv.svg"
|
||||
subdomain = var.subdomain
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
resource "coder_script" "install-dcv" {
|
||||
|
||||
@@ -14,7 +14,7 @@ Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's A
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
# Required: see below for how to generate
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
@@ -82,7 +82,7 @@ module "amazon-q" {
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_use_tmux = true
|
||||
@@ -94,7 +94,7 @@ module "amazon-q" {
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_report_tasks = true
|
||||
@@ -106,7 +106,7 @@ module "amazon-q" {
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_pre_install_script = "echo Pre-install!"
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
@@ -219,7 +213,7 @@ resource "coder_script" "amazon_q" {
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Amazon Q to report tasks via Coder MCP..."
|
||||
echo "Configuring Amazon Q to report tasks via Coder MCP..."
|
||||
mkdir -p ~/.aws/amazonq
|
||||
echo "${local.encoded_mcp_json}" | base64 -d > ~/.aws/amazonq/mcp.json
|
||||
echo "Created the ~/.aws/amazonq/mcp.json configuration file"
|
||||
@@ -233,19 +227,19 @@ resource "coder_script" "amazon_q" {
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background with tmux..."
|
||||
|
||||
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.amazon-q.log"
|
||||
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
|
||||
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
|
||||
|
||||
|
||||
tmux send-keys -t amazon-q "${local.full_prompt}"
|
||||
sleep 5
|
||||
tmux send-keys -t amazon-q Enter
|
||||
@@ -253,7 +247,7 @@ resource "coder_script" "amazon_q" {
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background..."
|
||||
|
||||
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
exit 1
|
||||
@@ -265,7 +259,7 @@ resource "coder_script" "amazon_q" {
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
@@ -277,7 +271,7 @@ resource "coder_script" "amazon_q" {
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
|
||||
screen -U -dmS amazon-q bash -c '
|
||||
cd ${var.folder}
|
||||
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
|
||||
@@ -332,6 +326,4 @@ resource "coder_app" "amazon_q" {
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -22,11 +22,6 @@ module "claude-code" {
|
||||
}
|
||||
```
|
||||
|
||||
> **Security Notice**: This module uses the [`--dangerously-skip-permissions`](https://docs.anthropic.com/en/docs/claude-code/cli-usage#cli-flags) flag when running Claude Code. This flag
|
||||
> bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While
|
||||
> this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as
|
||||
> the user running it. Use this module _only_ in trusted environments and be aware of the security implications.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js and npm must be installed in your workspace to install Claude Code
|
||||
@@ -107,7 +102,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
@@ -103,16 +97,6 @@ resource "coder_script" "claude_code" {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check if the specified folder exists
|
||||
if [ ! -d "${var.folder}" ]; then
|
||||
echo "Warning: The specified folder '${var.folder}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
# The folder must exist before tmux is started or else claude will start
|
||||
# in the home directory.
|
||||
mkdir -p "${var.folder}"
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
@@ -154,7 +138,7 @@ resource "coder_script" "claude_code" {
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Claude Code in the background with tmux..."
|
||||
|
||||
|
||||
# Check if tmux is installed
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
@@ -162,13 +146,13 @@ resource "coder_script" "claude_code" {
|
||||
fi
|
||||
|
||||
touch "$HOME/.claude-code.log"
|
||||
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions"
|
||||
|
||||
|
||||
# Send the prompt to the tmux session if needed
|
||||
if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then
|
||||
tmux send-keys -t claude-code "$CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
@@ -180,7 +164,7 @@ resource "coder_script" "claude_code" {
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Claude Code in the background..."
|
||||
|
||||
|
||||
# Check if screen is installed
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
@@ -194,7 +178,7 @@ resource "coder_script" "claude_code" {
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
@@ -206,7 +190,7 @@ resource "coder_script" "claude_code" {
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
|
||||
screen -U -dmS claude-code bash -c '
|
||||
cd ${var.folder}
|
||||
claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"
|
||||
@@ -262,6 +246,4 @@ resource "coder_app" "claude_code" {
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ module "code-server" {
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_version = "4.8.3"
|
||||
}
|
||||
@@ -44,7 +44,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = [
|
||||
"dracula-theme.theme-dracula"
|
||||
@@ -62,7 +62,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -79,7 +79,7 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
}
|
||||
@@ -95,7 +95,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
use_cached = true
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
@@ -108,7 +108,7 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,12 +89,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "offline" {
|
||||
type = bool
|
||||
description = "Just run code-server in the background, don't fetch it from GitHub"
|
||||
@@ -193,7 +187,6 @@ resource "coder_app" "code-server" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
open_in = var.open_in
|
||||
|
||||
healthcheck {
|
||||
|
||||
@@ -17,7 +17,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ module "cursor" {
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
@@ -60,7 +54,6 @@ resource "coder_app" "cursor" {
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"cursor://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
|
||||
@@ -19,7 +19,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -32,7 +32,7 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -43,7 +43,7 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
}
|
||||
@@ -55,14 +55,14 @@ module "dotfiles" {
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "dotfiles-root" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
dotfiles_uri = module.dotfiles.dotfiles_uri
|
||||
@@ -77,7 +77,7 @@ You can set a default dotfiles repository for all users by setting the `default_
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
}
|
||||
|
||||
@@ -4,23 +4,11 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "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 "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
@@ -91,8 +79,6 @@ resource "coder_app" "dotfiles" {
|
||||
display_name = "Refresh Dotfiles"
|
||||
slug = "dotfiles"
|
||||
icon = "/icon/dotfiles.svg"
|
||||
order = var.order
|
||||
group = var.group
|
||||
command = templatefile("${path.module}/run.sh", {
|
||||
DOTFILES_URI : local.dotfiles_uri,
|
||||
DOTFILES_USER : local.user
|
||||
|
||||
@@ -15,7 +15,7 @@ A file browser for your workspace.
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
database_path = ".config/filebrowser.db"
|
||||
}
|
||||
@@ -54,7 +54,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "main"
|
||||
subdomain = false
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,12 +68,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the coder_app resource."
|
||||
@@ -114,7 +108,6 @@ resource "coder_app" "filebrowser" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
healthcheck {
|
||||
url = local.healthcheck_url
|
||||
@@ -127,4 +120,4 @@ locals {
|
||||
server_base_path = var.subdomain ? "" : format("/@%s/%s%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name != null ? ".${var.agent_name}" : "", var.slug)
|
||||
url = "http://localhost:${var.port}${local.server_base_path}"
|
||||
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/health"
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -24,14 +24,14 @@ module "goose" {
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `screen` or `tmux` must be installed in your workspace to run Goose in the background
|
||||
- `screen` must be installed in your workspace to run Goose in the background
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
|
||||
## Examples
|
||||
|
||||
Your workspace must have `screen` or `tmux` installed to use the background session functionality.
|
||||
Your workspace must have `screen` installed to use this.
|
||||
|
||||
### Run in the background and report tasks (Experimental)
|
||||
|
||||
@@ -90,7 +90,7 @@ resource "coder_agent" "main" {
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -99,12 +99,8 @@ module "goose" {
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
|
||||
# Run Goose in the background with screen (pick one: screen or tmux)
|
||||
# Run Goose in the background
|
||||
experiment_use_screen = true
|
||||
# experiment_use_tmux = true # Alternative: use tmux instead of screen
|
||||
|
||||
# Optional: customize the session name (defaults to "goose")
|
||||
# session_name = "goose-session"
|
||||
|
||||
# Avoid configuring Goose manually
|
||||
experiment_auto_configure = true
|
||||
@@ -147,12 +143,12 @@ Note: The indentation in the heredoc is preserved, so you can write the YAML nat
|
||||
|
||||
## Run standalone
|
||||
|
||||
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or tmux, and without any task reporting to the Coder UI.
|
||||
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
@@ -60,18 +54,6 @@ variable "experiment_use_screen" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Goose in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "session_name" {
|
||||
type = string
|
||||
description = "Name for the persistent session (screen or tmux)"
|
||||
default = "goose"
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
@@ -200,59 +182,15 @@ GOOSE_MODEL: ${var.experiment_goose_model}
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
fi
|
||||
|
||||
|
||||
# Write system prompt to config
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints"
|
||||
|
||||
# Handle terminal multiplexer selection (tmux or screen)
|
||||
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Goose in the background with tmux..."
|
||||
|
||||
# Check if tmux is installed
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.goose.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Goose in the background..."
|
||||
|
||||
|
||||
# Check if screen is installed
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
@@ -266,7 +204,7 @@ EOL
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.goose.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.goose.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
@@ -278,12 +216,32 @@ EOL
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
screen -U -dmS goose bash -c "
|
||||
cd ${var.folder}
|
||||
\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"
|
||||
/bin/bash
|
||||
"
|
||||
else
|
||||
# Check if goose is installed before running
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
EOT
|
||||
run_on_start = true
|
||||
@@ -312,31 +270,20 @@ resource "coder_app" "goose" {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Goose tmux session." | tee -a "$HOME/.goose.log"
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting a new Goose tmux session." | tee -a "$HOME/.goose.log"
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
# Check if session exists first
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
if ! screen -list | grep -q "goose"; then
|
||||
echo "Error: No existing Goose session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
# Only attach to existing session
|
||||
screen -xRR ${var.session_name}
|
||||
screen -xRR goose
|
||||
else
|
||||
cd ${var.folder}
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
"$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
||||
@@ -36,7 +36,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -50,7 +50,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -65,7 +65,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -90,7 +90,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -108,7 +108,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
http = {
|
||||
source = "hashicorp/http"
|
||||
@@ -45,7 +45,7 @@ variable "folder" {
|
||||
type = string
|
||||
description = "The directory to open in the IDE. e.g. /home/coder/project"
|
||||
validation {
|
||||
condition = can(regex("^(?:/[^/]+)+/?$", var.folder))
|
||||
condition = can(regex("^(?:/[^/]+)+$", var.folder))
|
||||
error_message = "The folder must be a full path and must not start with a ~."
|
||||
}
|
||||
}
|
||||
@@ -62,12 +62,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
@@ -330,7 +324,6 @@ resource "coder_app" "gateway" {
|
||||
icon = local.icon
|
||||
external = true
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"jetbrains-gateway://connect#type=coder&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
|
||||
@@ -17,7 +17,7 @@ A module that adds Jupyter Notebook in your Coder template.
|
||||
module "jupyter-notebook" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jupyter-notebook/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "jupyter-notebook" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "jupyter-notebook"
|
||||
@@ -68,5 +62,4 @@ resource "coder_app" "jupyter-notebook" {
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ A module that adds JupyterLab in your Coder template.
|
||||
module "jupyterlab" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jupyterlab/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,12 +51,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "jupyterlab" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "jupyterlab"
|
||||
@@ -78,5 +72,4 @@ resource "coder_app" "jupyterlab" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.0.23"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,56 +29,32 @@ variable "kasm_version" {
|
||||
variable "desktop_environment" {
|
||||
type = string
|
||||
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
|
||||
|
||||
validation {
|
||||
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
|
||||
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
|
||||
}
|
||||
}
|
||||
|
||||
variable "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 "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
default = true
|
||||
description = "Is subdomain sharing enabled in your cluster?"
|
||||
}
|
||||
|
||||
resource "coder_script" "kasm_vnc" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "KasmVNC"
|
||||
icon = "/icon/kasmvnc.svg"
|
||||
run_on_start = true
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
PORT = var.port,
|
||||
DESKTOP_ENVIRONMENT = var.desktop_environment,
|
||||
KASM_VERSION = var.kasm_version
|
||||
SUBDOMAIN = tostring(var.subdomain)
|
||||
PATH_VNC_HTML = var.subdomain ? "" : file("${path.module}/path_vnc.html")
|
||||
PORT : var.port,
|
||||
DESKTOP_ENVIRONMENT : var.desktop_environment,
|
||||
KASM_VERSION : var.kasm_version
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "kasm_vnc" {
|
||||
agent_id = var.agent_id
|
||||
slug = "kasm-vnc"
|
||||
display_name = "KasmVNC"
|
||||
display_name = "kasmVNC"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = "/icon/kasmvnc.svg"
|
||||
subdomain = var.subdomain
|
||||
subdomain = true
|
||||
share = "owner"
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:${var.port}/app"
|
||||
interval = 5
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Path-Sharing Bounce Page</title>
|
||||
<style type="text/css">
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--dark: #121212;
|
||||
--header-bg: rgba(127,127,127,0.2);
|
||||
--light: white;
|
||||
--rule-color: light-dark(rgba(0,0,0,0.8), rgba(255,255,255,0.8));
|
||||
background-color: light-dark(var(--light), var(--dark));
|
||||
color: light-dark(var(--dark), var(--light));
|
||||
}
|
||||
body, h1, p {
|
||||
box-sizing: border-box;
|
||||
margin:0; padding:0;
|
||||
}
|
||||
body{
|
||||
font-family:Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
h1{
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
letter-spacing: -1.5pt;
|
||||
padding-bottom:10px;
|
||||
border-bottom: 1px solid var(--rule-color);
|
||||
background-color: var(--header-bg);
|
||||
}
|
||||
p {
|
||||
padding: 1rem; letter-spacing: -0.5pt;}
|
||||
a.indent { display:inline-block; padding-top:0.5rem; padding-left: 2rem; font-size:0.8rem }
|
||||
</style>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Path-Sharing Bounce Page</h1>
|
||||
<p>
|
||||
This application is being served via path sharing.
|
||||
If you are not redirected, <span id="help">check the
|
||||
Javascript console in your browser's developer tools
|
||||
for more information.</span>
|
||||
</p>
|
||||
</body>
|
||||
<script language="javascript">
|
||||
// This page exists to satisfy the querystring driven client API
|
||||
// specified here - https://raw.githubusercontent.com/kasmtech/noVNC/bce2d6a7048025c6e6c05df9d98b206c23f6dbab/docs/EMBEDDING.md
|
||||
// tl;dr:
|
||||
// * `host` - The WebSocket host to connect to.
|
||||
// This is just the hostname component of the original URL
|
||||
// * `port` - The WebSocket port to connect to.
|
||||
// It doesn't look like we need to set this unless it's different
|
||||
// than the incoming http request.
|
||||
// * `encrypt` - If TLS should be used for the WebSocket connection.
|
||||
// we base this on whether or not the protocol is `https`, seems
|
||||
// reasonable for now.
|
||||
// * `path` - The WebSocket path to use.
|
||||
// This apparently doesn't tolerate a leading `/` so we use a
|
||||
// function to tidy that up.
|
||||
function trimFirstCharIf(str, char) {
|
||||
return str.charAt(0) === char ? str.slice(1) : str;
|
||||
}
|
||||
function trimLastCharIf(str, char) {
|
||||
return str.endsWith("/") ? str.slice(0,str.length-1) : str;
|
||||
}
|
||||
const newloc = new URL(window.location);
|
||||
const h = document.getElementById("help")
|
||||
|
||||
// Building the websockify path must happen before we append the filename to newloc.pathname
|
||||
newloc.searchParams.append("path",
|
||||
trimLastCharIf(trimFirstCharIf(newloc.pathname,"/"),"/")+"/websockify");
|
||||
newloc.searchParams.append("encrypted", newloc.protocol==="https:"? true : false);
|
||||
|
||||
newloc.pathname += "vnc.html"
|
||||
console.log(newloc);
|
||||
|
||||
h.innerHTML = `click <a id="link" href="${newloc.toString()}">here</a> to go to the application.
|
||||
<br/><br/>The rewritten URL is:<br/><a id="link" class="indent" href="${newloc.toString()}">${newloc.toString()}</a>`
|
||||
window.location = newloc.href;
|
||||
</script>
|
||||
</html>
|
||||
@@ -3,8 +3,6 @@
|
||||
# Exit on error, undefined variables, and pipe failures
|
||||
set -euo pipefail
|
||||
|
||||
error() { printf "💀 ERROR: %s\n" "$@"; exit 1; }
|
||||
|
||||
# Function to check if vncserver is already installed
|
||||
check_installed() {
|
||||
if command -v vncserver &> /dev/null; then
|
||||
@@ -190,7 +188,7 @@ if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
|
||||
SUDO=sudo
|
||||
else
|
||||
kasm_config_file="$HOME/.vnc/kasmvnc.yaml"
|
||||
SUDO=""
|
||||
SUDO=
|
||||
|
||||
echo "WARNING: Sudo access not available, using user config dir!"
|
||||
|
||||
@@ -208,7 +206,6 @@ echo "Writing KasmVNC config to $kasm_config_file"
|
||||
$SUDO tee "$kasm_config_file" > /dev/null << EOF
|
||||
network:
|
||||
protocol: http
|
||||
interface: 127.0.0.1
|
||||
websocket_port: ${PORT}
|
||||
ssl:
|
||||
require_ssl: false
|
||||
@@ -223,82 +220,16 @@ EOF
|
||||
# and does not listen publicly
|
||||
echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"
|
||||
|
||||
get_http_dir() {
|
||||
# determine the served file path
|
||||
# Start with the default
|
||||
httpd_directory="/usr/share/kasmvnc/www"
|
||||
|
||||
# Check the system configuration path
|
||||
if [[ -e /etc/kasmvnc/kasmvnc.yaml ]]; then
|
||||
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
|
||||
# If this grep is successful, it will return:
|
||||
# httpd_directory: /usr/share/kasmvnc/www
|
||||
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
|
||||
httpd_directory="$${d[1]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check the home directory for overriding values
|
||||
if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then
|
||||
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
|
||||
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
|
||||
httpd_directory="$${d[1]}"
|
||||
fi
|
||||
fi
|
||||
echo $httpd_directory
|
||||
}
|
||||
|
||||
fix_server_index_file(){
|
||||
local fname=$${FUNCNAME[0]} # gets current function name
|
||||
if [[ $# -ne 1 ]]; then
|
||||
error "$fname requires exactly 1 parameter:\n\tpath to KasmVNC httpd_directory"
|
||||
fi
|
||||
local httpdir="$1"
|
||||
if [[ ! -d "$httpdir" ]]; then
|
||||
error "$fname: $httpdir is not a directory"
|
||||
fi
|
||||
pushd "$httpdir" > /dev/null
|
||||
|
||||
cat <<'EOH' > /tmp/path_vnc.html
|
||||
${PATH_VNC_HTML}
|
||||
EOH
|
||||
$SUDO mv /tmp/path_vnc.html .
|
||||
# check for the switcheroo
|
||||
if [[ -f "index.html" && -L "vnc.html" ]]; then
|
||||
$SUDO mv $httpdir/index.html $httpdir/vnc.html
|
||||
fi
|
||||
$SUDO ln -s -f path_vnc.html index.html
|
||||
popd > /dev/null
|
||||
}
|
||||
|
||||
patch_kasm_http_files(){
|
||||
homedir=$(get_http_dir)
|
||||
fix_server_index_file "$homedir"
|
||||
}
|
||||
|
||||
if [[ "${SUBDOMAIN}" == "false" ]]; then
|
||||
echo "🩹 Patching up webserver files to support path-sharing..."
|
||||
patch_kasm_http_files
|
||||
fi
|
||||
|
||||
VNC_LOG="/tmp/kasmvncserver.log"
|
||||
# Start the server
|
||||
printf "🚀 Starting KasmVNC server...\n"
|
||||
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > /tmp/kasmvncserver.log 2>&1 &
|
||||
pid=$!
|
||||
|
||||
set +e
|
||||
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
|
||||
RETVAL=$?
|
||||
set -e
|
||||
|
||||
if [[ $RETVAL -ne 0 ]]; then
|
||||
echo "ERROR: Failed to start KasmVNC server. Return code: $RETVAL"
|
||||
if [[ -f "$VNC_LOG" ]]; then
|
||||
echo "Full logs:"
|
||||
cat "$VNC_LOG"
|
||||
else
|
||||
echo "ERROR: Log file not found: $VNC_LOG"
|
||||
fi
|
||||
# Wait for server to start
|
||||
sleep 5
|
||||
grep -v '^[[:space:]]*$' /tmp/kasmvncserver.log | tail -n 10
|
||||
if ps -p $pid | grep -q "^$pid"; then
|
||||
echo "ERROR: Failed to start KasmVNC server. Check full logs at /tmp/kasmvncserver.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "🚀 KasmVNC server started successfully!\n"
|
||||
|
||||
@@ -20,12 +20,11 @@ variable "vault_token" {
|
||||
}
|
||||
|
||||
module "vault" {
|
||||
source = "registry.coder.com/coder/vault-token/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_token = var.token # optional
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_namespace = "prod" # optional, vault enterprise only
|
||||
source = "registry.coder.com/coder/vault-token/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_token = var.token # optional
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,7 +74,7 @@ variable "vault_token" {
|
||||
|
||||
module "vault" {
|
||||
source = "registry.coder.com/coder/vault-token/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.token
|
||||
|
||||
@@ -26,11 +26,6 @@ variable "vault_token" {
|
||||
sensitive = true
|
||||
default = null
|
||||
}
|
||||
variable "vault_namespace" {
|
||||
type = string
|
||||
description = "The Vault namespace to use."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "vault_cli_version" {
|
||||
type = string
|
||||
@@ -67,10 +62,3 @@ resource "coder_env" "vault_token" {
|
||||
name = "VAULT_TOKEN"
|
||||
value = var.vault_token
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_namespace" {
|
||||
count = var.vault_namespace != null ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_NAMESPACE"
|
||||
value = var.vault_namespace
|
||||
}
|
||||
@@ -17,7 +17,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ module "vscode" {
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
@@ -48,8 +42,6 @@ resource "coder_app" "vscode" {
|
||||
slug = "vscode"
|
||||
display_name = "VS Code Desktop"
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
url = join("", [
|
||||
"vscode://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
|
||||
@@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
accept_license = true
|
||||
}
|
||||
@@ -31,7 +31,7 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_prefix = "/home/coder/.vscode-web"
|
||||
folder = "/home/coder"
|
||||
@@ -45,7 +45,7 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
||||
accept_license = true
|
||||
@@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -78,7 +78,7 @@ By default, this module installs the latest. To pin a specific version, retrieve
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,12 +97,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "settings" {
|
||||
type = any
|
||||
description = "A map of settings to apply to VS Code web."
|
||||
@@ -200,7 +194,6 @@ resource "coder_app" "vscode-web" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
healthcheck {
|
||||
url = local.healthcheck_url
|
||||
|
||||
@@ -17,7 +17,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ module "windsurf" {
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,6 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
@@ -48,7 +42,6 @@ resource "coder_app" "windsurf" {
|
||||
slug = "windsurf"
|
||||
display_name = "Windsurf Editor"
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"windsurf://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB |
@@ -2,7 +2,6 @@
|
||||
display_name: Nataindata
|
||||
bio: Data engineer
|
||||
github: nataindata
|
||||
avatar: ./.images/avatar.png
|
||||
website: https://www.nataindata.com
|
||||
status: community
|
||||
---
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,8 +1,7 @@
|
||||
---
|
||||
display_name: TheZoker
|
||||
bio: I'm a master computer science student at the TU munich and a web designer.
|
||||
bio: I'm a master computer science student at the TU munich and a webdesigner.
|
||||
github: TheZoker
|
||||
avatar: ./.images/avatar.jpeg
|
||||
website: https://gareis.io/
|
||||
status: community
|
||||
---
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,7 +1,6 @@
|
||||
---
|
||||
display_name: WhizUs
|
||||
bio: WhizUs is your premier choice for DevOps, Kubernetes, and Cloud Native consulting. Based in Vienna we combine our expert solutions with a strong commitment to the community. Explore automation, scalability and drive success through collaboration.
|
||||
avatar: ./.images/avatar.png
|
||||
github: WhizUs
|
||||
linkedin: https://www.linkedin.com/company/whizus
|
||||
website: https://www.whizus.com/
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
// Package gorules defines custom lint rules for ruleguard.
|
||||
//
|
||||
// golangci-lint runs these rules via go-critic, which includes support
|
||||
// for ruleguard. All Go files in this directory define lint rules
|
||||
// in the Ruleguard DSL; see:
|
||||
//
|
||||
// - https://go-ruleguard.github.io/by-example/
|
||||
// - https://pkg.go.dev/github.com/quasilyte/go-ruleguard/dsl
|
||||
//
|
||||
// You run one of the following commands to execute your go rules only:
|
||||
//
|
||||
// golangci-lint run
|
||||
// golangci-lint run --disable-all --enable=gocritic
|
||||
//
|
||||
// Note: don't forget to run `golangci-lint cache clean`!
|
||||
package gorules
|
||||
|
||||
import (
|
||||
"github.com/quasilyte/go-ruleguard/dsl"
|
||||
)
|
||||
|
||||
// Use xerrors everywhere! It provides additional stacktrace info!
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
func xerrors(m dsl.Matcher) {
|
||||
m.Import("errors")
|
||||
m.Import("fmt")
|
||||
m.Import("golang.org/x/xerrors")
|
||||
|
||||
m.Match("fmt.Errorf($arg)").
|
||||
Suggest("xerrors.New($arg)").
|
||||
Report("Use xerrors to provide additional stacktrace information!")
|
||||
|
||||
m.Match("fmt.Errorf($arg1, $*args)").
|
||||
Suggest("xerrors.Errorf($arg1, $args)").
|
||||
Report("Use xerrors to provide additional stacktrace information!")
|
||||
|
||||
m.Match("errors.$_($msg)").
|
||||
Where(m["msg"].Type.Is("string")).
|
||||
Suggest("xerrors.New($msg)").
|
||||
Report("Use xerrors to provide additional stacktrace information!")
|
||||
}
|
||||
|
||||
// databaseImport enforces not importing any database types into /codersdk.
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
func databaseImport(m dsl.Matcher) {
|
||||
m.Import("github.com/coder/coder/v2/coderd/database")
|
||||
m.Match("database.$_").
|
||||
Report("Do not import any database types into codersdk").
|
||||
Where(m.File().PkgPath.Matches("github.com/coder/coder/v2/codersdk"))
|
||||
}
|
||||
|
||||
// doNotCallTFailNowInsideGoroutine enforces not calling t.FailNow or
|
||||
// functions that may themselves call t.FailNow in goroutines outside
|
||||
// the main test goroutine. See testing.go:834 for why.
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
func doNotCallTFailNowInsideGoroutine(m dsl.Matcher) {
|
||||
m.Import("testing")
|
||||
m.Match(`
|
||||
go func($*_){
|
||||
$*_
|
||||
$require.$_($*_)
|
||||
$*_
|
||||
}($*_)`).
|
||||
At(m["require"]).
|
||||
Where(m["require"].Text == "require").
|
||||
Report("Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834)")
|
||||
|
||||
// require.Eventually runs the function in a goroutine.
|
||||
m.Match(`
|
||||
require.Eventually(t, func() bool {
|
||||
$*_
|
||||
$require.$_($*_)
|
||||
$*_
|
||||
}, $*_)`).
|
||||
At(m["require"]).
|
||||
Where(m["require"].Text == "require").
|
||||
Report("Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834)")
|
||||
|
||||
m.Match(`
|
||||
go func($*_){
|
||||
$*_
|
||||
$t.$fail($*_)
|
||||
$*_
|
||||
}($*_)`).
|
||||
At(m["fail"]).
|
||||
Where(m["t"].Type.Implements("testing.TB") && m["fail"].Text.Matches("^(FailNow|Fatal|Fatalf)$")).
|
||||
Report("Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834)")
|
||||
}
|
||||
|
||||
// useStandardTimeoutsAndDelaysInTests ensures all tests use common
|
||||
// constants for timeouts and delays in usual scenarios, this allows us
|
||||
// to tweak them based on platform (important to avoid CI flakes).
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
func useStandardTimeoutsAndDelaysInTests(m dsl.Matcher) {
|
||||
m.Import("github.com/stretchr/testify/require")
|
||||
m.Import("github.com/stretchr/testify/assert")
|
||||
m.Import("github.com/coder/coder/v2/testutil")
|
||||
|
||||
m.Match(`context.WithTimeout($ctx, $duration)`).
|
||||
Where(m.File().Imports("testing") && !m.File().PkgPath.Matches("testutil$") && !m["duration"].Text.Matches("^testutil\\.")).
|
||||
At(m["duration"]).
|
||||
Report("Do not use magic numbers in test timeouts and delays. Use the standard testutil.Wait* or testutil.Interval* constants instead.")
|
||||
|
||||
m.Match(`
|
||||
$testify.$Eventually($t, func() bool {
|
||||
$*_
|
||||
}, $timeout, $interval, $*_)
|
||||
`).
|
||||
Where((m["testify"].Text == "require" || m["testify"].Text == "assert") &&
|
||||
(m["Eventually"].Text == "Eventually" || m["Eventually"].Text == "Eventuallyf") &&
|
||||
!m["timeout"].Text.Matches("^testutil\\.")).
|
||||
At(m["timeout"]).
|
||||
Report("Do not use magic numbers in test timeouts and delays. Use the standard testutil.Wait* or testutil.Interval* constants instead.")
|
||||
|
||||
m.Match(`
|
||||
$testify.$Eventually($t, func() bool {
|
||||
$*_
|
||||
}, $timeout, $interval, $*_)
|
||||
`).
|
||||
Where((m["testify"].Text == "require" || m["testify"].Text == "assert") &&
|
||||
(m["Eventually"].Text == "Eventually" || m["Eventually"].Text == "Eventuallyf") &&
|
||||
!m["interval"].Text.Matches("^testutil\\.")).
|
||||
At(m["interval"]).
|
||||
Report("Do not use magic numbers in test timeouts and delays. Use the standard testutil.Wait* or testutil.Interval* constants instead.")
|
||||
}
|
||||
|
||||
// ProperRBACReturn ensures we always write to the response writer after a
|
||||
// call to Authorize. If we just do a return, the client will get a status code
|
||||
// 200, which is incorrect.
|
||||
func ProperRBACReturn(m dsl.Matcher) {
|
||||
m.Match(`
|
||||
if !$_.Authorize($*_) {
|
||||
return
|
||||
}
|
||||
`).Report("Must write to 'ResponseWriter' before returning'")
|
||||
}
|
||||
|
||||
// slogFieldNameSnakeCase is a lint rule that ensures naming consistency
|
||||
// of logged field names.
|
||||
func slogFieldNameSnakeCase(m dsl.Matcher) {
|
||||
m.Import("cdr.dev/slog")
|
||||
m.Match(
|
||||
`slog.F($name, $value)`,
|
||||
).
|
||||
Where(m["name"].Const && !m["name"].Text.Matches(`^"[a-z]+(_[a-z]+)*"$`)).
|
||||
Report("Field name $name must be snake_case.")
|
||||
}
|
||||
|
||||
// slogUUIDFieldNameHasIDSuffix ensures that "uuid.UUID" field has ID prefix
|
||||
// in the field name.
|
||||
func slogUUIDFieldNameHasIDSuffix(m dsl.Matcher) {
|
||||
m.Import("cdr.dev/slog")
|
||||
m.Import("github.com/google/uuid")
|
||||
m.Match(
|
||||
`slog.F($name, $value)`,
|
||||
).
|
||||
Where(m["value"].Type.Is("uuid.UUID") && !m["name"].Text.Matches(`_id"$`)).
|
||||
Report(`uuid.UUID field $name must have "_id" suffix.`)
|
||||
}
|
||||
|
||||
// slogMessageFormat ensures that the log message starts with lowercase, and does not
|
||||
// end with special character.
|
||||
func slogMessageFormat(m dsl.Matcher) {
|
||||
m.Import("cdr.dev/slog")
|
||||
m.Match(
|
||||
`logger.Error($ctx, $message, $*args)`,
|
||||
`logger.Warn($ctx, $message, $*args)`,
|
||||
`logger.Info($ctx, $message, $*args)`,
|
||||
`logger.Debug($ctx, $message, $*args)`,
|
||||
|
||||
`$foo.logger.Error($ctx, $message, $*args)`,
|
||||
`$foo.logger.Warn($ctx, $message, $*args)`,
|
||||
`$foo.logger.Info($ctx, $message, $*args)`,
|
||||
`$foo.logger.Debug($ctx, $message, $*args)`,
|
||||
|
||||
`Logger.Error($ctx, $message, $*args)`,
|
||||
`Logger.Warn($ctx, $message, $*args)`,
|
||||
`Logger.Info($ctx, $message, $*args)`,
|
||||
`Logger.Debug($ctx, $message, $*args)`,
|
||||
|
||||
`$foo.Logger.Error($ctx, $message, $*args)`,
|
||||
`$foo.Logger.Warn($ctx, $message, $*args)`,
|
||||
`$foo.Logger.Info($ctx, $message, $*args)`,
|
||||
`$foo.Logger.Debug($ctx, $message, $*args)`,
|
||||
).
|
||||
Where(
|
||||
(
|
||||
// It doesn't end with a special character:
|
||||
m["message"].Text.Matches(`[.!?]"$`) ||
|
||||
// it starts with lowercase:
|
||||
m["message"].Text.Matches(`^"[A-Z]{1}`) &&
|
||||
// but there are exceptions:
|
||||
!m["message"].Text.Matches(`^"Prometheus`) &&
|
||||
!m["message"].Text.Matches(`^"X11`) &&
|
||||
!m["message"].Text.Matches(`^"CSP`) &&
|
||||
!m["message"].Text.Matches(`^"OIDC`))).
|
||||
Report(`Message $message must start with lowercase, and does not end with a special characters.`)
|
||||
}
|
||||
|
||||
// slogMessageLength ensures that important log messages are meaningful, and must be at least 16 characters long.
|
||||
func slogMessageLength(m dsl.Matcher) {
|
||||
m.Import("cdr.dev/slog")
|
||||
m.Match(
|
||||
`logger.Error($ctx, $message, $*args)`,
|
||||
`logger.Warn($ctx, $message, $*args)`,
|
||||
`logger.Info($ctx, $message, $*args)`,
|
||||
|
||||
`$foo.logger.Error($ctx, $message, $*args)`,
|
||||
`$foo.logger.Warn($ctx, $message, $*args)`,
|
||||
`$foo.logger.Info($ctx, $message, $*args)`,
|
||||
|
||||
`Logger.Error($ctx, $message, $*args)`,
|
||||
`Logger.Warn($ctx, $message, $*args)`,
|
||||
`Logger.Info($ctx, $message, $*args)`,
|
||||
|
||||
`$foo.Logger.Error($ctx, $message, $*args)`,
|
||||
`$foo.Logger.Warn($ctx, $message, $*args)`,
|
||||
`$foo.Logger.Info($ctx, $message, $*args)`,
|
||||
|
||||
// no debug
|
||||
).
|
||||
Where(
|
||||
// It has at least 16 characters (+ ""):
|
||||
m["message"].Text.Matches(`^".{0,15}"$`) &&
|
||||
// but there are exceptions:
|
||||
!m["message"].Text.Matches(`^"command exit"$`)).
|
||||
Report(`Message $message is too short, it must be at least 16 characters long.`)
|
||||
}
|
||||
|
||||
// slogErr ensures that errors are logged with "slog.Error" instead of "slog.F"
|
||||
func slogError(m dsl.Matcher) {
|
||||
m.Import("cdr.dev/slog")
|
||||
m.Match(
|
||||
`slog.F($name, $value)`,
|
||||
).
|
||||
Where(m["name"].Const && m["value"].Type.Is("error") && !m["name"].Text.Matches(`^"internal_error"$`)).
|
||||
Report(`Error should be logged using "slog.Error" instead.`)
|
||||
}
|
||||
|
||||
// withTimezoneUTC ensures that we don't just sprinkle dbtestutil.WithTimezone("UTC") about
|
||||
// to work around real timezone bugs in our code.
|
||||
//
|
||||
//nolint:unused,deadcode,varnamelen
|
||||
func withTimezoneUTC(m dsl.Matcher) {
|
||||
m.Match(
|
||||
`dbtestutil.WithTimezone($tz)`,
|
||||
).Where(
|
||||
m["tz"].Text.Matches(`[uU][tT][cC]"$`),
|
||||
).Report(`Setting database timezone to UTC may mask timezone-related bugs.`).
|
||||
At(m["tz"])
|
||||
}
|
||||
Reference in New Issue
Block a user