Compare commits

...

13 Commits

Author SHA1 Message Date
Cian Johnston 41bfcccd4a chore: cherry-pick #16183 and #16303 for 2.18 (#16313) 2025-01-29 14:14:20 -06:00
Spike Curtis 8fffdce0a3 chore: add cherry-picks #15898 and #15927 to backport the fix to #15616 (#16265)
Cherry-picks #15898 and #15927 to backport the fix to #15616

#15927 is the fix, but it depends on #15898 because of some earlier work
in the tailscale fork to move everything to `coder/websocket`
2025-01-27 15:11:31 +04:00
gcp-cherry-pick-bot[bot] 6125358569 fix: fetch custom roles from workspace agent context (cherry-pick #16237) (#16246)
Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
2025-01-23 14:23:12 -06:00
gcp-cherry-pick-bot[bot] 803e2c7679 fix: change precedence order between coder_workspace_tags and request_tags (#16239)
Co-authored-by: Cian Johnston <cian@coder.com>
2025-01-23 14:19:04 -06:00
gcp-cherry-pick-bot[bot] bd6f4266a7 fix: return first workspace agent script timing per script (cherry-pick #16203) (#16208)
Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
Fixes https://github.com/coder/coder/issues/16124
2025-01-21 21:06:30 +05:00
Phorcys 075269a94e fix(examples/templates/docker): persist /home/coder instead of /home/${local.user} (#16190)
Backport of #16189, which is a fix for #16188
2025-01-19 08:42:35 +05:00
Ben Potter a5a7326415 chore: add cherry picks for v2.18.3 (#16187)
fixes #16182

<img width="1177" alt="Screenshot 2025-01-17 at 23 20 04"
src="https://github.com/user-attachments/assets/e0eec4bb-6b50-45e9-a27b-d3a89552562a"
/>
<img width="1163" alt="Screenshot 2025-01-17 at 23 20 12"
src="https://github.com/user-attachments/assets/d8a7129c-0240-4ef5-9f21-84cda5631366"
/>
<img width="1161" alt="Screenshot 2025-01-17 at 23 20 22"
src="https://github.com/user-attachments/assets/0ec78f3d-ec96-46a6-8def-aad4700712ac"
/>

(cherry picked from commit fbc2a73bef)

---------

Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
2025-01-18 10:21:56 -06:00
Stephen Kirby d15c4702b0 chore: add cherry-picks for patch 2.18.2 (#16061)
Co-authored-by: Cian Johnston <cian@coder.com>
Co-authored-by: Joobi S B <joobisb@gmail.com>
2025-01-07 15:05:04 -06:00
Stephen Kirby 765d99caa3 chore: cherry-pick commits for 2.18.1 (#15885)
Co-authored-by: Cian Johnston <cian@coder.com>
Co-authored-by: Eric Paulsen <ericpaulsen@coder.com>
Co-authored-by: Sas Swart <sas.swart.cdk@gmail.com>
2024-12-16 13:49:21 -06:00
Stephen Kirby c5e87690be chore: acquire lock for individual workspace transition (#15883)
When Coder is ran in High Availability mode, each Coder instance has a
lifecycle executor. These lifecycle executors are all trying to do the
same work, and whilst transactions saves us from this causing an issue,
we are still doing extra work that could be prevented.

This PR adds a `TryAcquireLock` call for each attempted workspace
transition, meaning two Coder instances shouldn't duplicate effort.

(cherry picked from commit 50ff06cc3c)

Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
2024-12-16 12:01:04 -06:00
Phorcys 4097ec3a6d fix(examples/templates/aws-linux): (backport) add hashicorp/cloudinit provider (#15796)
Backport #15794
2024-12-09 19:48:59 +05:00
Jon Ayers d8651d7682 fix: remove dylib job from release workflow (#15736)
cc @ethanndickson some errors happening when trying to run the release
workflow
2024-12-03 22:51:15 +02:00
Stephen Kirby 41359ce2fd chore: add cherry-picks for release 2.18 (#15735)
Co-authored-by: Hugo Dutka <hugo@coder.com>
Co-authored-by: Sas Swart <sas.swart.cdk@gmail.com>
Co-authored-by: Spike Curtis <spike@coder.com>
Co-authored-by: Cian Johnston <cian@coder.com>
2024-12-03 13:58:07 -06:00
139 changed files with 4711 additions and 1080 deletions
+4 -89
View File
@@ -32,80 +32,8 @@ env:
CODER_RELEASE_NOTES: ${{ inputs.release_notes }}
jobs:
# build-dylib is a separate job to build the dylib on macOS.
build-dylib:
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 0
- name: Setup build tools
run: |
brew install bash gnu-getopt make
echo "$(brew --prefix bash)/bin" >> $GITHUB_PATH
echo "$(brew --prefix gnu-getopt)/bin" >> $GITHUB_PATH
echo "$(brew --prefix make)/libexec/gnubin" >> $GITHUB_PATH
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Install rcodesign
run: |
set -euo pipefail
wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
sudo tar -xzf /tmp/rcodesign.tar.gz \
-C /usr/local/bin \
--strip-components=1 \
apple-codesign-0.22.0-macos-universal/rcodesign
rm /tmp/rcodesign.tar.gz
- name: Setup Apple Developer certificate and API key
run: |
set -euo pipefail
touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
chmod 600 /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
echo "$AC_CERTIFICATE_P12_BASE64" | base64 -d > /tmp/apple_cert.p12
echo "$AC_CERTIFICATE_PASSWORD" > /tmp/apple_cert_password.txt
echo "$AC_APIKEY_P8_BASE64" | base64 -d > /tmp/apple_apikey.p8
env:
AC_CERTIFICATE_P12_BASE64: ${{ secrets.AC_CERTIFICATE_P12_BASE64 }}
AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
- name: Build dylibs
run: |
set -euxo pipefail
go mod download
make gen/mark-fresh
make build/coder-dylib
env:
CODER_SIGN_DARWIN: 1
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
- name: Upload build artifacts
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dylibs
path: |
./build/*.h
./build/*.dylib
retention-days: 7
- name: Delete Apple Developer certificate and API key
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
release:
name: Build and publish
needs: build-dylib
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
permissions:
# Required to publish a release
@@ -217,18 +145,6 @@ jobs:
- name: Install nsis and zstd
run: sudo apt-get install -y nsis zstd
- name: Download dylibs
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dylibs
path: ./build
- name: Insert dylibs
run: |
mv ./build/*amd64.dylib ./site/out/bin/coder-vpn-darwin-amd64.dylib
mv ./build/*arm64.dylib ./site/out/bin/coder-vpn-darwin-arm64.dylib
mv ./build/*arm64.h ./site/out/bin/coder-vpn-darwin-dylib.h
- name: Install nfpm
run: |
set -euo pipefail
@@ -355,7 +271,6 @@ jobs:
${{ steps.image-base-tag.outputs.tag }}
- name: Verify that images are pushed properly
if: steps.image-base-tag.outputs.tag != ''
run: |
# retry 10 times with a 5 second delay as the images may not be
# available immediately
@@ -388,6 +303,10 @@ jobs:
run: |
set -euxo pipefail
# build Docker images for each architecture
version="$(./scripts/version.sh)"
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
# we can't build multi-arch if the images aren't pushed, so quit now
# if dry-running
if [[ "$CODER_RELEASE" != *t* ]]; then
@@ -395,10 +314,6 @@ jobs:
exit 0
fi
# build Docker images for each architecture
version="$(./scripts/version.sh)"
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
# build and push multi-arch manifest, this depends on the other images
# being pushed so will automatically push them.
make push/build/coder_"$version"_linux.tag
+132 -17
View File
@@ -300,9 +300,10 @@ func (m selectModel) filteredOptions() []string {
}
type MultiSelectOptions struct {
Message string
Options []string
Defaults []string
Message string
Options []string
Defaults []string
EnableCustomInput bool
}
func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) {
@@ -328,9 +329,10 @@ func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, er
}
initialModel := multiSelectModel{
search: textinput.New(),
options: options,
message: opts.Message,
search: textinput.New(),
options: options,
message: opts.Message,
enableCustomInput: opts.EnableCustomInput,
}
initialModel.search.Prompt = ""
@@ -370,12 +372,15 @@ type multiSelectOption struct {
}
type multiSelectModel struct {
search textinput.Model
options []*multiSelectOption
cursor int
message string
canceled bool
selected bool
search textinput.Model
options []*multiSelectOption
cursor int
message string
canceled bool
selected bool
isCustomInputMode bool // track if we're adding a custom option
customInput string // store custom input
enableCustomInput bool // control whether custom input is allowed
}
func (multiSelectModel) Init() tea.Cmd {
@@ -386,6 +391,10 @@ func (multiSelectModel) Init() tea.Cmd {
func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
if m.isCustomInputMode {
return m.handleCustomInputMode(msg)
}
switch msg := msg.(type) {
case terminateMsg:
m.canceled = true
@@ -398,6 +407,11 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
case tea.KeyEnter:
// Switch to custom input mode if we're on the "+ Add custom value:" option
if m.enableCustomInput && m.cursor == len(m.filteredOptions()) {
m.isCustomInputMode = true
return m, nil
}
if len(m.options) != 0 {
m.selected = true
return m, tea.Quit
@@ -413,16 +427,16 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case tea.KeyUp:
options := m.filteredOptions()
maxIndex := m.getMaxIndex()
if m.cursor > 0 {
m.cursor--
} else {
m.cursor = len(options) - 1
m.cursor = maxIndex
}
case tea.KeyDown:
options := m.filteredOptions()
if m.cursor < len(options)-1 {
maxIndex := m.getMaxIndex()
if m.cursor < maxIndex {
m.cursor++
} else {
m.cursor = 0
@@ -457,6 +471,91 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
func (m multiSelectModel) getMaxIndex() int {
options := m.filteredOptions()
if m.enableCustomInput {
// Include the "+ Add custom value" entry
return len(options)
}
// Includes only the actual options
return len(options) - 1
}
// handleCustomInputMode manages keyboard interactions when in custom input mode
func (m *multiSelectModel) handleCustomInputMode(msg tea.Msg) (tea.Model, tea.Cmd) {
keyMsg, ok := msg.(tea.KeyMsg)
if !ok {
return m, nil
}
switch keyMsg.Type {
case tea.KeyEnter:
return m.handleCustomInputSubmission()
case tea.KeyCtrlC:
m.canceled = true
return m, tea.Quit
case tea.KeyBackspace:
return m.handleCustomInputBackspace()
default:
m.customInput += keyMsg.String()
return m, nil
}
}
// handleCustomInputSubmission processes the submission of custom input
func (m *multiSelectModel) handleCustomInputSubmission() (tea.Model, tea.Cmd) {
if m.customInput == "" {
m.isCustomInputMode = false
return m, nil
}
// Clear search to ensure option is visible and cursor points to the new option
m.search.SetValue("")
// Check for duplicates
for i, opt := range m.options {
if opt.option == m.customInput {
// If the option exists but isn't chosen, select it
if !opt.chosen {
opt.chosen = true
}
// Point cursor to the new option
m.cursor = i
// Reset custom input mode to disabled
m.isCustomInputMode = false
m.customInput = ""
return m, nil
}
}
// Add new unique option
m.options = append(m.options, &multiSelectOption{
option: m.customInput,
chosen: true,
})
// Point cursor to the newly added option
m.cursor = len(m.options) - 1
// Reset custom input mode to disabled
m.customInput = ""
m.isCustomInputMode = false
return m, nil
}
// handleCustomInputBackspace handles backspace in custom input mode
func (m *multiSelectModel) handleCustomInputBackspace() (tea.Model, tea.Cmd) {
if len(m.customInput) > 0 {
m.customInput = m.customInput[:len(m.customInput)-1]
}
return m, nil
}
func (m multiSelectModel) View() string {
var s strings.Builder
@@ -469,13 +568,19 @@ func (m multiSelectModel) View() string {
return s.String()
}
if m.isCustomInputMode {
_, _ = s.WriteString(fmt.Sprintf("%s\nEnter custom value: %s\n", msg, m.customInput))
return s.String()
}
_, _ = s.WriteString(fmt.Sprintf(
"%s %s[Use arrows to move, space to select, <right> to all, <left> to none, type to filter]\n",
msg,
m.search.View(),
))
for i, option := range m.filteredOptions() {
options := m.filteredOptions()
for i, option := range options {
cursor := " "
chosen := "[ ]"
o := option.option
@@ -498,6 +603,16 @@ func (m multiSelectModel) View() string {
))
}
if m.enableCustomInput {
// Add the "+ Add custom value" option at the bottom
cursor := " "
text := " + Add custom value"
if m.cursor == len(options) {
cursor = pretty.Sprint(DefaultStyles.Keyword, "> ")
text = pretty.Sprint(DefaultStyles.Keyword, text)
}
_, _ = s.WriteString(fmt.Sprintf("%s%s\n", cursor, text))
}
return s.String()
}
+33
View File
@@ -101,6 +101,39 @@ func TestMultiSelect(t *testing.T) {
}()
require.Equal(t, items, <-msgChan)
})
t.Run("MultiSelectWithCustomInput", func(t *testing.T) {
t.Parallel()
items := []string{"Code", "Chairs", "Whale", "Diamond", "Carrot"}
ptty := ptytest.New(t)
msgChan := make(chan []string)
go func() {
resp, err := newMultiSelectWithCustomInput(ptty, items)
assert.NoError(t, err)
msgChan <- resp
}()
require.Equal(t, items, <-msgChan)
})
}
func newMultiSelectWithCustomInput(ptty *ptytest.PTY, items []string) ([]string, error) {
var values []string
cmd := &serpent.Command{
Handler: func(inv *serpent.Invocation) error {
selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
Options: items,
Defaults: items,
EnableCustomInput: true,
})
if err == nil {
values = selectedItems
}
return err
},
}
inv := cmd.Invoke()
ptty.Attach(inv)
return values, inv.Run()
}
func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
+53
View File
@@ -0,0 +1,53 @@
package cliutil
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
)
var (
warnNoMatchedProvisioners = `Your build has been enqueued, but there are no provisioners that accept the required tags. Once a compatible provisioner becomes available, your build will continue. Please contact your administrator.
Details:
Provisioner job ID : %s
Requested tags : %s
`
warnNoAvailableProvisioners = `Provisioners that accept the required tags have not responded for longer than expected. This may delay your build. Please contact your administrator if your build does not complete.
Details:
Provisioner job ID : %s
Requested tags : %s
Most recently seen : %s
`
)
// WarnMatchedProvisioners warns the user if there are no provisioners that
// match the requested tags for a given provisioner job.
// If the job is not pending, it is ignored.
func WarnMatchedProvisioners(w io.Writer, mp *codersdk.MatchedProvisioners, job codersdk.ProvisionerJob) {
if mp == nil {
// Nothing in the response, nothing to do here!
return
}
if job.Status != codersdk.ProvisionerJobPending {
// Only warn if the job is pending.
return
}
var tagsJSON strings.Builder
if err := json.NewEncoder(&tagsJSON).Encode(job.Tags); err != nil {
// Fall back to the less-pretty string representation.
tagsJSON.Reset()
_, _ = tagsJSON.WriteString(fmt.Sprintf("%v", job.Tags))
}
if mp.Count == 0 {
cliui.Warnf(w, warnNoMatchedProvisioners, job.ID, tagsJSON.String())
return
}
if mp.Available == 0 {
cliui.Warnf(w, warnNoAvailableProvisioners, job.ID, strings.TrimSpace(tagsJSON.String()), mp.MostRecentlySeen.Time)
return
}
}
+74
View File
@@ -0,0 +1,74 @@
package cliutil_test
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/codersdk"
)
func TestWarnMatchedProvisioners(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
name string
mp *codersdk.MatchedProvisioners
job codersdk.ProvisionerJob
expect string
}{
{
name: "no_match",
mp: &codersdk.MatchedProvisioners{
Count: 0,
Available: 0,
},
job: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
},
expect: `there are no provisioners that accept the required tags`,
},
{
name: "no_available",
mp: &codersdk.MatchedProvisioners{
Count: 1,
Available: 0,
},
job: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
},
expect: `Provisioners that accept the required tags have not responded for longer than expected`,
},
{
name: "match",
mp: &codersdk.MatchedProvisioners{
Count: 1,
Available: 1,
},
job: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobPending,
},
},
{
name: "not_pending",
mp: &codersdk.MatchedProvisioners{},
job: codersdk.ProvisionerJob{
Status: codersdk.ProvisionerJobRunning,
},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var w strings.Builder
cliutil.WarnMatchedProvisioners(&w, tt.mp, tt.job)
if tt.expect != "" {
require.Contains(t, w.String(), tt.expect)
} else {
require.Empty(t, w.String())
}
})
}
}
+10 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
@@ -289,7 +290,7 @@ func (r *RootCmd) create() *serpent.Command {
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
}
workspace, err := client.CreateWorkspace(inv.Context(), template.OrganizationID, workspaceOwner, codersdk.CreateWorkspaceRequest{
workspace, err := client.CreateUserWorkspace(inv.Context(), workspaceOwner, codersdk.CreateWorkspaceRequest{
TemplateVersionID: templateVersionID,
Name: workspaceName,
AutostartSchedule: schedSpec,
@@ -301,6 +302,8 @@ func (r *RootCmd) create() *serpent.Command {
return xerrors.Errorf("create workspace: %w", err)
}
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, workspace.LatestBuild.ID)
if err != nil {
return xerrors.Errorf("watch build: %w", err)
@@ -433,6 +436,12 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
if err != nil {
return nil, xerrors.Errorf("begin workspace dry-run: %w", err)
}
matchedProvisioners, err := client.TemplateVersionDryRunMatchedProvisioners(inv.Context(), templateVersion.ID, dryRun.ID)
if err != nil {
return nil, xerrors.Errorf("get matched provisioners: %w", err)
}
cliutil.WarnMatchedProvisioners(inv.Stdout, &matchedProvisioners, dryRun)
_, _ = fmt.Fprintln(inv.Stdout, "Planning workspace...")
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
+2
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
@@ -55,6 +56,7 @@ func (r *RootCmd) deleteWorkspace() *serpent.Command {
if err != nil {
return err
}
cliutil.WarnMatchedProvisioners(inv.Stdout, build.MatchedProvisioners, build.Job)
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
if err != nil {
+44
View File
@@ -12,6 +12,8 @@ import (
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
@@ -164,4 +166,46 @@ func TestDelete(t *testing.T) {
}()
<-doneChan
})
t.Run("WarnNoProvisioners", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
IncludeProvisionerDaemon: true,
})
// Given: a user, template, and workspace
user := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin())
version := coderdtest.CreateTemplateVersion(t, templateAdmin, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, templateAdmin, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, templateAdmin, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, workspace.LatestBuild.ID)
// When: all provisioner daemons disappear
require.NoError(t, closeDaemon.Close())
_, err := db.Exec("DELETE FROM provisioner_daemons;")
require.NoError(t, err)
// Then: the workspace deletion should warn about no provisioners
inv, root := clitest.New(t, "delete", workspace.Name, "-y")
pty := ptytest.New(t).Attach(inv)
clitest.SetupConfig(t, templateAdmin, root)
doneChan := make(chan struct{})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
go func() {
defer close(doneChan)
_ = inv.WithContext(ctx).Run()
}()
pty.ExpectMatch("there are no provisioners that accept the required tags")
cancel()
<-doneChan
})
}
+13 -3
View File
@@ -41,6 +41,15 @@ func (RootCmd) promptExample() *serpent.Command {
Default: "",
Value: serpent.StringArrayOf(&multiSelectValues),
}
enableCustomInput bool
enableCustomInputOption = serpent.Option{
Name: "enable-custom-input",
Description: "Enable custom input option in multi-select.",
Required: false,
Flag: "enable-custom-input",
Value: serpent.BoolOf(&enableCustomInput),
}
)
cmd := &serpent.Command{
Use: "prompt-example",
@@ -156,14 +165,15 @@ func (RootCmd) promptExample() *serpent.Command {
multiSelectValues, multiSelectError = cliui.MultiSelect(inv, cliui.MultiSelectOptions{
Message: "Select some things:",
Options: []string{
"Code", "Chair", "Whale", "Diamond", "Carrot",
"Code", "Chairs", "Whale", "Diamond", "Carrot",
},
Defaults: []string{"Code"},
Defaults: []string{"Code"},
EnableCustomInput: enableCustomInput,
})
}
_, _ = fmt.Fprintf(inv.Stdout, "%q are nice choices.\n", strings.Join(multiSelectValues, ", "))
return multiSelectError
}, useThingsOption),
}, useThingsOption, enableCustomInputOption),
promptCmd("rich-parameter", func(inv *serpent.Invocation) error {
value, err := cliui.RichSelect(inv, cliui.RichSelectOptions{
Options: []codersdk.TemplateVersionParameterOption{
+19
View File
@@ -8,6 +8,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
@@ -35,6 +36,23 @@ func (r *RootCmd) start() *serpent.Command {
}
var build codersdk.WorkspaceBuild
switch workspace.LatestBuild.Status {
case codersdk.WorkspaceStatusPending:
// The above check is technically duplicated in cliutil.WarnmatchedProvisioners
// but we still want to avoid users spamming multiple builds that will
// not be picked up.
_, _ = fmt.Fprintf(
inv.Stdout,
"\nThe %s workspace is waiting to start!\n",
cliui.Keyword(workspace.Name),
)
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enqueue another start?",
IsConfirm: true,
Default: cliui.ConfirmNo,
}); err != nil {
return err
}
case codersdk.WorkspaceStatusRunning:
_, _ = fmt.Fprintf(
inv.Stdout, "\nThe %s workspace is already running!\n",
@@ -159,6 +177,7 @@ func startWorkspace(inv *serpent.Invocation, client *codersdk.Client, workspace
if err != nil {
return codersdk.WorkspaceBuild{}, xerrors.Errorf("create workspace build: %w", err)
}
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
return build, nil
}
+17
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
@@ -36,6 +37,21 @@ func (r *RootCmd) stop() *serpent.Command {
if err != nil {
return err
}
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobPending {
// cliutil.WarnMatchedProvisioners also checks if the job is pending
// but we still want to avoid users spamming multiple builds that will
// not be picked up.
cliui.Warn(inv.Stderr, "The workspace is already stopping!")
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enqueue another stop?",
IsConfirm: true,
Default: cliui.ConfirmNo,
}); err != nil {
return err
}
}
wbr := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStop,
}
@@ -46,6 +62,7 @@ func (r *RootCmd) stop() *serpent.Command {
if err != nil {
return err
}
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
if err != nil {
+2 -25
View File
@@ -2,7 +2,6 @@ package cli
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
@@ -17,6 +16,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/pretty"
@@ -416,30 +416,7 @@ func createValidTemplateVersion(inv *serpent.Invocation, args createValidTemplat
if err != nil {
return nil, err
}
var tagsJSON strings.Builder
if err := json.NewEncoder(&tagsJSON).Encode(version.Job.Tags); err != nil {
// Fall back to the less-pretty string representation.
tagsJSON.Reset()
_, _ = tagsJSON.WriteString(fmt.Sprintf("%v", version.Job.Tags))
}
if version.MatchedProvisioners.Count == 0 {
cliui.Warnf(inv.Stderr, `No provisioners are available to handle the job!
Please contact your deployment administrator for assistance.
Details:
Provisioner job ID : %s
Requested tags : %s
`, version.Job.ID, tagsJSON.String())
} else if version.MatchedProvisioners.Available == 0 {
cliui.Warnf(inv.Stderr, `All available provisioner daemons have been silent for a while.
Your build will proceed once they become available.
If this persists, please contact your deployment administrator for assistance.
Details:
Provisioner job ID : %s
Requested tags : %s
Most recently seen : %s
`, version.Job.ID, strings.TrimSpace(tagsJSON.String()), version.MatchedProvisioners.MostRecentlySeen.Time)
}
cliutil.WarnMatchedProvisioners(inv.Stderr, version.MatchedProvisioners, version.Job)
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
version, err := client.TemplateVersion(inv.Context(), version.ID)
+149 -69
View File
@@ -3,6 +3,7 @@ package cli_test
import (
"bytes"
"context"
"database/sql"
"os"
"path/filepath"
"runtime"
@@ -18,6 +19,7 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
@@ -412,84 +414,162 @@ func TestTemplatePush(t *testing.T) {
t.Run("WorkspaceTagsTerraform", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
// Start an instance **without** a built-in provisioner.
// We're not actually testing that the Terraform applies.
// What we test is that a provisioner job is created with the expected
// tags based on the __content__ of the Terraform.
store, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
})
tests := []struct {
name string
setupDaemon func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error
expectOutput string
}{
{
name: "no provisioners available",
setupDaemon: func(_ context.Context, _ database.Store, _ codersdk.CreateFirstUserResponse, _ database.StringMap, _ time.Time) error {
return nil
},
expectOutput: "there are no provisioners that accept the required tags",
},
{
name: "provisioner stale",
setupDaemon: func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error {
pk, err := store.InsertProvisionerKey(ctx, database.InsertProvisionerKeyParams{
ID: uuid.New(),
CreatedAt: now,
OrganizationID: owner.OrganizationID,
Name: "test",
Tags: tags,
HashedSecret: []byte("secret"),
})
if err != nil {
return err
}
oneHourAgo := now.Add(-time.Hour)
_, err = store.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
LastSeenAt: sql.NullTime{Time: oneHourAgo, Valid: true},
CreatedAt: oneHourAgo,
Name: "test",
Tags: tags,
OrganizationID: owner.OrganizationID,
KeyID: pk.ID,
})
return err
},
expectOutput: "Provisioners that accept the required tags have not responded for longer than expected",
},
{
name: "active provisioner",
setupDaemon: func(ctx context.Context, store database.Store, owner codersdk.CreateFirstUserResponse, tags database.StringMap, now time.Time) error {
pk, err := store.InsertProvisionerKey(ctx, database.InsertProvisionerKeyParams{
ID: uuid.New(),
CreatedAt: now,
OrganizationID: owner.OrganizationID,
Name: "test",
Tags: tags,
HashedSecret: []byte("secret"),
})
if err != nil {
return err
}
_, err = store.UpsertProvisionerDaemon(ctx, database.UpsertProvisionerDaemonParams{
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
LastSeenAt: sql.NullTime{Time: now, Valid: true},
CreatedAt: now,
Name: "test-active",
Tags: tags,
OrganizationID: owner.OrganizationID,
KeyID: pk.ID,
})
return err
},
expectOutput: "",
},
}
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Create a tar file with some pre-defined content
tarFile := testutil.CreateTar(t, map[string]string{
"main.tf": `
variable "a" {
type = string
default = "1"
}
data "coder_parameter" "b" {
type = string
default = "2"
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
"foo": "bar",
"a": var.a,
"b": data.coder_parameter.b.value,
}
}`,
})
// Start an instance **without** a built-in provisioner.
// We're not actually testing that the Terraform applies.
// What we test is that a provisioner job is created with the expected
// tags based on the __content__ of the Terraform.
store, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
})
// Write the tar file to disk.
tempDir := t.TempDir()
err := tfparse.WriteArchive(tarFile, "application/x-tar", tempDir)
require.NoError(t, err)
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
// Run `coder templates push`
templateName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
var stdout, stderr strings.Builder
inv, root := clitest.New(t, "templates", "push", templateName, "-d", tempDir, "--yes")
inv.Stdout = &stdout
inv.Stderr = &stderr
clitest.SetupConfig(t, templateAdmin, root)
// Create a tar file with some pre-defined content
tarFile := testutil.CreateTar(t, map[string]string{
"main.tf": `
variable "a" {
type = string
default = "1"
}
data "coder_parameter" "b" {
type = string
default = "2"
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
"a": var.a,
"b": data.coder_parameter.b.value,
"test_name": "` + tt.name + `"
}
}`,
})
// Don't forget to clean up!
cancelCtx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)
done := make(chan error)
go func() {
done <- inv.WithContext(cancelCtx).Run()
}()
// Write the tar file to disk.
tempDir := t.TempDir()
err := tfparse.WriteArchive(tarFile, "application/x-tar", tempDir)
require.NoError(t, err)
// Assert that a provisioner job was created with the desired tags.
wantTags := database.StringMap(provisionersdk.MutateTags(uuid.Nil, map[string]string{
"foo": "bar",
"a": "1",
"b": "2",
}))
require.Eventually(t, func() bool {
jobs, err := store.GetProvisionerJobsCreatedAfter(ctx, time.Time{})
if !assert.NoError(t, err) {
return false
}
if len(jobs) == 0 {
return false
}
return assert.EqualValues(t, wantTags, jobs[0].Tags)
}, testutil.WaitShort, testutil.IntervalSlow)
wantTags := database.StringMap(provisionersdk.MutateTags(uuid.Nil, map[string]string{
"a": "1",
"b": "2",
"test_name": tt.name,
}))
cancel()
<-done
templateName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
require.Contains(t, stderr.String(), "No provisioners are available to handle the job!")
inv, root := clitest.New(t, "templates", "push", templateName, "-d", tempDir, "--yes")
clitest.SetupConfig(t, templateAdmin, root)
pty := ptytest.New(t).Attach(inv)
ctx := testutil.Context(t, testutil.WaitShort)
now := dbtime.Now()
require.NoError(t, tt.setupDaemon(ctx, store, owner, wantTags, now))
cancelCtx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)
done := make(chan error)
go func() {
done <- inv.WithContext(cancelCtx).Run()
}()
require.Eventually(t, func() bool {
jobs, err := store.GetProvisionerJobsCreatedAfter(ctx, time.Time{})
if !assert.NoError(t, err) {
return false
}
if len(jobs) == 0 {
return false
}
return assert.EqualValues(t, wantTags, jobs[0].Tags)
}, testutil.WaitShort, testutil.IntervalFast)
if tt.expectOutput != "" {
pty.ExpectMatch(tt.expectOutput)
}
cancel()
<-done
})
}
})
t.Run("ChangeTags", func(t *testing.T) {
+6 -1
View File
@@ -50,7 +50,12 @@
"deadline": "[timestamp]",
"max_deadline": null,
"status": "running",
"daily_cost": 0
"daily_cost": 0,
"matched_provisioners": {
"count": 0,
"available": 0,
"most_recently_seen": null
}
},
"outdated": false,
"name": "test-workspace",
+46
View File
@@ -4851,6 +4851,49 @@ const docTemplate = `{
}
}
},
"/templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": [
"application/json"
],
"tags": [
"Templates"
],
"summary": "Get template version dry-run matched provisioners",
"operationId": "get-template-version-dry-run-matched-provisioners",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Job ID",
"name": "jobID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.MatchedProvisioners"
}
}
}
}
},
"/templateversions/{templateversion}/dry-run/{jobID}/resources": {
"get": {
"security": [
@@ -15068,6 +15111,9 @@ const docTemplate = `{
"job": {
"$ref": "#/definitions/codersdk.ProvisionerJob"
},
"matched_provisioners": {
"$ref": "#/definitions/codersdk.MatchedProvisioners"
},
"max_deadline": {
"type": "string",
"format": "date-time"
+42
View File
@@ -4275,6 +4275,45 @@
}
}
},
"/templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"produces": ["application/json"],
"tags": ["Templates"],
"summary": "Get template version dry-run matched provisioners",
"operationId": "get-template-version-dry-run-matched-provisioners",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Job ID",
"name": "jobID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.MatchedProvisioners"
}
}
}
}
},
"/templateversions/{templateversion}/dry-run/{jobID}/resources": {
"get": {
"security": [
@@ -13712,6 +13751,9 @@
"job": {
"$ref": "#/definitions/codersdk.ProvisionerJob"
},
"matched_provisioners": {
"$ref": "#/definitions/codersdk.MatchedProvisioners"
},
"max_deadline": {
"type": "string",
"format": "date-time"
+12 -2
View File
@@ -3,6 +3,7 @@ package autobuild
import (
"context"
"database/sql"
"fmt"
"net/http"
"sync"
"sync/atomic"
@@ -177,6 +178,15 @@ func (e *Executor) runOnce(t time.Time) Stats {
err := e.db.InTx(func(tx database.Store) error {
var err error
ok, err := tx.TryAcquireLock(e.ctx, database.GenLockID(fmt.Sprintf("lifecycle-executor:%s", wsID)))
if err != nil {
return xerrors.Errorf("try acquire lifecycle executor lock: %w", err)
}
if !ok {
log.Debug(e.ctx, "unable to acquire lock for workspace, skipping")
return nil
}
// Re-check eligibility since the first check was outside the
// transaction and the workspace settings may have changed.
ws, err = tx.GetWorkspaceByID(e.ctx, wsID)
@@ -245,7 +255,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
}
}
nextBuild, job, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
nextBuild, job, _, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
if err != nil {
return xerrors.Errorf("build workspace with transition %q: %w", nextTransition, err)
}
@@ -372,7 +382,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
}
return nil
}()
if err != nil {
if err != nil && !xerrors.Is(err, context.Canceled) {
log.Error(e.ctx, "failed to transition workspace", slog.Error(err))
statsMu.Lock()
stats.Errors[wsID] = err
@@ -18,6 +18,7 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
"github.com/coder/coder/v2/coderd/schedule"
@@ -72,6 +73,76 @@ func TestExecutorAutostartOK(t *testing.T) {
require.Equal(t, template.AutostartRequirement.DaysOfWeek, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"})
}
func TestMultipleLifecycleExecutors(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
var (
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
// Create our first client
tickCh = make(chan time.Time, 2)
statsChA = make(chan autobuild.Stats)
clientA = coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
AutobuildTicker: tickCh,
AutobuildStats: statsChA,
Database: db,
Pubsub: ps,
})
// ... And then our second client
statsChB = make(chan autobuild.Stats)
_ = coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
AutobuildTicker: tickCh,
AutobuildStats: statsChB,
Database: db,
Pubsub: ps,
})
// Now create a workspace (we can use either client, it doesn't matter)
workspace = mustProvisionWorkspace(t, clientA, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = ptr.Ref(sched.String())
})
)
// Have the workspace stopped so we can perform an autostart
workspace = coderdtest.MustTransitionWorkspace(t, clientA, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
// Get both clients to perform a lifecycle execution tick
next := sched.Next(workspace.LatestBuild.CreatedAt)
startCh := make(chan struct{})
go func() {
<-startCh
tickCh <- next
}()
go func() {
<-startCh
tickCh <- next
}()
close(startCh)
// Now we want to check the stats for both clients
statsA := <-statsChA
statsB := <-statsChB
// We expect there to be no errors
assert.Len(t, statsA.Errors, 0)
assert.Len(t, statsB.Errors, 0)
// We also expect there to have been only one transition
require.Equal(t, 1, len(statsA.Transitions)+len(statsB.Transitions))
stats := statsA
if len(statsB.Transitions) == 1 {
stats = statsB
}
// And we expect this transition to have been a start transition
assert.Contains(t, stats.Transitions, workspace.ID)
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
}
func TestExecutorAutostartTemplateUpdated(t *testing.T) {
t.Parallel()
+3 -1
View File
@@ -628,7 +628,8 @@ func New(options *Options) *API {
CurrentVersion: buildinfo.Version(),
CurrentAPIMajorVersion: proto.CurrentMajor,
Store: options.Database,
// TimeNow and StaleInterval set to defaults, see healthcheck/provisioner.go
StaleInterval: provisionerdserver.StaleInterval,
// TimeNow set to default, see healthcheck/provisioner.go
},
})
}
@@ -1054,6 +1055,7 @@ func New(options *Options) *API {
r.Get("/{jobID}", api.templateVersionDryRun)
r.Get("/{jobID}/resources", api.templateVersionDryRunResources)
r.Get("/{jobID}/logs", api.templateVersionDryRunLogs)
r.Get("/{jobID}/matched-provisioners", api.templateVersionDryRunMatchedProvisioners)
r.Patch("/{jobID}/cancel", api.patchTemplateVersionDryRunCancel)
})
})
+20
View File
@@ -673,3 +673,23 @@ func CryptoKey(key database.CryptoKey) codersdk.CryptoKey {
Secret: key.Secret.String,
}
}
func MatchedProvisioners(provisionerDaemons []database.ProvisionerDaemon, now time.Time, staleInterval time.Duration) codersdk.MatchedProvisioners {
minLastSeenAt := now.Add(-staleInterval)
mostRecentlySeen := codersdk.NullTime{}
var matched codersdk.MatchedProvisioners
for _, provisioner := range provisionerDaemons {
if !provisioner.LastSeenAt.Valid {
continue
}
matched.Count++
if provisioner.LastSeenAt.Time.After(minLastSeenAt) {
matched.Available++
}
if provisioner.LastSeenAt.Time.After(mostRecentlySeen.Time) {
matched.MostRecentlySeen.Valid = true
matched.MostRecentlySeen.Time = provisioner.LastSeenAt.Time
}
}
return matched
}
+35 -8
View File
@@ -299,7 +299,7 @@ var (
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionDelete, policy.ActionRead},
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
rbac.ResourceProvisionerKeys.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
@@ -317,6 +317,23 @@ var (
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
subjectSystemReadProvisionerDaemons = rbac.Subject{
FriendlyName: "Provisioner Daemons Reader",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "system-read-provisioner-daemons"},
DisplayName: "Coder",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceProvisionerDaemon.Type: {policy.ActionRead},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
)
// AsProvisionerd returns a context with an actor that has permissions required
@@ -359,6 +376,12 @@ func AsSystemRestricted(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, subjectSystemRestricted)
}
// AsSystemReadProvisionerDaemons returns a context with an actor that has permissions
// to read provisioner daemons.
func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context {
return context.WithValue(ctx, authContextKey{}, subjectSystemReadProvisionerDaemons)
}
var AsRemoveActor = rbac.Subject{
ID: "remove-actor",
}
@@ -1538,6 +1561,10 @@ func (q *querier) GetDeploymentWorkspaceStats(ctx context.Context) (database.Get
return q.db.GetDeploymentWorkspaceStats(ctx)
}
func (q *querier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetEligibleProvisionerDaemonsByProvisionerJobIDs)(ctx, provisionerJobIds)
}
func (q *querier) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetExternalAuthLink)(ctx, arg)
}
@@ -3330,13 +3357,6 @@ func (q *querier) RegisterWorkspaceProxy(ctx context.Context, arg database.Regis
return updateWithReturn(q.log, q.auth, fetch, q.db.RegisterWorkspaceProxy)(ctx, arg)
}
func (q *querier) RemoveRefreshToken(ctx context.Context, arg database.RemoveRefreshTokenParams) error {
fetch := func(ctx context.Context, arg database.RemoveRefreshTokenParams) (database.ExternalAuthLink, error) {
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
}
return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.RemoveRefreshToken)(ctx, arg)
}
func (q *querier) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error {
// This is a system function to clear user groups in group sync.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
@@ -3435,6 +3455,13 @@ func (q *querier) UpdateExternalAuthLink(ctx context.Context, arg database.Updat
return fetchAndQuery(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateExternalAuthLink)(ctx, arg)
}
func (q *querier) UpdateExternalAuthLinkRefreshToken(ctx context.Context, arg database.UpdateExternalAuthLinkRefreshTokenParams) error {
fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkRefreshTokenParams) (database.ExternalAuthLink, error) {
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
}
return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, fetch, q.db.UpdateExternalAuthLinkRefreshToken)(ctx, arg)
}
func (q *querier) UpdateGitSSHKey(ctx context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) {
fetch := func(ctx context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) {
return q.db.GetGitSSHKey(ctx, arg.UserID)
+30 -5
View File
@@ -1282,12 +1282,14 @@ func (s *MethodTestSuite) TestUser() {
UserID: u.ID,
}).Asserts(u, policy.ActionUpdatePersonal)
}))
s.Run("RemoveRefreshToken", s.Subtest(func(db database.Store, check *expects) {
s.Run("UpdateExternalAuthLinkRefreshToken", s.Subtest(func(db database.Store, check *expects) {
link := dbgen.ExternalAuthLink(s.T(), db, database.ExternalAuthLink{})
check.Args(database.RemoveRefreshTokenParams{
ProviderID: link.ProviderID,
UserID: link.UserID,
UpdatedAt: link.UpdatedAt,
check.Args(database.UpdateExternalAuthLinkRefreshTokenParams{
OAuthRefreshToken: "",
OAuthRefreshTokenKeyID: "",
ProviderID: link.ProviderID,
UserID: link.UserID,
UpdatedAt: link.UpdatedAt,
}).Asserts(rbac.ResourceUserObject(link.UserID), policy.ActionUpdatePersonal)
}))
s.Run("UpdateExternalAuthLink", s.Subtest(func(db database.Store, check *expects) {
@@ -2098,6 +2100,29 @@ func (s *MethodTestSuite) TestExtraMethods() {
s.NoError(err, "get provisioner daemon by org")
check.Args(database.GetProvisionerDaemonsByOrganizationParams{OrganizationID: org.ID}).Asserts(d, policy.ActionRead).Returns(ds)
}))
s.Run("GetEligibleProvisionerDaemonsByProvisionerJobIDs", s.Subtest(func(db database.Store, check *expects) {
org := dbgen.Organization(s.T(), db, database.Organization{})
tags := database.StringMap(map[string]string{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
})
j, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
Tags: tags,
Provisioner: database.ProvisionerTypeEcho,
StorageMethod: database.ProvisionerStorageMethodFile,
})
s.NoError(err, "insert provisioner job")
d, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
OrganizationID: org.ID,
Tags: tags,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
})
s.NoError(err, "insert provisioner daemon")
ds, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{j.ID})
s.NoError(err, "get provisioner daemon by org")
check.Args(uuid.UUIDs{j.ID}).Asserts(d, policy.ActionRead).Returns(ds)
}))
s.Run("DeleteOldProvisionerDaemons", s.Subtest(func(db database.Store, check *expects) {
_, err := db.UpsertProvisionerDaemon(context.Background(), database.UpsertProvisionerDaemonParams{
Tags: database.StringMap(map[string]string{
+62 -13
View File
@@ -209,9 +209,17 @@ func WorkspaceAgentScript(t testing.TB, db database.Store, orig database.Workspa
return scripts[0]
}
func WorkspaceAgentScriptTimings(t testing.TB, db database.Store, script database.WorkspaceAgentScript, count int) []database.WorkspaceAgentScriptTiming {
timings := make([]database.WorkspaceAgentScriptTiming, count)
for i := range count {
func WorkspaceAgentScripts(t testing.TB, db database.Store, count int, orig database.WorkspaceAgentScript) []database.WorkspaceAgentScript {
scripts := make([]database.WorkspaceAgentScript, 0, count)
for range count {
scripts = append(scripts, WorkspaceAgentScript(t, db, orig))
}
return scripts
}
func WorkspaceAgentScriptTimings(t testing.TB, db database.Store, scripts []database.WorkspaceAgentScript) []database.WorkspaceAgentScriptTiming {
timings := make([]database.WorkspaceAgentScriptTiming, len(scripts))
for i, script := range scripts {
timings[i] = WorkspaceAgentScriptTiming(t, db, database.WorkspaceAgentScriptTiming{
ScriptID: script.ID,
})
@@ -502,6 +510,46 @@ func GroupMember(t testing.TB, db database.Store, member database.GroupMemberTab
return groupMember
}
// ProvisionerDaemon creates a provisioner daemon as far as the database is concerned. It does not run a provisioner daemon.
// If no key is provided, it will create one.
func ProvisionerDaemon(t testing.TB, db database.Store, daemon database.ProvisionerDaemon) database.ProvisionerDaemon {
t.Helper()
if daemon.KeyID == uuid.Nil {
key, err := db.InsertProvisionerKey(genCtx, database.InsertProvisionerKeyParams{
ID: uuid.New(),
Name: daemon.Name + "-key",
OrganizationID: daemon.OrganizationID,
HashedSecret: []byte("secret"),
CreatedAt: dbtime.Now(),
Tags: daemon.Tags,
})
require.NoError(t, err)
daemon.KeyID = key.ID
}
if daemon.CreatedAt.IsZero() {
daemon.CreatedAt = dbtime.Now()
}
if daemon.Name == "" {
daemon.Name = "test-daemon"
}
d, err := db.UpsertProvisionerDaemon(genCtx, database.UpsertProvisionerDaemonParams{
Name: daemon.Name,
OrganizationID: daemon.OrganizationID,
CreatedAt: daemon.CreatedAt,
Provisioners: daemon.Provisioners,
Tags: daemon.Tags,
KeyID: daemon.KeyID,
LastSeenAt: daemon.LastSeenAt,
Version: daemon.Version,
APIVersion: daemon.APIVersion,
})
require.NoError(t, err)
return d
}
// ProvisionerJob is a bit more involved to get the values such as "completedAt", "startedAt", "cancelledAt" set. ps
// can be set to nil if you are SURE that you don't require a provisionerdaemon to acquire the job in your test.
func ProvisionerJob(t testing.TB, db database.Store, ps pubsub.Pubsub, orig database.ProvisionerJob) database.ProvisionerJob {
@@ -788,16 +836,17 @@ func TemplateVersion(t testing.TB, db database.Store, orig database.TemplateVers
err := db.InTx(func(db database.Store) error {
versionID := takeFirst(orig.ID, uuid.New())
err := db.InsertTemplateVersion(genCtx, database.InsertTemplateVersionParams{
ID: versionID,
TemplateID: takeFirst(orig.TemplateID, uuid.NullUUID{}),
OrganizationID: takeFirst(orig.OrganizationID, uuid.New()),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
Name: takeFirst(orig.Name, testutil.GetRandomName(t)),
Message: orig.Message,
Readme: takeFirst(orig.Readme, testutil.GetRandomName(t)),
JobID: takeFirst(orig.JobID, uuid.New()),
CreatedBy: takeFirst(orig.CreatedBy, uuid.New()),
ID: versionID,
TemplateID: takeFirst(orig.TemplateID, uuid.NullUUID{}),
OrganizationID: takeFirst(orig.OrganizationID, uuid.New()),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
Name: takeFirst(orig.Name, testutil.GetRandomName(t)),
Message: orig.Message,
Readme: takeFirst(orig.Readme, testutil.GetRandomName(t)),
JobID: takeFirst(orig.JobID, uuid.New()),
CreatedBy: takeFirst(orig.CreatedBy, uuid.New()),
SourceExampleID: takeFirst(orig.SourceExampleID, sql.NullString{}),
})
if err != nil {
return err
+120 -45
View File
@@ -1119,6 +1119,14 @@ func (q *FakeQuerier) getWorkspaceAgentScriptsByAgentIDsNoLock(ids []uuid.UUID)
return scripts, nil
}
// getOwnerFromTags returns the lowercase owner from tags, matching SQL's COALESCE(tags ->> 'owner', ”)
func getOwnerFromTags(tags map[string]string) string {
if owner, ok := tags["owner"]; ok {
return strings.ToLower(owner)
}
return ""
}
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
return xerrors.New("AcquireLock must only be called within a transaction")
}
@@ -2743,6 +2751,63 @@ func (q *FakeQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (database
return stat, nil
}
func (q *FakeQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(_ context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
results := make([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, 0)
seen := make(map[string]struct{}) // Track unique combinations
for _, jobID := range provisionerJobIds {
var job database.ProvisionerJob
found := false
for _, j := range q.provisionerJobs {
if j.ID == jobID {
job = j
found = true
break
}
}
if !found {
continue
}
for _, daemon := range q.provisionerDaemons {
if daemon.OrganizationID != job.OrganizationID {
continue
}
if !tagsSubset(job.Tags, daemon.Tags) {
continue
}
provisionerMatches := false
for _, p := range daemon.Provisioners {
if p == job.Provisioner {
provisionerMatches = true
break
}
}
if !provisionerMatches {
continue
}
key := jobID.String() + "-" + daemon.ID.String()
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
results = append(results, database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{
JobID: jobID,
ProvisionerDaemon: daemon,
})
}
}
return results, nil
}
func (q *FakeQuerier) GetExternalAuthLink(_ context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
if err := validateDatabaseType(arg); err != nil {
return database.ExternalAuthLink{}, err
@@ -5953,6 +6018,15 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex
WorkspaceAgentName: agent.Name,
})
}
// We want to only return the first script run for each Script ID.
slices.SortFunc(rows, func(a, b database.GetWorkspaceAgentScriptTimingsByBuildIDRow) int {
return a.StartedAt.Compare(b.StartedAt)
})
rows = slices.CompactFunc(rows, func(e1, e2 database.GetWorkspaceAgentScriptTimingsByBuildIDRow) bool {
return e1.ScriptID == e2.ScriptID
})
return rows, nil
}
@@ -7699,16 +7773,17 @@ func (q *FakeQuerier) InsertTemplateVersion(_ context.Context, arg database.Inse
//nolint:gosimple
version := database.TemplateVersionTable{
ID: arg.ID,
TemplateID: arg.TemplateID,
OrganizationID: arg.OrganizationID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Name: arg.Name,
Message: arg.Message,
Readme: arg.Readme,
JobID: arg.JobID,
CreatedBy: arg.CreatedBy,
ID: arg.ID,
TemplateID: arg.TemplateID,
OrganizationID: arg.OrganizationID,
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Name: arg.Name,
Message: arg.Message,
Readme: arg.Readme,
JobID: arg.JobID,
CreatedBy: arg.CreatedBy,
SourceExampleID: arg.SourceExampleID,
}
q.templateVersions = append(q.templateVersions, version)
return nil
@@ -8555,29 +8630,6 @@ func (q *FakeQuerier) RegisterWorkspaceProxy(_ context.Context, arg database.Reg
return database.WorkspaceProxy{}, sql.ErrNoRows
}
func (q *FakeQuerier) RemoveRefreshToken(_ context.Context, arg database.RemoveRefreshTokenParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for index, gitAuthLink := range q.externalAuthLinks {
if gitAuthLink.ProviderID != arg.ProviderID {
continue
}
if gitAuthLink.UserID != arg.UserID {
continue
}
gitAuthLink.UpdatedAt = arg.UpdatedAt
gitAuthLink.OAuthRefreshToken = ""
q.externalAuthLinks[index] = gitAuthLink
return nil
}
return sql.ErrNoRows
}
func (q *FakeQuerier) RemoveUserFromAllGroups(_ context.Context, userID uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -8797,6 +8849,29 @@ func (q *FakeQuerier) UpdateExternalAuthLink(_ context.Context, arg database.Upd
return database.ExternalAuthLink{}, sql.ErrNoRows
}
func (q *FakeQuerier) UpdateExternalAuthLinkRefreshToken(_ context.Context, arg database.UpdateExternalAuthLinkRefreshTokenParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for index, gitAuthLink := range q.externalAuthLinks {
if gitAuthLink.ProviderID != arg.ProviderID {
continue
}
if gitAuthLink.UserID != arg.UserID {
continue
}
gitAuthLink.UpdatedAt = arg.UpdatedAt
gitAuthLink.OAuthRefreshToken = arg.OAuthRefreshToken
q.externalAuthLinks[index] = gitAuthLink
return nil
}
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateGitSSHKey(_ context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) {
if err := validateDatabaseType(arg); err != nil {
return database.GitSSHKey{}, err
@@ -10248,25 +10323,26 @@ func (q *FakeQuerier) UpsertOAuthSigningKey(_ context.Context, value string) err
}
func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.UpsertProvisionerDaemonParams) (database.ProvisionerDaemon, error) {
err := validateDatabaseType(arg)
if err != nil {
if err := validateDatabaseType(arg); err != nil {
return database.ProvisionerDaemon{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for _, d := range q.provisionerDaemons {
if d.Name == arg.Name {
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeOrganization && arg.Tags[provisionersdk.TagOwner] != "" {
continue
}
if d.Tags[provisionersdk.TagScope] == provisionersdk.ScopeUser && arg.Tags[provisionersdk.TagOwner] != d.Tags[provisionersdk.TagOwner] {
continue
}
// Look for existing daemon using the same composite key as SQL
for i, d := range q.provisionerDaemons {
if d.OrganizationID == arg.OrganizationID &&
d.Name == arg.Name &&
getOwnerFromTags(d.Tags) == getOwnerFromTags(arg.Tags) {
d.Provisioners = arg.Provisioners
d.Tags = maps.Clone(arg.Tags)
d.Version = arg.Version
d.LastSeenAt = arg.LastSeenAt
d.Version = arg.Version
d.APIVersion = arg.APIVersion
d.OrganizationID = arg.OrganizationID
d.KeyID = arg.KeyID
q.provisionerDaemons[i] = d
return d, nil
}
}
@@ -10276,7 +10352,6 @@ func (q *FakeQuerier) UpsertProvisionerDaemon(_ context.Context, arg database.Up
Name: arg.Name,
Provisioners: arg.Provisioners,
Tags: maps.Clone(arg.Tags),
ReplicaID: uuid.NullUUID{},
LastSeenAt: arg.LastSeenAt,
Version: arg.Version,
APIVersion: arg.APIVersion,
+14 -7
View File
@@ -630,6 +630,13 @@ func (m queryMetricsStore) GetDeploymentWorkspaceStats(ctx context.Context) (dat
return row, err
}
func (m queryMetricsStore) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
start := time.Now()
r0, r1 := m.s.GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx, provisionerJobIds)
m.queryLatencies.WithLabelValues("GetEligibleProvisionerDaemonsByProvisionerJobIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetExternalAuthLink(ctx context.Context, arg database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
start := time.Now()
link, err := m.s.GetExternalAuthLink(ctx, arg)
@@ -2093,13 +2100,6 @@ func (m queryMetricsStore) RegisterWorkspaceProxy(ctx context.Context, arg datab
return proxy, err
}
func (m queryMetricsStore) RemoveRefreshToken(ctx context.Context, arg database.RemoveRefreshTokenParams) error {
start := time.Now()
r0 := m.s.RemoveRefreshToken(ctx, arg)
m.queryLatencies.WithLabelValues("RemoveRefreshToken").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error {
start := time.Now()
r0 := m.s.RemoveUserFromAllGroups(ctx, userID)
@@ -2170,6 +2170,13 @@ func (m queryMetricsStore) UpdateExternalAuthLink(ctx context.Context, arg datab
return link, err
}
func (m queryMetricsStore) UpdateExternalAuthLinkRefreshToken(ctx context.Context, arg database.UpdateExternalAuthLinkRefreshTokenParams) error {
start := time.Now()
r0 := m.s.UpdateExternalAuthLinkRefreshToken(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateExternalAuthLinkRefreshToken").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) UpdateGitSSHKey(ctx context.Context, arg database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) {
start := time.Now()
key, err := m.s.UpdateGitSSHKey(ctx, arg)
+29 -14
View File
@@ -1267,6 +1267,21 @@ func (mr *MockStoreMockRecorder) GetDeploymentWorkspaceStats(arg0 any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentWorkspaceStats", reflect.TypeOf((*MockStore)(nil).GetDeploymentWorkspaceStats), arg0)
}
// GetEligibleProvisionerDaemonsByProvisionerJobIDs mocks base method.
func (m *MockStore) GetEligibleProvisionerDaemonsByProvisionerJobIDs(arg0 context.Context, arg1 []uuid.UUID) ([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEligibleProvisionerDaemonsByProvisionerJobIDs", arg0, arg1)
ret0, _ := ret[0].([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEligibleProvisionerDaemonsByProvisionerJobIDs indicates an expected call of GetEligibleProvisionerDaemonsByProvisionerJobIDs.
func (mr *MockStoreMockRecorder) GetEligibleProvisionerDaemonsByProvisionerJobIDs(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEligibleProvisionerDaemonsByProvisionerJobIDs", reflect.TypeOf((*MockStore)(nil).GetEligibleProvisionerDaemonsByProvisionerJobIDs), arg0, arg1)
}
// GetExternalAuthLink mocks base method.
func (m *MockStore) GetExternalAuthLink(arg0 context.Context, arg1 database.GetExternalAuthLinkParams) (database.ExternalAuthLink, error) {
m.ctrl.T.Helper()
@@ -4463,20 +4478,6 @@ func (mr *MockStoreMockRecorder) RegisterWorkspaceProxy(arg0, arg1 any) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterWorkspaceProxy", reflect.TypeOf((*MockStore)(nil).RegisterWorkspaceProxy), arg0, arg1)
}
// RemoveRefreshToken mocks base method.
func (m *MockStore) RemoveRefreshToken(arg0 context.Context, arg1 database.RemoveRefreshTokenParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveRefreshToken", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveRefreshToken indicates an expected call of RemoveRefreshToken.
func (mr *MockStoreMockRecorder) RemoveRefreshToken(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRefreshToken", reflect.TypeOf((*MockStore)(nil).RemoveRefreshToken), arg0, arg1)
}
// RemoveUserFromAllGroups mocks base method.
func (m *MockStore) RemoveUserFromAllGroups(arg0 context.Context, arg1 uuid.UUID) error {
m.ctrl.T.Helper()
@@ -4622,6 +4623,20 @@ func (mr *MockStoreMockRecorder) UpdateExternalAuthLink(arg0, arg1 any) *gomock.
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExternalAuthLink", reflect.TypeOf((*MockStore)(nil).UpdateExternalAuthLink), arg0, arg1)
}
// UpdateExternalAuthLinkRefreshToken mocks base method.
func (m *MockStore) UpdateExternalAuthLinkRefreshToken(arg0 context.Context, arg1 database.UpdateExternalAuthLinkRefreshTokenParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateExternalAuthLinkRefreshToken", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateExternalAuthLinkRefreshToken indicates an expected call of UpdateExternalAuthLinkRefreshToken.
func (mr *MockStoreMockRecorder) UpdateExternalAuthLinkRefreshToken(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExternalAuthLinkRefreshToken", reflect.TypeOf((*MockStore)(nil).UpdateExternalAuthLinkRefreshToken), arg0, arg1)
}
// UpdateGitSSHKey mocks base method.
func (m *MockStore) UpdateGitSSHKey(arg0 context.Context, arg1 database.UpdateGitSSHKeyParams) (database.GitSSHKey, error) {
m.ctrl.T.Helper()
+3 -1
View File
@@ -1217,7 +1217,8 @@ CREATE TABLE template_versions (
created_by uuid NOT NULL,
external_auth_providers jsonb DEFAULT '[]'::jsonb NOT NULL,
message character varying(1048576) DEFAULT ''::character varying NOT NULL,
archived boolean DEFAULT false NOT NULL
archived boolean DEFAULT false NOT NULL,
source_example_id text
);
COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of External auth providers for a specific template version';
@@ -1245,6 +1246,7 @@ CREATE VIEW template_version_with_user AS
template_versions.external_auth_providers,
template_versions.message,
template_versions.archived,
template_versions.source_example_id,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username
FROM (template_versions
@@ -0,0 +1,28 @@
-- We cannot alter the column type while a view depends on it, so we drop it and recreate it.
DROP VIEW template_version_with_user;
ALTER TABLE
template_versions
DROP COLUMN source_example_id;
-- Recreate `template_version_with_user` as described in dump.sql
CREATE VIEW template_version_with_user AS
SELECT
template_versions.id,
template_versions.template_id,
template_versions.organization_id,
template_versions.created_at,
template_versions.updated_at,
template_versions.name,
template_versions.readme,
template_versions.job_id,
template_versions.created_by,
template_versions.external_auth_providers,
template_versions.message,
template_versions.archived,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username
FROM (template_versions
LEFT JOIN visible_users ON (template_versions.created_by = visible_users.id));
COMMENT ON VIEW template_version_with_user IS 'Joins in the username + avatar url of the created by user.';
@@ -0,0 +1,30 @@
-- We cannot alter the column type while a view depends on it, so we drop it and recreate it.
DROP VIEW template_version_with_user;
ALTER TABLE
template_versions
ADD
COLUMN source_example_id TEXT;
-- Recreate `template_version_with_user` as described in dump.sql
CREATE VIEW template_version_with_user AS
SELECT
template_versions.id,
template_versions.template_id,
template_versions.organization_id,
template_versions.created_at,
template_versions.updated_at,
template_versions.name,
template_versions.readme,
template_versions.job_id,
template_versions.created_by,
template_versions.external_auth_providers,
template_versions.message,
template_versions.archived,
template_versions.source_example_id,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username
FROM (template_versions
LEFT JOIN visible_users ON (template_versions.created_by = visible_users.id));
COMMENT ON VIEW template_version_with_user IS 'Joins in the username + avatar url of the created by user.';
+4
View File
@@ -268,6 +268,10 @@ func (p ProvisionerDaemon) RBACObject() rbac.Object {
InOrg(p.OrganizationID)
}
func (p GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) RBACObject() rbac.Object {
return p.ProvisionerDaemon.RBACObject()
}
func (p ProvisionerKey) RBACObject() rbac.Object {
return rbac.ResourceProvisionerKeys.
WithID(p.ID).
+4 -2
View File
@@ -2773,6 +2773,7 @@ type TemplateVersion struct {
ExternalAuthProviders json.RawMessage `db:"external_auth_providers" json:"external_auth_providers"`
Message string `db:"message" json:"message"`
Archived bool `db:"archived" json:"archived"`
SourceExampleID sql.NullString `db:"source_example_id" json:"source_example_id"`
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
}
@@ -2826,8 +2827,9 @@ type TemplateVersionTable struct {
// IDs of External auth providers for a specific template version
ExternalAuthProviders json.RawMessage `db:"external_auth_providers" json:"external_auth_providers"`
// Message describing the changes in this version of the template, similar to a Git commit message. Like a commit message, this should be a short, high-level description of the changes in this version of the template. This message is immutable and should not be updated after the fact.
Message string `db:"message" json:"message"`
Archived bool `db:"archived" json:"archived"`
Message string `db:"message" json:"message"`
Archived bool `db:"archived" json:"archived"`
SourceExampleID sql.NullString `db:"source_example_id" json:"source_example_id"`
}
type TemplateVersionVariable struct {
+2 -4
View File
@@ -144,6 +144,7 @@ type sqlcQuerier interface {
GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error)
GetDeploymentWorkspaceAgentUsageStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentUsageStatsRow, error)
GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error)
GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error)
GetExternalAuthLink(ctx context.Context, arg GetExternalAuthLinkParams) (ExternalAuthLink, error)
GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]ExternalAuthLink, error)
GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error)
@@ -424,10 +425,6 @@ type sqlcQuerier interface {
OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error)
ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error
RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error)
// Removing the refresh token disables the refresh behavior for a given
// auth token. If a refresh token is marked invalid, it is better to remove it
// then continually attempt to refresh the token.
RemoveRefreshToken(ctx context.Context, arg RemoveRefreshTokenParams) error
RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error
RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error)
RevokeDBCryptKey(ctx context.Context, activeKeyDigest string) error
@@ -443,6 +440,7 @@ type sqlcQuerier interface {
UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error)
UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error)
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
UpdateExternalAuthLinkRefreshToken(ctx context.Context, arg UpdateExternalAuthLinkRefreshTokenParams) error
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
+140
View File
@@ -27,6 +27,7 @@ import (
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/testutil"
)
@@ -211,6 +212,145 @@ func TestGetDeploymentWorkspaceAgentUsageStats(t *testing.T) {
})
}
func TestGetEligibleProvisionerDaemonsByProvisionerJobIDs(t *testing.T) {
t.Parallel()
t.Run("NoJobsReturnsEmpty", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{})
require.NoError(t, err)
require.Empty(t, daemons)
})
t.Run("MatchesProvisionerType", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
Provisioner: database.ProvisionerTypeEcho,
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
})
matchingDaemon := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "matching-daemon",
OrganizationID: org.ID,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
})
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "non-matching-daemon",
OrganizationID: org.ID,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
})
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{job.ID})
require.NoError(t, err)
require.Len(t, daemons, 1)
require.Equal(t, matchingDaemon.ID, daemons[0].ProvisionerDaemon.ID)
})
t.Run("MatchesOrganizationScope", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
Provisioner: database.ProvisionerTypeEcho,
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
provisionersdk.TagOwner: "",
},
})
orgDaemon := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "org-daemon",
OrganizationID: org.ID,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
provisionersdk.TagOwner: "",
},
})
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "user-daemon",
OrganizationID: org.ID,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeUser,
},
})
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{job.ID})
require.NoError(t, err)
require.Len(t, daemons, 1)
require.Equal(t, orgDaemon.ID, daemons[0].ProvisionerDaemon.ID)
})
t.Run("MatchesMultipleProvisioners", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
Type: database.ProvisionerJobTypeWorkspaceBuild,
Provisioner: database.ProvisionerTypeEcho,
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
})
daemon1 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "daemon-1",
OrganizationID: org.ID,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
})
daemon2 := dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "daemon-2",
OrganizationID: org.ID,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho},
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
})
dbgen.ProvisionerDaemon(t, db, database.ProvisionerDaemon{
Name: "daemon-3",
OrganizationID: org.ID,
Provisioners: []database.ProvisionerType{database.ProvisionerTypeTerraform},
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
},
})
daemons, err := db.GetEligibleProvisionerDaemonsByProvisionerJobIDs(context.Background(), []uuid.UUID{job.ID})
require.NoError(t, err)
require.Len(t, daemons, 2)
daemonIDs := []uuid.UUID{daemons[0].ProvisionerDaemon.ID, daemons[1].ProvisionerDaemon.ID}
require.ElementsMatch(t, []uuid.UUID{daemon1.ID, daemon2.ID}, daemonIDs)
})
}
func TestGetWorkspaceAgentUsageStats(t *testing.T) {
t.Parallel()
+120 -44
View File
@@ -1194,29 +1194,6 @@ func (q *sqlQuerier) InsertExternalAuthLink(ctx context.Context, arg InsertExter
return i, err
}
const removeRefreshToken = `-- name: RemoveRefreshToken :exec
UPDATE
external_auth_links
SET
oauth_refresh_token = '',
updated_at = $1
WHERE provider_id = $2 AND user_id = $3
`
type RemoveRefreshTokenParams struct {
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ProviderID string `db:"provider_id" json:"provider_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
// Removing the refresh token disables the refresh behavior for a given
// auth token. If a refresh token is marked invalid, it is better to remove it
// then continually attempt to refresh the token.
func (q *sqlQuerier) RemoveRefreshToken(ctx context.Context, arg RemoveRefreshTokenParams) error {
_, err := q.db.ExecContext(ctx, removeRefreshToken, arg.UpdatedAt, arg.ProviderID, arg.UserID)
return err
}
const updateExternalAuthLink = `-- name: UpdateExternalAuthLink :one
UPDATE external_auth_links SET
updated_at = $3,
@@ -1269,6 +1246,40 @@ func (q *sqlQuerier) UpdateExternalAuthLink(ctx context.Context, arg UpdateExter
return i, err
}
const updateExternalAuthLinkRefreshToken = `-- name: UpdateExternalAuthLinkRefreshToken :exec
UPDATE
external_auth_links
SET
oauth_refresh_token = $1,
updated_at = $2
WHERE
provider_id = $3
AND
user_id = $4
AND
-- Required for sqlc to generate a parameter for the oauth_refresh_token_key_id
$5 :: text = $5 :: text
`
type UpdateExternalAuthLinkRefreshTokenParams struct {
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ProviderID string `db:"provider_id" json:"provider_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
OAuthRefreshTokenKeyID string `db:"oauth_refresh_token_key_id" json:"oauth_refresh_token_key_id"`
}
func (q *sqlQuerier) UpdateExternalAuthLinkRefreshToken(ctx context.Context, arg UpdateExternalAuthLinkRefreshTokenParams) error {
_, err := q.db.ExecContext(ctx, updateExternalAuthLinkRefreshToken,
arg.OAuthRefreshToken,
arg.UpdatedAt,
arg.ProviderID,
arg.UserID,
arg.OAuthRefreshTokenKeyID,
)
return err
}
const getFileByHashAndCreator = `-- name: GetFileByHashAndCreator :one
SELECT
hash, created_at, created_by, mimetype, data, id
@@ -5244,6 +5255,60 @@ func (q *sqlQuerier) DeleteOldProvisionerDaemons(ctx context.Context) error {
return err
}
const getEligibleProvisionerDaemonsByProvisionerJobIDs = `-- name: GetEligibleProvisionerDaemonsByProvisionerJobIDs :many
SELECT DISTINCT
provisioner_jobs.id as job_id, provisioner_daemons.id, provisioner_daemons.created_at, provisioner_daemons.name, provisioner_daemons.provisioners, provisioner_daemons.replica_id, provisioner_daemons.tags, provisioner_daemons.last_seen_at, provisioner_daemons.version, provisioner_daemons.api_version, provisioner_daemons.organization_id, provisioner_daemons.key_id
FROM
provisioner_jobs
JOIN
provisioner_daemons ON provisioner_daemons.organization_id = provisioner_jobs.organization_id
AND provisioner_tagset_contains(provisioner_daemons.tags::tagset, provisioner_jobs.tags::tagset)
AND provisioner_jobs.provisioner = ANY(provisioner_daemons.provisioners)
WHERE
provisioner_jobs.id = ANY($1 :: uuid[])
`
type GetEligibleProvisionerDaemonsByProvisionerJobIDsRow struct {
JobID uuid.UUID `db:"job_id" json:"job_id"`
ProvisionerDaemon ProvisionerDaemon `db:"provisioner_daemon" json:"provisioner_daemon"`
}
func (q *sqlQuerier) GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx context.Context, provisionerJobIds []uuid.UUID) ([]GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getEligibleProvisionerDaemonsByProvisionerJobIDs, pq.Array(provisionerJobIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
for rows.Next() {
var i GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
if err := rows.Scan(
&i.JobID,
&i.ProvisionerDaemon.ID,
&i.ProvisionerDaemon.CreatedAt,
&i.ProvisionerDaemon.Name,
pq.Array(&i.ProvisionerDaemon.Provisioners),
&i.ProvisionerDaemon.ReplicaID,
&i.ProvisionerDaemon.Tags,
&i.ProvisionerDaemon.LastSeenAt,
&i.ProvisionerDaemon.Version,
&i.ProvisionerDaemon.APIVersion,
&i.ProvisionerDaemon.OrganizationID,
&i.ProvisionerDaemon.KeyID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getProvisionerDaemons = `-- name: GetProvisionerDaemons :many
SELECT
id, created_at, name, provisioners, replica_id, tags, last_seen_at, version, api_version, organization_id, key_id
@@ -8996,7 +9061,7 @@ FROM
-- Scope an archive to a single template and ignore already archived template versions
(
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id
FROM
template_versions
WHERE
@@ -9097,7 +9162,7 @@ func (q *sqlQuerier) ArchiveUnusedTemplateVersions(ctx context.Context, arg Arch
const getPreviousTemplateVersion = `-- name: GetPreviousTemplateVersion :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, created_by_avatar_url, created_by_username
FROM
template_version_with_user AS template_versions
WHERE
@@ -9134,6 +9199,7 @@ func (q *sqlQuerier) GetPreviousTemplateVersion(ctx context.Context, arg GetPrev
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
)
@@ -9142,7 +9208,7 @@ func (q *sqlQuerier) GetPreviousTemplateVersion(ctx context.Context, arg GetPrev
const getTemplateVersionByID = `-- name: GetTemplateVersionByID :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, created_by_avatar_url, created_by_username
FROM
template_version_with_user AS template_versions
WHERE
@@ -9165,6 +9231,7 @@ func (q *sqlQuerier) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
)
@@ -9173,7 +9240,7 @@ func (q *sqlQuerier) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (
const getTemplateVersionByJobID = `-- name: GetTemplateVersionByJobID :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, created_by_avatar_url, created_by_username
FROM
template_version_with_user AS template_versions
WHERE
@@ -9196,6 +9263,7 @@ func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.U
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
)
@@ -9204,7 +9272,7 @@ func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.U
const getTemplateVersionByTemplateIDAndName = `-- name: GetTemplateVersionByTemplateIDAndName :one
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, created_by_avatar_url, created_by_username
FROM
template_version_with_user AS template_versions
WHERE
@@ -9233,6 +9301,7 @@ func (q *sqlQuerier) GetTemplateVersionByTemplateIDAndName(ctx context.Context,
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
)
@@ -9241,7 +9310,7 @@ func (q *sqlQuerier) GetTemplateVersionByTemplateIDAndName(ctx context.Context,
const getTemplateVersionsByIDs = `-- name: GetTemplateVersionsByIDs :many
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, created_by_avatar_url, created_by_username
FROM
template_version_with_user AS template_versions
WHERE
@@ -9270,6 +9339,7 @@ func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UU
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
); err != nil {
@@ -9288,7 +9358,7 @@ func (q *sqlQuerier) GetTemplateVersionsByIDs(ctx context.Context, ids []uuid.UU
const getTemplateVersionsByTemplateID = `-- name: GetTemplateVersionsByTemplateID :many
SELECT
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, created_by_avatar_url, created_by_username
FROM
template_version_with_user AS template_versions
WHERE
@@ -9364,6 +9434,7 @@ func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg Ge
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
); err != nil {
@@ -9381,7 +9452,7 @@ func (q *sqlQuerier) GetTemplateVersionsByTemplateID(ctx context.Context, arg Ge
}
const getTemplateVersionsCreatedAfter = `-- name: GetTemplateVersionsCreatedAfter :many
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, created_by_avatar_url, created_by_username FROM template_version_with_user AS template_versions WHERE created_at > $1
SELECT id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by, external_auth_providers, message, archived, source_example_id, created_by_avatar_url, created_by_username FROM template_version_with_user AS template_versions WHERE created_at > $1
`
func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error) {
@@ -9406,6 +9477,7 @@ func (q *sqlQuerier) GetTemplateVersionsCreatedAfter(ctx context.Context, create
&i.ExternalAuthProviders,
&i.Message,
&i.Archived,
&i.SourceExampleID,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
); err != nil {
@@ -9434,23 +9506,25 @@ INSERT INTO
message,
readme,
job_id,
created_by
created_by,
source_example_id
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`
type InsertTemplateVersionParams struct {
ID uuid.UUID `db:"id" json:"id"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Message string `db:"message" json:"message"`
Readme string `db:"readme" json:"readme"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
ID uuid.UUID `db:"id" json:"id"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Name string `db:"name" json:"name"`
Message string `db:"message" json:"message"`
Readme string `db:"readme" json:"readme"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
SourceExampleID sql.NullString `db:"source_example_id" json:"source_example_id"`
}
func (q *sqlQuerier) InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error {
@@ -9465,6 +9539,7 @@ func (q *sqlQuerier) InsertTemplateVersion(ctx context.Context, arg InsertTempla
arg.Readme,
arg.JobID,
arg.CreatedBy,
arg.SourceExampleID,
)
return err
}
@@ -11581,7 +11656,7 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks
const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
SELECT
workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status,
DISTINCT ON (workspace_agent_script_timings.script_id) workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status,
workspace_agent_scripts.display_name,
workspace_agents.id as workspace_agent_id,
workspace_agents.name as workspace_agent_name
@@ -11591,6 +11666,7 @@ INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.wor
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
WHERE workspace_builds.id = $1
ORDER BY workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at
`
type GetWorkspaceAgentScriptTimingsByBuildIDRow struct {
+9 -6
View File
@@ -43,13 +43,16 @@ UPDATE external_auth_links SET
oauth_extra = $9
WHERE provider_id = $1 AND user_id = $2 RETURNING *;
-- name: RemoveRefreshToken :exec
-- Removing the refresh token disables the refresh behavior for a given
-- auth token. If a refresh token is marked invalid, it is better to remove it
-- then continually attempt to refresh the token.
-- name: UpdateExternalAuthLinkRefreshToken :exec
UPDATE
external_auth_links
SET
oauth_refresh_token = '',
oauth_refresh_token = @oauth_refresh_token,
updated_at = @updated_at
WHERE provider_id = @provider_id AND user_id = @user_id;
WHERE
provider_id = @provider_id
AND
user_id = @user_id
AND
-- Required for sqlc to generate a parameter for the oauth_refresh_token_key_id
@oauth_refresh_token_key_id :: text = @oauth_refresh_token_key_id :: text;
@@ -16,6 +16,18 @@ WHERE
-- adding support for searching by tags:
(@want_tags :: tagset = 'null' :: tagset OR provisioner_tagset_contains(provisioner_daemons.tags::tagset, @want_tags::tagset));
-- name: GetEligibleProvisionerDaemonsByProvisionerJobIDs :many
SELECT DISTINCT
provisioner_jobs.id as job_id, sqlc.embed(provisioner_daemons)
FROM
provisioner_jobs
JOIN
provisioner_daemons ON provisioner_daemons.organization_id = provisioner_jobs.organization_id
AND provisioner_tagset_contains(provisioner_daemons.tags::tagset, provisioner_jobs.tags::tagset)
AND provisioner_jobs.provisioner = ANY(provisioner_daemons.provisioners)
WHERE
provisioner_jobs.id = ANY(@provisioner_job_ids :: uuid[]);
-- name: DeleteOldProvisionerDaemons :exec
-- Delete provisioner daemons that have been created at least a week ago
-- and have not connected to coderd since a week.
+3 -2
View File
@@ -87,10 +87,11 @@ INSERT INTO
message,
readme,
job_id,
created_by
created_by,
source_example_id
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
-- name: UpdateTemplateVersionByID :exec
UPDATE
+3 -2
View File
@@ -304,7 +304,7 @@ RETURNING workspace_agent_script_timings.*;
-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
SELECT
workspace_agent_script_timings.*,
DISTINCT ON (workspace_agent_script_timings.script_id) workspace_agent_script_timings.*,
workspace_agent_scripts.display_name,
workspace_agents.id as workspace_agent_id,
workspace_agents.name as workspace_agent_name
@@ -313,4 +313,5 @@ INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_age
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
WHERE workspace_builds.id = $1;
WHERE workspace_builds.id = $1
ORDER BY workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at;
+6 -4
View File
@@ -143,10 +143,12 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
// get rid of it. Keeping it around will cause additional refresh
// attempts that will fail and cost us api rate limits.
if isFailedRefresh(existingToken, err) {
dbExecErr := db.RemoveRefreshToken(ctx, database.RemoveRefreshTokenParams{
UpdatedAt: dbtime.Now(),
ProviderID: externalAuthLink.ProviderID,
UserID: externalAuthLink.UserID,
dbExecErr := db.UpdateExternalAuthLinkRefreshToken(ctx, database.UpdateExternalAuthLinkRefreshTokenParams{
OAuthRefreshToken: "", // It is better to clear the refresh token than to keep retrying.
OAuthRefreshTokenKeyID: externalAuthLink.OAuthRefreshTokenKeyID.String,
UpdatedAt: dbtime.Now(),
ProviderID: externalAuthLink.ProviderID,
UserID: externalAuthLink.UserID,
})
if dbExecErr != nil {
// This error should be rare.
+1 -1
View File
@@ -190,7 +190,7 @@ func TestRefreshToken(t *testing.T) {
// Try again with a bad refresh token error
// Expect DB call to remove the refresh token
mDB.EXPECT().RemoveRefreshToken(gomock.Any(), gomock.Any()).Return(nil).Times(1)
mDB.EXPECT().UpdateExternalAuthLinkRefreshToken(gomock.Any(), gomock.Any()).Return(nil).Times(1)
refreshErr = &oauth2.RetrieveError{ // github error
Response: &http.Response{
StatusCode: http.StatusOK,
+1 -1
View File
@@ -50,7 +50,7 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae
now := opts.TimeNow()
if opts.StaleInterval == 0 {
opts.StaleInterval = provisionerdserver.DefaultHeartbeatInterval * 3
opts.StaleInterval = provisionerdserver.StaleInterval
}
if opts.CurrentVersion == "" {
+83 -29
View File
@@ -15,15 +15,21 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/healthcheck"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/testutil"
)
func TestProvisionerDaemonReport(t *testing.T) {
t.Parallel()
now := dbtime.Now()
var (
now = dbtime.Now()
oneHourAgo = now.Add(-time.Hour)
staleThreshold = now.Add(-provisionerdserver.StaleInterval).Add(-time.Second)
)
for _, tt := range []struct {
name string
@@ -65,7 +71,9 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentVersion: "v1.2.3",
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityOK,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-ok"), withVersion("v1.2.3"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -88,7 +96,9 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-old"), withVersion("v1.1.2"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -116,7 +126,9 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityError,
expectedWarningCode: health.CodeUnknown,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-version", "invalid", "1.0", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-invalid-version"), withVersion("invalid"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -144,7 +156,9 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityError,
expectedWarningCode: health.CodeUnknown,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-invalid-api", "v1.2.3", "invalid", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-invalid-api"), withVersion("v1.2.3"), withAPIVersion("invalid"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -172,7 +186,9 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentAPIMajorVersion: 2,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonAPIMajorVersionDeprecated,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-old-api", "v2.3.4", "1.0", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-old-api"), withVersion("v2.3.4"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -200,7 +216,10 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now), fakeProvisionerDaemon(t, "pd-old", "v1.1.2", "1.0", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-ok"), withVersion("v1.2.3"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
fakeProvisionerDaemon(t, withName("pd-old"), withVersion("v1.1.2"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -241,7 +260,10 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProvisionerDaemonVersionMismatch,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemon(t, "pd-ok", "v1.2.3", "1.0", now), fakeProvisionerDaemon(t, "pd-new", "v2.3.4", "1.0", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-ok"), withVersion("v1.2.3"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
fakeProvisionerDaemon(t, withName("pd-new"), withVersion("v2.3.4"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -281,7 +303,10 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentVersion: "v2.3.4",
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityOK,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-stale", "v1.2.3", "0.9", now.Add(-5*time.Minute), now), fakeProvisionerDaemon(t, "pd-ok", "v2.3.4", "1.0", now)},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-stale"), withVersion("v1.2.3"), withAPIVersion("0.9"), withCreatedAt(oneHourAgo), withLastSeenAt(staleThreshold)),
fakeProvisionerDaemon(t, withName("pd-ok"), withVersion("v2.3.4"), withAPIVersion("1.0"), withCreatedAt(now), withLastSeenAt(now)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{
{
ProvisionerDaemon: codersdk.ProvisionerDaemon{
@@ -304,8 +329,10 @@ func TestProvisionerDaemonReport(t *testing.T) {
currentAPIMajorVersion: proto.CurrentMajor,
expectedSeverity: health.SeverityError,
expectedWarningCode: health.CodeProvisionerDaemonsNoProvisionerDaemons,
provisionerDaemons: []database.ProvisionerDaemon{fakeProvisionerDaemonStale(t, "pd-ok", "v1.2.3", "0.9", now.Add(-5*time.Minute), now)},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{},
provisionerDaemons: []database.ProvisionerDaemon{
fakeProvisionerDaemon(t, withName("pd-stale"), withVersion("v1.2.3"), withAPIVersion("0.9"), withCreatedAt(oneHourAgo), withLastSeenAt(staleThreshold)),
},
expectedItems: []healthsdk.ProvisionerDaemonsReportItem{},
},
} {
tt := tt
@@ -353,25 +380,52 @@ func TestProvisionerDaemonReport(t *testing.T) {
}
}
func fakeProvisionerDaemon(t *testing.T, name, version, apiVersion string, now time.Time) database.ProvisionerDaemon {
t.Helper()
return database.ProvisionerDaemon{
ID: uuid.Nil,
Name: name,
CreatedAt: now,
LastSeenAt: sql.NullTime{Time: now, Valid: true},
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform},
ReplicaID: uuid.NullUUID{},
Tags: map[string]string{},
Version: version,
APIVersion: apiVersion,
func withName(s string) func(*database.ProvisionerDaemon) {
return func(pd *database.ProvisionerDaemon) {
pd.Name = s
}
}
func fakeProvisionerDaemonStale(t *testing.T, name, version, apiVersion string, lastSeenAt, now time.Time) database.ProvisionerDaemon {
t.Helper()
d := fakeProvisionerDaemon(t, name, version, apiVersion, now)
d.LastSeenAt.Valid = true
d.LastSeenAt.Time = lastSeenAt
return d
func withCreatedAt(at time.Time) func(*database.ProvisionerDaemon) {
return func(pd *database.ProvisionerDaemon) {
pd.CreatedAt = at
}
}
func withLastSeenAt(at time.Time) func(*database.ProvisionerDaemon) {
return func(pd *database.ProvisionerDaemon) {
pd.LastSeenAt.Valid = true
pd.LastSeenAt.Time = at
}
}
func withVersion(v string) func(*database.ProvisionerDaemon) {
return func(pd *database.ProvisionerDaemon) {
pd.Version = v
}
}
func withAPIVersion(v string) func(*database.ProvisionerDaemon) {
return func(pd *database.ProvisionerDaemon) {
pd.APIVersion = v
}
}
func fakeProvisionerDaemon(t *testing.T, opts ...func(*database.ProvisionerDaemon)) database.ProvisionerDaemon {
t.Helper()
pd := database.ProvisionerDaemon{
ID: uuid.Nil,
Name: testutil.GetRandomName(t),
CreatedAt: time.Time{},
LastSeenAt: sql.NullTime{},
Provisioners: []database.ProvisionerType{database.ProvisionerTypeEcho, database.ProvisionerTypeTerraform},
ReplicaID: uuid.NullUUID{},
Tags: map[string]string{},
Version: "",
APIVersion: "",
}
for _, o := range opts {
o(&pd)
}
return pd
}
+1 -1
View File
@@ -10,10 +10,10 @@ import (
"time"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/websocket"
)
type WebsocketReport healthsdk.WebsocketReport
+1 -1
View File
@@ -268,7 +268,7 @@ const websocketCloseMaxLen = 123
func WebsocketCloseSprintf(format string, vars ...any) string {
msg := fmt.Sprintf(format, vars...)
// Cap msg length at 123 bytes. nhooyr/websocket only allows close messages
// Cap msg length at 123 bytes. coder/websocket only allows close messages
// of this length.
if len(msg) > websocketCloseMaxLen {
// Trim the string to 123 bytes. If we accidentally cut in the middle of
+1 -1
View File
@@ -6,9 +6,9 @@ import (
"time"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"cdr.dev/slog"
"github.com/coder/websocket"
)
// Heartbeat loops to ping a WebSocket to keep it alive.
+7 -24
View File
@@ -109,37 +109,20 @@ func ExtractWorkspaceAgentAndLatestBuild(opts ExtractWorkspaceAgentAndLatestBuil
return
}
//nolint:gocritic // System needs to be able to get owner roles.
roles, err := opts.DB.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), row.WorkspaceTable.OwnerID)
subject, _, err := UserRBACSubject(ctx, opts.DB, row.WorkspaceTable.OwnerID, rbac.WorkspaceAgentScope(rbac.WorkspaceAgentScopeParams{
WorkspaceID: row.WorkspaceTable.ID,
OwnerID: row.WorkspaceTable.OwnerID,
TemplateID: row.WorkspaceTable.TemplateID,
VersionID: row.WorkspaceBuild.TemplateVersionID,
}))
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error checking workspace agent authorization.",
Message: "Internal error with workspace agent authorization context.",
Detail: err.Error(),
})
return
}
roleNames, err := roles.RoleNames()
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal server error",
Detail: err.Error(),
})
return
}
subject := rbac.Subject{
ID: row.WorkspaceTable.OwnerID.String(),
Roles: rbac.RoleIdentifiers(roleNames),
Groups: roles.Groups,
Scope: rbac.WorkspaceAgentScope(rbac.WorkspaceAgentScopeParams{
WorkspaceID: row.WorkspaceTable.ID,
OwnerID: row.WorkspaceTable.OwnerID,
TemplateID: row.WorkspaceTable.TemplateID,
VersionID: row.WorkspaceBuild.TemplateVersionID,
}),
}.WithCachedASTValue()
ctx = context.WithValue(ctx, workspaceAgentContextKey{}, row.WorkspaceAgent)
ctx = context.WithValue(ctx, latestBuildContextKey{}, row.WorkspaceBuild)
// Also set the dbauthz actor for the request.
@@ -57,6 +57,10 @@ const (
// DefaultHeartbeatInterval is the interval at which the provisioner daemon
// will update its last seen at timestamp in the database.
DefaultHeartbeatInterval = time.Minute
// StaleInterval is the amount of time after the last heartbeat for which
// the provisioner will be reported as 'stale'.
StaleInterval = 90 * time.Second
)
type Options struct {
+5 -7
View File
@@ -12,17 +12,17 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/websocket"
)
// Returns provisioner logs based on query parameters.
@@ -312,6 +312,7 @@ type logFollower struct {
r *http.Request
rw http.ResponseWriter
conn *websocket.Conn
enc *wsjson.Encoder[codersdk.ProvisionerJobLog]
jobID uuid.UUID
after int64
@@ -391,6 +392,7 @@ func (f *logFollower) follow() {
}
defer f.conn.Close(websocket.StatusNormalClosure, "done")
go httpapi.Heartbeat(f.ctx, f.conn)
f.enc = wsjson.NewEncoder[codersdk.ProvisionerJobLog](f.conn, websocket.MessageText)
// query for logs once right away, so we can get historical data from before
// subscription
@@ -488,11 +490,7 @@ func (f *logFollower) query() error {
return xerrors.Errorf("error fetching logs: %w", err)
}
for _, log := range logs {
logB, err := json.Marshal(convertProvisionerJobLog(log))
if err != nil {
return xerrors.Errorf("error marshaling log: %w", err)
}
err = f.conn.Write(f.ctx, websocket.MessageText, logB)
err := f.enc.Encode(convertProvisionerJobLog(log))
if err != nil {
return xerrors.Errorf("error writing to websocket: %w", err)
}
+1 -1
View File
@@ -14,7 +14,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"nhooyr.io/websocket"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbmock"
@@ -23,6 +22,7 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
)
func TestConvertProvisionerJob_Unit(t *testing.T) {
+9 -5
View File
@@ -868,6 +868,9 @@ func ConvertTemplateVersion(version database.TemplateVersion) TemplateVersion {
if version.TemplateID.Valid {
snapVersion.TemplateID = &version.TemplateID.UUID
}
if version.SourceExampleID.Valid {
snapVersion.SourceExampleID = &version.SourceExampleID.String
}
return snapVersion
}
@@ -1116,11 +1119,12 @@ type Template struct {
}
type TemplateVersion struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
TemplateID *uuid.UUID `json:"template_id,omitempty"`
OrganizationID uuid.UUID `json:"organization_id"`
JobID uuid.UUID `json:"job_id"`
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
TemplateID *uuid.UUID `json:"template_id,omitempty"`
OrganizationID uuid.UUID `json:"organization_id"`
JobID uuid.UUID `json:"job_id"`
SourceExampleID *string `json:"source_example_id,omitempty"`
}
type ProvisionerJob struct {
+17 -1
View File
@@ -1,6 +1,7 @@
package telemetry_test
import (
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -48,6 +49,10 @@ func TestTelemetry(t *testing.T) {
_ = dbgen.Template(t, db, database.Template{
Provisioner: database.ProvisionerTypeTerraform,
})
sourceExampleID := uuid.NewString()
_ = dbgen.TemplateVersion(t, db, database.TemplateVersion{
SourceExampleID: sql.NullString{String: sourceExampleID, Valid: true},
})
_ = dbgen.TemplateVersion(t, db, database.TemplateVersion{})
user := dbgen.User(t, db, database.User{})
_ = dbgen.Workspace(t, db, database.WorkspaceTable{})
@@ -93,7 +98,7 @@ func TestTelemetry(t *testing.T) {
require.Len(t, snapshot.ProvisionerJobs, 1)
require.Len(t, snapshot.Licenses, 1)
require.Len(t, snapshot.Templates, 1)
require.Len(t, snapshot.TemplateVersions, 1)
require.Len(t, snapshot.TemplateVersions, 2)
require.Len(t, snapshot.Users, 1)
require.Len(t, snapshot.Groups, 2)
// 1 member in the everyone group + 1 member in the custom group
@@ -111,6 +116,17 @@ func TestTelemetry(t *testing.T) {
require.Len(t, wsa.Subsystems, 2)
require.Equal(t, string(database.WorkspaceAgentSubsystemEnvbox), wsa.Subsystems[0])
require.Equal(t, string(database.WorkspaceAgentSubsystemExectrace), wsa.Subsystems[1])
tvs := snapshot.TemplateVersions
sort.Slice(tvs, func(i, j int) bool {
// Sort by SourceExampleID presence (non-nil comes before nil)
if (tvs[i].SourceExampleID != nil) != (tvs[j].SourceExampleID != nil) {
return tvs[i].SourceExampleID != nil
}
return false
})
require.Equal(t, tvs[0].SourceExampleID, &sourceExampleID)
require.Nil(t, tvs[1].SourceExampleID)
})
t.Run("HashedEmail", func(t *testing.T) {
t.Parallel()
+164 -66
View File
@@ -10,7 +10,6 @@ import (
"fmt"
"net/http"
"os"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
@@ -22,6 +21,8 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
"github.com/coder/coder/v2/coderd/externalauth"
@@ -32,6 +33,7 @@ import (
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/render"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/examples"
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
@@ -60,6 +62,22 @@ func (api *API) templateVersion(rw http.ResponseWriter, r *http.Request) {
return
}
var matchedProvisioners *codersdk.MatchedProvisioners
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
// nolint: gocritic // The user hitting this endpoint may not have
// permission to read provisioner daemons, but we want to show them
// information about the provisioner daemons that are available.
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
WantTags: jobs[0].ProvisionerJob.Tags,
})
if err != nil {
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
} else {
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
}
}
schemas, err := api.Database.GetParameterSchemasByJobID(ctx, jobs[0].ProvisionerJob.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
@@ -77,7 +95,7 @@ func (api *API) templateVersion(rw http.ResponseWriter, r *http.Request) {
warnings = append(warnings, codersdk.TemplateVersionWarningUnsupportedWorkspaces)
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, warnings))
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, warnings))
}
// @Summary Patch template version by ID
@@ -173,7 +191,23 @@ func (api *API) patchTemplateVersion(rw http.ResponseWriter, r *http.Request) {
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
var matchedProvisioners *codersdk.MatchedProvisioners
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
// nolint: gocritic // The user hitting this endpoint may not have
// permission to read provisioner daemons, but we want to show them
// information about the provisioner daemons that are available.
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
WantTags: jobs[0].ProvisionerJob.Tags,
})
if err != nil {
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
} else {
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
}
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(updatedTemplateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
}
// @Summary Cancel template version by ID
@@ -546,6 +580,43 @@ func (api *API) templateVersionDryRun(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, convertProvisionerJob(job))
}
// @Summary Get template version dry-run matched provisioners
// @ID get-template-version-dry-run-matched-provisioners
// @Security CoderSessionToken
// @Produce json
// @Tags Templates
// @Param templateversion path string true "Template version ID" format(uuid)
// @Param jobID path string true "Job ID" format(uuid)
// @Success 200 {object} codersdk.MatchedProvisioners
// @Router /templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners [get]
func (api *API) templateVersionDryRunMatchedProvisioners(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
job, ok := api.fetchTemplateVersionDryRunJob(rw, r)
if !ok {
return
}
// nolint:gocritic // The user may not have permissions to read all
// provisioner daemons in the org.
daemons, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: job.ProvisionerJob.OrganizationID,
WantTags: job.ProvisionerJob.Tags,
})
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner daemons by organization.",
Detail: err.Error(),
})
return
}
daemons = []database.ProvisionerDaemon{}
}
matchedProvisioners := db2sdk.MatchedProvisioners(daemons, dbtime.Now(), provisionerdserver.StaleInterval)
httpapi.Write(ctx, rw, http.StatusOK, matchedProvisioners)
}
// @Summary Get template version dry-run resources by job ID
// @ID get-template-version-dry-run-resources-by-job-id
// @Security CoderSessionToken
@@ -814,7 +885,7 @@ func (api *API) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Reque
return err
}
apiVersions = append(apiVersions, convertTemplateVersion(version, convertProvisionerJob(job), codersdk.MatchedProvisioners{}, nil))
apiVersions = append(apiVersions, convertTemplateVersion(version, convertProvisionerJob(job), nil, nil))
}
return nil
@@ -868,8 +939,23 @@ func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
})
return
}
var matchedProvisioners *codersdk.MatchedProvisioners
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
// nolint: gocritic // The user hitting this endpoint may not have
// permission to read provisioner daemons, but we want to show them
// information about the provisioner daemons that are available.
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
WantTags: jobs[0].ProvisionerJob.Tags,
})
if err != nil {
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
} else {
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
}
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
}
// @Summary Get template version by organization, template, and name
@@ -934,7 +1020,23 @@ func (api *API) templateVersionByOrganizationTemplateAndName(rw http.ResponseWri
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
var matchedProvisioners *codersdk.MatchedProvisioners
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
// nolint: gocritic // The user hitting this endpoint may not have
// permission to read provisioner daemons, but we want to show them
// information about the provisioner daemons that are available.
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
WantTags: jobs[0].ProvisionerJob.Tags,
})
if err != nil {
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
} else {
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
}
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
}
// @Summary Get previous template version by organization, template, and name
@@ -1020,7 +1122,23 @@ func (api *API) previousTemplateVersionByOrganizationTemplateAndName(rw http.Res
return
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), codersdk.MatchedProvisioners{}, nil))
var matchedProvisioners *codersdk.MatchedProvisioners
if jobs[0].ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
// nolint: gocritic // The user hitting this endpoint may not have
// permission to read provisioner daemons, but we want to show them
// information about the provisioner daemons that are available.
provisioners, err := api.Database.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: jobs[0].ProvisionerJob.OrganizationID,
WantTags: jobs[0].ProvisionerJob.Tags,
})
if err != nil {
api.Logger.Error(ctx, "failed to fetch provisioners for job id", slog.F("job_id", jobs[0].ProvisionerJob.ID), slog.Error(err))
} else {
matchedProvisioners = ptr.Ref(db2sdk.MatchedProvisioners(provisioners, dbtime.Now(), provisionerdserver.StaleInterval))
}
}
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(previousTemplateVersion, convertProvisionerJob(jobs[0]), matchedProvisioners, nil))
}
// @Summary Archive template unused versions by template id
@@ -1479,11 +1597,9 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
}
// Ensure the "owner" tag is properly applied in addition to request tags and coder_workspace_tags.
// Tag order precedence:
// 1) User-specified tags in the request
// 2) Tags parsed from coder_workspace_tags data source in template file
// 2 may clobber 1.
tags := provisionersdk.MutateTags(apiKey.UserID, req.ProvisionerTags, parsedTags)
// User-specified tags in the request will take precedence over tags parsed from `coder_workspace_tags`
// data sources defined in the template file.
tags := provisionersdk.MutateTags(apiKey.UserID, parsedTags, req.ProvisionerTags)
var templateVersion database.TemplateVersion
var provisionerJob database.ProvisionerJob
@@ -1513,27 +1629,6 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
return err
}
// Check for eligible provisioners. This allows us to log a message warning deployment administrators
// of users submitting jobs for which no provisioners are available.
matchedProvisioners, err = checkProvisioners(ctx, tx, organization.ID, tags, api.DeploymentValues.Provisioner.DaemonPollInterval.Value())
if err != nil {
api.Logger.Error(ctx, "failed to check eligible provisioner daemons for job", slog.Error(err))
} else if matchedProvisioners.Count == 0 {
api.Logger.Warn(ctx, "no matching provisioners found for job",
slog.F("user_id", apiKey.UserID),
slog.F("job_id", jobID),
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
slog.F("tags", tags),
)
} else if matchedProvisioners.Available == 0 {
api.Logger.Warn(ctx, "no active provisioners found for job",
slog.F("user_id", apiKey.UserID),
slog.F("job_id", jobID),
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
slog.F("tags", tags),
)
}
provisionerJob, err = tx.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
ID: jobID,
CreatedAt: dbtime.Now(),
@@ -1559,6 +1654,36 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
return err
}
// Check for eligible provisioners. This allows us to return a warning to the user if they
// submit a job for which no provisioner is available.
// nolint: gocritic // The user hitting this endpoint may not have
// permission to read provisioner daemons, but we want to show them
// information about the provisioner daemons that are available.
eligibleProvisioners, err := tx.GetProvisionerDaemonsByOrganization(dbauthz.AsSystemReadProvisionerDaemons(ctx), database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: organization.ID,
WantTags: provisionerJob.Tags,
})
if err != nil {
// Log the error but do not return any warnings. This is purely advisory and we should not block.
api.Logger.Error(ctx, "failed to check eligible provisioner daemons for job", slog.Error(err))
}
matchedProvisioners = db2sdk.MatchedProvisioners(eligibleProvisioners, provisionerJob.CreatedAt, provisionerdserver.StaleInterval)
if matchedProvisioners.Count == 0 {
api.Logger.Warn(ctx, "no matching provisioners found for job",
slog.F("user_id", apiKey.UserID),
slog.F("job_id", jobID),
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
slog.F("tags", tags),
)
} else if matchedProvisioners.Available == 0 {
api.Logger.Warn(ctx, "no active provisioners found for job",
slog.F("user_id", apiKey.UserID),
slog.F("job_id", jobID),
slog.F("job_type", database.ProvisionerJobTypeTemplateVersionImport),
slog.F("tags", tags),
)
}
var templateID uuid.NullUUID
if req.TemplateID != uuid.Nil {
templateID = uuid.NullUUID{
@@ -1582,6 +1707,10 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
Readme: "",
JobID: provisionerJob.ID,
CreatedBy: apiKey.UserID,
SourceExampleID: sql.NullString{
String: req.ExampleID,
Valid: req.ExampleID != "",
},
})
if err != nil {
if database.IsUniqueViolation(err, database.UniqueTemplateVersionsTemplateIDNameKey) {
@@ -1629,7 +1758,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
ProvisionerJob: provisionerJob,
QueuePosition: 0,
}),
matchedProvisioners,
&matchedProvisioners,
warnings))
}
@@ -1697,7 +1826,7 @@ func (api *API) templateVersionLogs(rw http.ResponseWriter, r *http.Request) {
api.provisionerJobLogs(rw, r, job)
}
func convertTemplateVersion(version database.TemplateVersion, job codersdk.ProvisionerJob, matchedProvisioners codersdk.MatchedProvisioners, warnings []codersdk.TemplateVersionWarning) codersdk.TemplateVersion {
func convertTemplateVersion(version database.TemplateVersion, job codersdk.ProvisionerJob, matchedProvisioners *codersdk.MatchedProvisioners, warnings []codersdk.TemplateVersionWarning) codersdk.TemplateVersion {
return codersdk.TemplateVersion{
ID: version.ID,
TemplateID: &version.TemplateID.UUID,
@@ -1818,34 +1947,3 @@ func (api *API) publishTemplateUpdate(ctx context.Context, templateID uuid.UUID)
slog.F("template_id", templateID), slog.Error(err))
}
}
func checkProvisioners(ctx context.Context, store database.Store, orgID uuid.UUID, wantTags map[string]string, pollInterval time.Duration) (codersdk.MatchedProvisioners, error) {
// Check for eligible provisioners. This allows us to return a warning to the user if they
// submit a job for which no provisioner is available.
eligibleProvisioners, err := store.GetProvisionerDaemonsByOrganization(ctx, database.GetProvisionerDaemonsByOrganizationParams{
OrganizationID: orgID,
WantTags: wantTags,
})
if err != nil {
// Log the error but do not return any warnings. This is purely advisory and we should not block.
return codersdk.MatchedProvisioners{}, xerrors.Errorf("provisioner daemons by organization: %w", err)
}
threePollsAgo := time.Now().Add(-3 * pollInterval)
mostRecentlySeen := codersdk.NullTime{}
var matched codersdk.MatchedProvisioners
for _, provisioner := range eligibleProvisioners {
if !provisioner.LastSeenAt.Valid {
continue
}
matched.Count++
if provisioner.LastSeenAt.Time.After(threePollsAgo) {
matched.Available++
}
if provisioner.LastSeenAt.Time.After(mostRecentlySeen.Time) {
matched.MostRecentlySeen.Valid = true
matched.MostRecentlySeen.Time = provisioner.LastSeenAt.Time
}
}
return matched, nil
}
+270 -40
View File
@@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/rbac"
@@ -49,6 +50,12 @@ func TestTemplateVersion(t *testing.T) {
tv, err := client.TemplateVersion(ctx, version.ID)
authz.AssertChecked(t, policy.ActionRead, tv)
require.NoError(t, err)
if assert.Equal(t, tv.Job.Status, codersdk.ProvisionerJobPending) {
assert.NotNil(t, tv.MatchedProvisioners)
assert.Zero(t, tv.MatchedProvisioners.Available)
assert.Zero(t, tv.MatchedProvisioners.Count)
assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid)
}
assert.Equal(t, "bananas", tv.Name)
assert.Equal(t, "first try", tv.Message)
@@ -86,8 +93,14 @@ func TestTemplateVersion(t *testing.T) {
client1, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
_, err := client1.TemplateVersion(ctx, version.ID)
tv, err := client1.TemplateVersion(ctx, version.ID)
require.NoError(t, err)
if assert.Equal(t, tv.Job.Status, codersdk.ProvisionerJobPending) {
assert.NotNil(t, tv.MatchedProvisioners)
assert.Zero(t, tv.MatchedProvisioners.Available)
assert.Zero(t, tv.MatchedProvisioners.Count)
assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
}
@@ -134,7 +147,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
t.Run("WithParameters", func(t *testing.T) {
t.Parallel()
auditor := audit.NewMock()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
user := coderdtest.CreateFirstUser(t, client)
data, err := echo.Tar(&echo.Responses{
Parse: echo.ParseComplete,
@@ -157,14 +170,26 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "bananas", version.Name)
require.Equal(t, provisionersdk.ScopeOrganization, version.Job.Tags[provisionersdk.TagScope])
if assert.Equal(t, version.Job.Status, codersdk.ProvisionerJobPending) {
assert.NotNil(t, version.MatchedProvisioners)
assert.Equal(t, version.MatchedProvisioners.Available, 1)
assert.Equal(t, version.MatchedProvisioners.Count, 1)
assert.True(t, version.MatchedProvisioners.MostRecentlySeen.Valid)
}
require.Len(t, auditor.AuditLogs(), 2)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[1].Action)
admin, err := client.User(ctx, user.UserID.String())
require.NoError(t, err)
tvDB, err := db.GetTemplateVersionByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(admin, user.OrganizationID)), version.ID)
require.NoError(t, err)
require.False(t, tvDB.SourceExampleID.Valid)
})
t.Run("Example", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
client, db := coderdtest.NewWithDatabase(t, nil)
user := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -205,6 +230,12 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "my-example", tv.Name)
admin, err := client.User(ctx, user.UserID.String())
require.NoError(t, err)
tvDB, err := db.GetTemplateVersionByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(admin, user.OrganizationID)), tv.ID)
require.NoError(t, err)
require.Equal(t, ls[0].ID, tvDB.SourceExampleID.String)
// ensure the template tar was uploaded correctly
fl, ct, err := client.Download(ctx, tv.Job.FileID)
require.NoError(t, err)
@@ -262,6 +293,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
type = string
default = "2"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}`,
},
wantTags: map[string]string{"owner": "", "scope": "organization"},
@@ -270,18 +306,23 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
name: "main.tf with empty workspace tags",
files: map[string]string{
`main.tf`: `
variable "a" {
type = string
default = "1"
}
data "coder_parameter" "b" {
type = string
default = "2"
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {}
}`,
variable "a" {
type = string
default = "1"
}
data "coder_parameter" "b" {
type = string
default = "2"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {}
}`,
},
wantTags: map[string]string{"owner": "", "scope": "organization"},
},
@@ -297,6 +338,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
type = string
default = "2"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
@@ -309,29 +355,83 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
wantTags: map[string]string{"owner": "", "scope": "organization", "foo": "bar", "a": "1", "b": "2"},
},
{
name: "main.tf with workspace tags and request tags",
name: "main.tf with request tags not clobbering workspace tags",
files: map[string]string{
`main.tf`: `
variable "a" {
type = string
default = "1"
}
data "coder_parameter" "b" {
type = string
default = "2"
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
"foo": "bar",
"a": var.a,
"b": data.coder_parameter.b.value,
// This file is, once again, the same as the above, except
// for a slightly different comment.
variable "a" {
type = string
default = "1"
}
}`,
data "coder_parameter" "b" {
type = string
default = "2"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
"foo": "bar",
"a": var.a,
"b": data.coder_parameter.b.value,
}
}`,
},
reqTags: map[string]string{"baz": "zap", "foo": "noclobber"},
reqTags: map[string]string{"baz": "zap"},
wantTags: map[string]string{"owner": "", "scope": "organization", "foo": "bar", "baz": "zap", "a": "1", "b": "2"},
},
{
name: "main.tf with request tags clobbering workspace tags",
files: map[string]string{
`main.tf`: `
// This file is the same as the above, except for this comment.
variable "a" {
type = string
default = "1"
}
data "coder_parameter" "b" {
type = string
default = "2"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
"foo": "bar",
"a": var.a,
"b": data.coder_parameter.b.value,
}
}`,
},
reqTags: map[string]string{"baz": "zap", "foo": "clobbered"},
wantTags: map[string]string{"owner": "", "scope": "organization", "foo": "clobbered", "baz": "zap", "a": "1", "b": "2"},
},
// FIXME(cian): we should skip evaluating tags for which values have already been provided.
{
name: "main.tf with variable missing default value but value is passed in request",
files: map[string]string{
`main.tf`: `
variable "a" {
type = string
}
data "coder_workspace_tags" "tags" {
tags = {
"a": var.a,
}
}`,
},
reqTags: map[string]string{"a": "b"},
wantTags: map[string]string{"owner": "", "scope": "organization", "a": "b"},
},
{
name: "main.tf with disallowed workspace tag value",
files: map[string]string{
@@ -344,6 +444,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
type = string
default = "2"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {
name = "foo"
}
@@ -370,6 +475,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
type = string
default = "2"
}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {
name = "foo"
}
@@ -378,11 +488,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
"foo": "bar",
"a": var.a,
"b": data.coder_parameter.b.value,
"test": try(null_resource.test.name, "whatever"),
"test": pathexpand("~/file.txt"),
}
}`,
},
expectError: `Function calls not allowed; Functions may not be called here.`,
expectError: `function "pathexpand" may not be used here`,
},
// We will allow coder_workspace_tags to set the scope on a template version import job
// BUT the user ID will be ultimately determined by the API key in the scope.
@@ -392,6 +502,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
name: "main.tf with workspace tags that attempts to set user scope",
files: map[string]string{
`main.tf`: `
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
@@ -406,6 +521,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
name: "main.tf with workspace tags that attempt to clobber org ID",
files: map[string]string{
`main.tf`: `
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
@@ -420,6 +540,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
name: "main.tf with workspace tags that set scope=user",
files: map[string]string{
`main.tf`: `
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
resource "null_resource" "test" {}
data "coder_workspace_tags" "tags" {
tags = {
@@ -429,6 +554,55 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
},
wantTags: map[string]string{"owner": templateAdminUser.ID.String(), "scope": "user"},
},
// Ref: https://github.com/coder/coder/issues/16021
{
name: "main.tf with no workspace_tags and a function call in a parameter default",
files: map[string]string{
`main.tf`: `
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}`,
},
wantTags: map[string]string{"owner": "", "scope": "organization"},
},
{
name: "main.tf with tags from parameter with default value from variable no default",
files: map[string]string{
`main.tf`: `
variable "provisioner" {
type = string
}
variable "default_provisioner" {
type = string
default = "" # intentionally blank, set on template creation
}
data "coder_parameter" "provisioner" {
name = "provisioner"
mutable = false
default = var.default_provisioner
dynamic "option" {
for_each = toset(split(",", var.provisioner))
content {
name = option.value
value = option.value
}
}
}
data "coder_workspace_tags" "tags" {
tags = {
"provisioner" : data.coder_parameter.provisioner.value
}
}`,
},
reqTags: map[string]string{
"provisioner": "alpha",
},
wantTags: map[string]string{
"provisioner": "alpha", "owner": "", "scope": "organization",
},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
@@ -458,14 +632,13 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
pj, err := store.GetProvisionerJobByID(ctx, tv.Job.ID)
require.NoError(t, err)
require.EqualValues(t, tt.wantTags, pj.Tags)
// Also assert that we get the expected information back from the API endpoint
require.Zero(t, tv.MatchedProvisioners.Count)
require.Zero(t, tv.MatchedProvisioners.Available)
require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time)
} else {
require.ErrorContains(t, err, tt.expectError)
}
// Also assert that we get the expected information back from the API endpoint
require.Zero(t, tv.MatchedProvisioners.Count)
require.Zero(t, tv.MatchedProvisioners.Available)
require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time)
})
}
})
@@ -778,8 +951,15 @@ func TestTemplateVersionByName(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.TemplateVersionByName(ctx, template.ID, version.Name)
tv, err := client.TemplateVersionByName(ctx, template.ID, version.Name)
require.NoError(t, err)
if assert.Equal(t, tv.Job.Status, codersdk.ProvisionerJobPending) {
assert.NotNil(t, tv.MatchedProvisioners)
assert.Zero(t, tv.MatchedProvisioners.Available)
assert.Zero(t, tv.MatchedProvisioners.Count)
assert.False(t, tv.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
}
@@ -967,6 +1147,13 @@ func TestTemplateVersionDryRun(t *testing.T) {
require.NoError(t, err)
require.Equal(t, job.ID, newJob.ID)
// Check matched provisioners
matched, err := client.TemplateVersionDryRunMatchedProvisioners(ctx, version.ID, job.ID)
require.NoError(t, err)
require.Equal(t, 1, matched.Count)
require.Equal(t, 1, matched.Available)
require.NotZero(t, matched.MostRecentlySeen.Time)
// Stream logs
logs, closer, err := client.TemplateVersionDryRunLogsAfter(ctx, version.ID, job.ID, 0)
require.NoError(t, err)
@@ -1139,6 +1326,49 @@ func TestTemplateVersionDryRun(t *testing.T) {
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})
})
t.Run("Pending", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
client, closer := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
IncludeProvisionerDaemon: true,
})
defer closer.Close()
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete,
})
version = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
require.Equal(t, codersdk.ProvisionerJobSucceeded, version.Job.Status)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
ctx := testutil.Context(t, testutil.WaitShort)
_, err := db.Exec("DELETE FROM provisioner_daemons")
require.NoError(t, err)
job, err := templateAdmin.CreateTemplateVersionDryRun(ctx, version.ID, codersdk.CreateTemplateVersionDryRunRequest{
WorkspaceName: "test",
RichParameterValues: []codersdk.WorkspaceBuildParameter{},
UserVariableValues: []codersdk.VariableValue{},
})
require.NoError(t, err)
require.Equal(t, codersdk.ProvisionerJobPending, job.Status)
matched, err := templateAdmin.TemplateVersionDryRunMatchedProvisioners(ctx, version.ID, job.ID)
require.NoError(t, err)
require.Equal(t, 0, matched.Count)
require.Equal(t, 0, matched.Available)
require.Zero(t, matched.MostRecentlySeen.Time)
})
}
// TestPaginatedTemplateVersions creates a list of template versions and paginate.
+9 -19
View File
@@ -20,7 +20,6 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"tailscale.com/tailcfg"
"cdr.dev/slog"
@@ -39,8 +38,10 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/websocket"
)
// @Summary Get workspace agent by ID
@@ -377,7 +378,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
// Allow client to request no compression. This is useful for buggy
// clients or if there's a client/server incompatibility. This is
// needed with e.g. nhooyr/websocket and Safari (confirmed in 16.5).
// needed with e.g. coder/websocket and Safari (confirmed in 16.5).
//
// See:
// * https://github.com/nhooyr/websocket/issues/218
@@ -396,11 +397,9 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) {
}
go httpapi.Heartbeat(ctx, conn)
ctx, wsNetConn := codersdk.WebsocketNetConn(ctx, conn, websocket.MessageText)
defer wsNetConn.Close() // Also closes conn.
encoder := wsjson.NewEncoder[[]codersdk.WorkspaceAgentLog](conn, websocket.MessageText)
defer encoder.Close(websocket.StatusNormalClosure)
// The Go stdlib JSON encoder appends a newline character after message write.
encoder := json.NewEncoder(wsNetConn)
err = encoder.Encode(convertWorkspaceAgentLogs(logs))
if err != nil {
return
@@ -740,16 +739,8 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
})
return
}
ctx, nconn := codersdk.WebsocketNetConn(ctx, ws, websocket.MessageBinary)
defer nconn.Close()
// Slurp all packets from the connection into io.Discard so pongs get sent
// by the websocket package. We don't do any reads ourselves so this is
// necessary.
go func() {
_, _ = io.Copy(io.Discard, nconn)
_ = nconn.Close()
}()
encoder := wsjson.NewEncoder[*tailcfg.DERPMap](ws, websocket.MessageBinary)
defer encoder.Close(websocket.StatusGoingAway)
go func(ctx context.Context) {
// TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout?
@@ -767,7 +758,7 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
err := ws.Ping(ctx)
cancel()
if err != nil {
_ = nconn.Close()
_ = ws.Close(websocket.StatusGoingAway, "ping failed")
return
}
}
@@ -780,9 +771,8 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) {
for {
derpMap := api.DERPMap()
if lastDERPMap == nil || !tailnet.CompareDERPMaps(lastDERPMap, derpMap) {
err := json.NewEncoder(nconn).Encode(derpMap)
err := encoder.Encode(derpMap)
if err != nil {
_ = nconn.Close()
return
}
lastDERPMap = derpMap
+1 -1
View File
@@ -20,7 +20,6 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"google.golang.org/protobuf/types/known/timestamppb"
"nhooyr.io/websocket"
"tailscale.com/tailcfg"
"cdr.dev/slog"
@@ -50,6 +49,7 @@ import (
"github.com/coder/coder/v2/tailnet/tailnettest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
"github.com/coder/websocket"
)
func TestWorkspaceAgent(t *testing.T) {
+1 -1
View File
@@ -14,7 +14,6 @@ import (
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/proto"
@@ -30,6 +29,7 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/tailnet"
tailnetproto "github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/websocket"
)
// @Summary Workspace agent RPC API
+3 -4
View File
@@ -8,18 +8,17 @@ import (
"testing"
"time"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/wspubsub"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"nhooyr.io/websocket"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/wspubsub"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
)
func TestAgentConnectionMonitor_ContextCancel(t *testing.T) {
+1 -1
View File
@@ -17,7 +17,6 @@ import (
"github.com/go-jose/go-jose/v4/jwt"
"github.com/google/uuid"
"go.opentelemetry.io/otel/trace"
"nhooyr.io/websocket"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentssh"
@@ -32,6 +31,7 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/coder/v2/site"
"github.com/coder/websocket"
)
const (
+61 -27
View File
@@ -27,6 +27,7 @@ import (
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/wsbuilder"
@@ -85,6 +86,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
data.scripts,
data.logSources,
data.templateVersions[0],
nil,
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -200,6 +202,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
data.scripts,
data.logSources,
data.templateVersions,
data.provisionerDaemons,
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -289,6 +292,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
data.scripts,
data.logSources,
data.templateVersions[0],
data.provisionerDaemons,
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -352,7 +356,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
builder = builder.State(createBuild.ProvisionerState)
}
workspaceBuild, provisionerJob, err := builder.Build(
workspaceBuild, provisionerJob, provisionerDaemons, err := builder.Build(
ctx,
api.Database,
func(action policy.Action, object rbac.Objecter) bool {
@@ -384,10 +388,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
})
return
}
err = provisionerjobs.PostJob(api.Pubsub, *provisionerJob)
if err != nil {
// Client probably doesn't care about this error, so just log it.
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
if provisionerJob != nil {
if err := provisionerjobs.PostJob(api.Pubsub, *provisionerJob); err != nil {
// Client probably doesn't care about this error, so just log it.
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
}
}
apiBuild, err := api.convertWorkspaceBuild(
@@ -404,6 +410,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
[]database.WorkspaceAgentScript{},
[]database.WorkspaceAgentLogSource{},
database.TemplateVersion{},
provisionerDaemons,
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -638,14 +645,15 @@ func (api *API) workspaceBuildTimings(rw http.ResponseWriter, r *http.Request) {
}
type workspaceBuildsData struct {
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
templateVersions []database.TemplateVersion
resources []database.WorkspaceResource
metadata []database.WorkspaceResourceMetadatum
agents []database.WorkspaceAgent
apps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
logSources []database.WorkspaceAgentLogSource
jobs []database.GetProvisionerJobsByIDsWithQueuePositionRow
templateVersions []database.TemplateVersion
resources []database.WorkspaceResource
metadata []database.WorkspaceResourceMetadatum
agents []database.WorkspaceAgent
apps []database.WorkspaceApp
scripts []database.WorkspaceAgentScript
logSources []database.WorkspaceAgentLogSource
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
}
func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) {
@@ -657,6 +665,17 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("get provisioner jobs: %w", err)
}
pendingJobIDs := []uuid.UUID{}
for _, job := range jobs {
if job.ProvisionerJob.JobStatus == database.ProvisionerJobStatusPending {
pendingJobIDs = append(pendingJobIDs, job.ProvisionerJob.ID)
}
}
pendingJobProvisioners, err := api.Database.GetEligibleProvisionerDaemonsByProvisionerJobIDs(ctx, pendingJobIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("get provisioner daemons: %w", err)
}
templateVersionIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
for _, build := range workspaceBuilds {
@@ -677,8 +696,9 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
if len(resources) == 0 {
return workspaceBuildsData{
jobs: jobs,
templateVersions: templateVersions,
jobs: jobs,
templateVersions: templateVersions,
provisionerDaemons: pendingJobProvisioners,
}, nil
}
@@ -701,10 +721,11 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
if len(resources) == 0 {
return workspaceBuildsData{
jobs: jobs,
templateVersions: templateVersions,
resources: resources,
metadata: metadata,
jobs: jobs,
templateVersions: templateVersions,
resources: resources,
metadata: metadata,
provisionerDaemons: pendingJobProvisioners,
}, nil
}
@@ -741,14 +762,15 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab
}
return workspaceBuildsData{
jobs: jobs,
templateVersions: templateVersions,
resources: resources,
metadata: metadata,
agents: agents,
apps: apps,
scripts: scripts,
logSources: logSources,
jobs: jobs,
templateVersions: templateVersions,
resources: resources,
metadata: metadata,
agents: agents,
apps: apps,
scripts: scripts,
logSources: logSources,
provisionerDaemons: pendingJobProvisioners,
}, nil
}
@@ -763,6 +785,7 @@ func (api *API) convertWorkspaceBuilds(
agentScripts []database.WorkspaceAgentScript,
agentLogSources []database.WorkspaceAgentLogSource,
templateVersions []database.TemplateVersion,
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
) ([]codersdk.WorkspaceBuild, error) {
workspaceByID := map[uuid.UUID]database.Workspace{}
for _, workspace := range workspaces {
@@ -804,6 +827,7 @@ func (api *API) convertWorkspaceBuilds(
agentScripts,
agentLogSources,
templateVersion,
provisionerDaemons,
)
if err != nil {
return nil, xerrors.Errorf("converting workspace build: %w", err)
@@ -826,6 +850,7 @@ func (api *API) convertWorkspaceBuild(
agentScripts []database.WorkspaceAgentScript,
agentLogSources []database.WorkspaceAgentLogSource,
templateVersion database.TemplateVersion,
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow,
) (codersdk.WorkspaceBuild, error) {
resourcesByJobID := map[uuid.UUID][]database.WorkspaceResource{}
for _, resource := range workspaceResources {
@@ -851,6 +876,14 @@ func (api *API) convertWorkspaceBuild(
for _, logSource := range agentLogSources {
logSourcesByAgentID[logSource.WorkspaceAgentID] = append(logSourcesByAgentID[logSource.WorkspaceAgentID], logSource)
}
provisionerDaemonsForThisWorkspaceBuild := []database.ProvisionerDaemon{}
for _, provisionerDaemon := range provisionerDaemons {
if provisionerDaemon.JobID != job.ProvisionerJob.ID {
continue
}
provisionerDaemonsForThisWorkspaceBuild = append(provisionerDaemonsForThisWorkspaceBuild, provisionerDaemon.ProvisionerDaemon)
}
matchedProvisioners := db2sdk.MatchedProvisioners(provisionerDaemonsForThisWorkspaceBuild, job.ProvisionerJob.CreatedAt, provisionerdserver.StaleInterval)
resources := resourcesByJobID[job.ProvisionerJob.ID]
apiResources := make([]codersdk.WorkspaceResource, 0)
@@ -918,6 +951,7 @@ func (api *API) convertWorkspaceBuild(
Resources: apiResources,
Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition),
DailyCost: build.DailyCost,
MatchedProvisioners: &matchedProvisioners,
}, nil
}
+170 -2
View File
@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"cdr.dev/slog"
@@ -1097,6 +1098,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
Transition: codersdk.WorkspaceTransitionStart,
})
require.NoError(t, err)
if assert.NotNil(t, build.MatchedProvisioners) {
require.Equal(t, 1, build.MatchedProvisioners.Count)
require.Equal(t, 1, build.MatchedProvisioners.Available)
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
}
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
require.Eventually(t, func() bool {
@@ -1124,6 +1131,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
Transition: codersdk.WorkspaceTransitionStart,
})
require.NoError(t, err)
if assert.NotNil(t, build.MatchedProvisioners) {
require.Equal(t, 1, build.MatchedProvisioners.Count)
require.Equal(t, 1, build.MatchedProvisioners.Available)
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
}
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
})
@@ -1150,6 +1163,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
ProvisionerState: wantState,
})
require.NoError(t, err)
if assert.NotNil(t, build.MatchedProvisioners) {
require.Equal(t, 1, build.MatchedProvisioners.Count)
require.Equal(t, 1, build.MatchedProvisioners.Available)
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
}
gotState, err := client.WorkspaceBuildState(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, wantState, gotState)
@@ -1173,6 +1192,12 @@ func TestPostWorkspaceBuild(t *testing.T) {
})
require.NoError(t, err)
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
if assert.NotNil(t, build.MatchedProvisioners) {
require.Equal(t, 1, build.MatchedProvisioners.Count)
require.Equal(t, 1, build.MatchedProvisioners.Available)
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
}
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
@@ -1181,6 +1206,102 @@ func TestPostWorkspaceBuild(t *testing.T) {
require.NoError(t, err)
require.Len(t, res.Workspaces, 0)
})
t.Run("NoProvisionersAvailable", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
// Given: a coderd instance with a provisioner daemon
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
IncludeProvisionerDaemon: true,
})
defer closeDaemon.Close()
// Given: a user, template, and workspace
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the provisioner daemon.
require.NoError(t, closeDaemon.Close())
ctx := testutil.Context(t, testutil.WaitLong)
// Given: no provisioner daemons exist.
_, err := db.ExecContext(ctx, `DELETE FROM provisioner_daemons;`)
require.NoError(t, err)
// When: a new workspace build is created
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
})
// Then: the request should succeed.
require.NoError(t, err)
// Then: the provisioner job should remain pending.
require.Equal(t, codersdk.ProvisionerJobPending, build.Job.Status)
// Then: the response should indicate no provisioners are available.
if assert.NotNil(t, build.MatchedProvisioners) {
assert.Zero(t, build.MatchedProvisioners.Count)
assert.Zero(t, build.MatchedProvisioners.Available)
assert.Zero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
assert.False(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
t.Run("AllProvisionersStale", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
// Given: a coderd instance with a provisioner daemon
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
IncludeProvisionerDaemon: true,
})
defer closeDaemon.Close()
// Given: a user, template, and workspace
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
ctx := testutil.Context(t, testutil.WaitLong)
// Given: all provisioner daemons are stale
// First stop the provisioner
require.NoError(t, closeDaemon.Close())
newLastSeenAt := dbtime.Now().Add(-time.Hour)
// Update the last seen at for all provisioner daemons. We have to use the
// SQL db directly because store.UpdateProvisionerDaemonLastSeenAt has a
// built-in check to prevent updating the last seen at to a time in the past.
_, err := db.ExecContext(ctx, `UPDATE provisioner_daemons SET last_seen_at = $1;`, newLastSeenAt)
require.NoError(t, err)
// When: a new workspace build is created
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
})
// Then: the request should succeed
require.NoError(t, err)
// Then: the provisioner job should remain pending
require.Equal(t, codersdk.ProvisionerJobPending, build.Job.Status)
// Then: the response should indicate no provisioners are available
if assert.NotNil(t, build.MatchedProvisioners) {
assert.Zero(t, build.MatchedProvisioners.Available)
assert.Equal(t, 1, build.MatchedProvisioners.Count)
assert.Equal(t, newLastSeenAt.UTC(), build.MatchedProvisioners.MostRecentlySeen.Time.UTC())
assert.True(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
}
func TestWorkspaceBuildTimings(t *testing.T) {
@@ -1301,6 +1422,47 @@ func TestWorkspaceBuildTimings(t *testing.T) {
}
})
t.Run("MultipleTimingsForSameAgentScript", func(t *testing.T) {
t.Parallel()
// Given: a build with multiple timings for the same script
build := makeBuild(t)
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
JobID: build.JobID,
})
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
script := dbgen.WorkspaceAgentScript(t, db, database.WorkspaceAgentScript{
WorkspaceAgentID: agent.ID,
})
timings := make([]database.WorkspaceAgentScriptTiming, 3)
scriptStartedAt := dbtime.Now()
for i := range timings {
timings[i] = dbgen.WorkspaceAgentScriptTiming(t, db, database.WorkspaceAgentScriptTiming{
StartedAt: scriptStartedAt,
EndedAt: scriptStartedAt.Add(1 * time.Minute),
ScriptID: script.ID,
})
// Add an hour to the previous "started at" so we can
// reliably differentiate the scripts from each other.
scriptStartedAt = scriptStartedAt.Add(1 * time.Hour)
}
// When: fetching timings for the build
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
t.Cleanup(cancel)
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
require.NoError(t, err)
// Then: return a response with the first agent script timing
require.Len(t, res.AgentScriptTimings, 1)
require.Equal(t, timings[0].StartedAt.UnixMilli(), res.AgentScriptTimings[0].StartedAt.UnixMilli())
require.Equal(t, timings[0].EndedAt.UnixMilli(), res.AgentScriptTimings[0].EndedAt.UnixMilli())
})
t.Run("AgentScriptTimings", func(t *testing.T) {
t.Parallel()
@@ -1312,10 +1474,10 @@ func TestWorkspaceBuildTimings(t *testing.T) {
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
script := dbgen.WorkspaceAgentScript(t, db, database.WorkspaceAgentScript{
scripts := dbgen.WorkspaceAgentScripts(t, db, 5, database.WorkspaceAgentScript{
WorkspaceAgentID: agent.ID,
})
agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5)
agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, scripts)
// When: fetching timings for the build
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -1325,6 +1487,12 @@ func TestWorkspaceBuildTimings(t *testing.T) {
// Then: return a response with the expected timings
require.Len(t, res.AgentScriptTimings, 5)
slices.SortFunc(res.AgentScriptTimings, func(a, b codersdk.AgentScriptTiming) int {
return a.StartedAt.Compare(b.StartedAt)
})
slices.SortFunc(agentScriptTimings, func(a, b database.WorkspaceAgentScriptTiming) int {
return a.StartedAt.Compare(b.StartedAt)
})
for i := range res.AgentScriptTimings {
timingRes := res.AgentScriptTimings[i]
genTiming := agentScriptTimings[i]
+9 -5
View File
@@ -593,8 +593,7 @@ func createWorkspace(
}},
})
return
}
if err != nil && !errors.Is(err, sql.ErrNoRows) {
} else if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: fmt.Sprintf("Internal error fetching workspace by name %q.", req.Name),
Detail: err.Error(),
@@ -603,8 +602,9 @@ func createWorkspace(
}
var (
provisionerJob *database.ProvisionerJob
workspaceBuild *database.WorkspaceBuild
provisionerJob *database.ProvisionerJob
workspaceBuild *database.WorkspaceBuild
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
)
err = api.Database.InTx(func(db database.Store) error {
now := dbtime.Now()
@@ -645,7 +645,7 @@ func createWorkspace(
builder = builder.VersionID(req.TemplateVersionID)
}
workspaceBuild, provisionerJob, err = builder.Build(
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(
ctx,
db,
func(action policy.Action, object rbac.Objecter) bool {
@@ -655,6 +655,7 @@ func createWorkspace(
)
return err
}, nil)
var bldErr wsbuilder.BuildError
if xerrors.As(err, &bldErr) {
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
@@ -675,6 +676,7 @@ func createWorkspace(
// Client probably doesn't care about this error, so just log it.
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
}
auditReq.New = workspace.WorkspaceTable()
api.Telemetry.Report(&telemetry.Snapshot{
@@ -696,6 +698,7 @@ func createWorkspace(
[]database.WorkspaceAgentScript{},
[]database.WorkspaceAgentLogSource{},
database.TemplateVersion{},
provisionerDaemons,
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
@@ -1816,6 +1819,7 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa
data.scripts,
data.logSources,
data.templateVersions,
data.provisionerDaemons,
)
if err != nil {
return workspaceData{}, xerrors.Errorf("convert workspace builds: %w", err)
+90 -2
View File
@@ -766,6 +766,94 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, exp, *ws.TTLMillis)
})
t.Run("NoProvisionersAvailable", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
// Given: a coderd instance with a provisioner daemon
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
IncludeProvisionerDaemon: true,
})
defer closeDaemon.Close()
// Given: a user, template, and workspace
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
// Given: all the provisioner daemons disappear
ctx := testutil.Context(t, testutil.WaitLong)
_, err := db.ExecContext(ctx, `DELETE FROM provisioner_daemons;`)
require.NoError(t, err)
// When: a new workspace is created
ws, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
})
// Then: the request succeeds
require.NoError(t, err)
// Then: the workspace build is pending
require.Equal(t, codersdk.ProvisionerJobPending, ws.LatestBuild.Job.Status)
// Then: the workspace build has no matched provisioners
if assert.NotNil(t, ws.LatestBuild.MatchedProvisioners) {
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Count)
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Available)
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Time)
assert.False(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
t.Run("AllProvisionersStale", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
// Given: a coderd instance with a provisioner daemon
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
IncludeProvisionerDaemon: true,
})
defer closeDaemon.Close()
// Given: a user, template, and workspace
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
// Given: all the provisioner daemons have not been seen for a while
ctx := testutil.Context(t, testutil.WaitLong)
newLastSeenAt := dbtime.Now().Add(-time.Hour)
_, err := db.ExecContext(ctx, `UPDATE provisioner_daemons SET last_seen_at = $1;`, newLastSeenAt)
require.NoError(t, err)
// When: a new workspace is created
ws, err := client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
})
// Then: the request succeeds
require.NoError(t, err)
// Then: the workspace build is pending
require.Equal(t, codersdk.ProvisionerJobPending, ws.LatestBuild.Job.Status)
// Then: we can see that there are some provisioners that are stale
if assert.NotNil(t, ws.LatestBuild.MatchedProvisioners) {
assert.Equal(t, 1, ws.LatestBuild.MatchedProvisioners.Count)
assert.Zero(t, ws.LatestBuild.MatchedProvisioners.Available)
assert.Equal(t, newLastSeenAt.UTC(), ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Time.UTC())
assert.True(t, ws.LatestBuild.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
}
func TestWorkspaceByOwnerAndName(t *testing.T) {
@@ -3669,10 +3757,10 @@ func TestWorkspaceTimings(t *testing.T) {
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
ResourceID: resource.ID,
})
script := dbgen.WorkspaceAgentScript(t, db, database.WorkspaceAgentScript{
scripts := dbgen.WorkspaceAgentScripts(t, db, 3, database.WorkspaceAgentScript{
WorkspaceAgentID: agent.ID,
})
dbgen.WorkspaceAgentScriptTimings(t, db, script, 3)
dbgen.WorkspaceAgentScriptTimings(t, db, scripts)
// When: fetching the timings
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
+79 -63
View File
@@ -12,9 +12,9 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
"github.com/coder/coder/v2/provisionersdk"
"github.com/google/uuid"
@@ -24,6 +24,7 @@ import (
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/provisionerdserver"
@@ -63,6 +64,7 @@ type Builder struct {
templateVersion *database.TemplateVersion
templateVersionJob *database.ProvisionerJob
templateVersionParameters *[]database.TemplateVersionParameter
templateVersionVariables *[]database.TemplateVersionVariable
templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag
lastBuild *database.WorkspaceBuild
lastBuildErr *error
@@ -213,12 +215,12 @@ func (b *Builder) Build(
authFunc func(action policy.Action, object rbac.Objecter) bool,
auditBaggage audit.WorkspaceBuildBaggage,
) (
*database.WorkspaceBuild, *database.ProvisionerJob, error,
*database.WorkspaceBuild, *database.ProvisionerJob, []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error,
) {
var err error
b.ctx, err = audit.BaggageToContext(ctx, auditBaggage)
if err != nil {
return nil, nil, xerrors.Errorf("create audit baggage: %w", err)
return nil, nil, nil, xerrors.Errorf("create audit baggage: %w", err)
}
// Run the build in a transaction with RepeatableRead isolation, and retries.
@@ -227,16 +229,17 @@ func (b *Builder) Build(
// later reads are consistent with earlier ones.
var workspaceBuild *database.WorkspaceBuild
var provisionerJob *database.ProvisionerJob
var provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
err = database.ReadModifyUpdate(store, func(tx database.Store) error {
var err error
b.store = tx
workspaceBuild, provisionerJob, err = b.buildTx(authFunc)
workspaceBuild, provisionerJob, provisionerDaemons, err = b.buildTx(authFunc)
return err
})
if err != nil {
return nil, nil, xerrors.Errorf("build tx: %w", err)
return nil, nil, nil, xerrors.Errorf("build tx: %w", err)
}
return workspaceBuild, provisionerJob, nil
return workspaceBuild, provisionerJob, provisionerDaemons, nil
}
// buildTx contains the business logic of computing a new build. Attributes of the new database objects are computed
@@ -246,35 +249,35 @@ func (b *Builder) Build(
//
// In order to utilize this cache, the functions that compute build attributes use a pointer receiver type.
func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Objecter) bool) (
*database.WorkspaceBuild, *database.ProvisionerJob, error,
*database.WorkspaceBuild, *database.ProvisionerJob, []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow, error,
) {
if authFunc != nil {
err := b.authorize(authFunc)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
}
err := b.checkTemplateVersionMatchesTemplate()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
err = b.checkTemplateJobStatus()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
err = b.checkRunningBuild()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
template, err := b.getTemplate()
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "failed to fetch template", err}
return nil, nil, nil, BuildError{http.StatusInternalServerError, "failed to fetch template", err}
}
templateVersionJob, err := b.getTemplateVersionJob()
if err != nil {
return nil, nil, BuildError{
return nil, nil, nil, BuildError{
http.StatusInternalServerError, "failed to fetch template version job", err,
}
}
@@ -294,7 +297,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
LogLevel: b.logLevel,
})
if err != nil {
return nil, nil, BuildError{
return nil, nil, nil, BuildError{
http.StatusInternalServerError,
"marshal provision job",
err,
@@ -302,12 +305,12 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
}
traceMetadataRaw, err := json.Marshal(tracing.MetadataFromContext(b.ctx))
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "marshal metadata", err}
return nil, nil, nil, BuildError{http.StatusInternalServerError, "marshal metadata", err}
}
tags, err := b.getProvisionerTags()
if err != nil {
return nil, nil, err // already wrapped BuildError
return nil, nil, nil, err // already wrapped BuildError
}
now := dbtime.Now()
@@ -329,20 +332,32 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
},
})
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "insert provisioner job", err}
return nil, nil, nil, BuildError{http.StatusInternalServerError, "insert provisioner job", err}
}
// nolint:gocritic // The user performing this request may not have permission
// to read all provisioner daemons. We need to retrieve the eligible
// provisioner daemons for this job to show in the UI if there is no
// matching provisioner daemon.
provisionerDaemons, err := b.store.GetEligibleProvisionerDaemonsByProvisionerJobIDs(dbauthz.AsSystemReadProvisionerDaemons(b.ctx), []uuid.UUID{provisionerJob.ID})
if err != nil {
// NOTE: we do **not** want to fail a workspace build if we fail to
// retrieve provisioner daemons. This is just to show in the UI if there
// is no matching provisioner daemon for the job.
provisionerDaemons = []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}
}
templateVersionID, err := b.getTemplateVersionID()
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "compute template version ID", err}
return nil, nil, nil, BuildError{http.StatusInternalServerError, "compute template version ID", err}
}
buildNum, err := b.getBuildNumber()
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "compute build number", err}
return nil, nil, nil, BuildError{http.StatusInternalServerError, "compute build number", err}
}
state, err := b.getState()
if err != nil {
return nil, nil, BuildError{http.StatusInternalServerError, "compute build state", err}
return nil, nil, nil, BuildError{http.StatusInternalServerError, "compute build state", err}
}
var workspaceBuild database.WorkspaceBuild
@@ -393,10 +408,10 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
return nil
}, nil)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
return &workspaceBuild, &provisionerJob, nil
return &workspaceBuild, &provisionerJob, provisionerDaemons, nil
}
func (b *Builder) getTemplate() (*database.Template, error) {
@@ -603,6 +618,22 @@ func (b *Builder) getTemplateVersionParameters() ([]database.TemplateVersionPara
return tvp, nil
}
func (b *Builder) getTemplateVersionVariables() ([]database.TemplateVersionVariable, error) {
if b.templateVersionVariables != nil {
return *b.templateVersionVariables, nil
}
tvID, err := b.getTemplateVersionID()
if err != nil {
return nil, xerrors.Errorf("get template version ID to get variables: %w", err)
}
tvs, err := b.store.GetTemplateVersionVariables(b.ctx, tvID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get template version %s variables: %w", tvID, err)
}
b.templateVersionVariables = &tvs
return tvs, nil
}
// verifyNoLegacyParameters verifies that initiator can't start the workspace build
// if it uses legacy parameters (database.ParameterSchemas).
func (b *Builder) verifyNoLegacyParameters() error {
@@ -664,17 +695,40 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
tags[name] = value
}
// Step 2: Mutate workspace tags
// Step 2: Mutate workspace tags:
// - Get workspace tags from the template version job
// - Get template version variables from the template version as they can be
// referenced in workspace tags
// - Get parameters from the workspace build as they can also be referenced
// in workspace tags
// - Evaluate workspace tags given the above inputs
workspaceTags, err := b.getTemplateVersionWorkspaceTags()
if err != nil {
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version workspace tags", err}
}
tvs, err := b.getTemplateVersionVariables()
if err != nil {
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version variables", err}
}
varsM := make(map[string]string)
for _, tv := range tvs {
// FIXME: do this in Terraform? This is a bit of a hack.
if tv.Value == "" {
varsM[tv.Name] = tv.DefaultValue
} else {
varsM[tv.Name] = tv.Value
}
}
parameterNames, parameterValues, err := b.getParameters()
if err != nil {
return nil, err // already wrapped BuildError
}
paramsM := make(map[string]string)
for i, name := range parameterNames {
paramsM[name] = parameterValues[i]
}
evalCtx := buildParametersEvalContext(parameterNames, parameterValues)
evalCtx := tfparse.BuildEvalContext(varsM, paramsM)
for _, workspaceTag := range workspaceTags {
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTag.Value), "expression.hcl", hcl.InitialPos)
if diags.HasErrors() {
@@ -687,7 +741,7 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
}
// Do not use "val.AsString()" as it can panic
str, err := ctyValueString(val)
str, err := tfparse.CtyValueString(val)
if err != nil {
return nil, BuildError{http.StatusBadRequest, "failed to marshal cty.Value as string", err}
}
@@ -696,44 +750,6 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
return tags, nil
}
func buildParametersEvalContext(names, values []string) *hcl.EvalContext {
m := map[string]cty.Value{}
for i, name := range names {
m[name] = cty.MapVal(map[string]cty.Value{
"value": cty.StringVal(values[i]),
})
}
if len(m) == 0 {
return nil // otherwise, panic: must not call MapVal with empty map
}
return &hcl.EvalContext{
Variables: map[string]cty.Value{
"data": cty.MapVal(map[string]cty.Value{
"coder_parameter": cty.MapVal(m),
}),
},
}
}
func ctyValueString(val cty.Value) (string, error) {
switch val.Type() {
case cty.Bool:
if val.True() {
return "true", nil
} else {
return "false", nil
}
case cty.Number:
return val.AsBigFloat().String(), nil
case cty.String:
return val.AsString(), nil
default:
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
}
}
func (b *Builder) getTemplateVersionWorkspaceTags() ([]database.TemplateVersionWorkspaceTag, error) {
if b.templateVersionWorkspaceTags != nil {
return *b.templateVersionWorkspaceTags, nil
+88 -21
View File
@@ -58,9 +58,11 @@ func TestBuilder_NoOptions(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
@@ -94,7 +96,8 @@ func TestBuilder_NoOptions(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
}
@@ -111,9 +114,11 @@ func TestBuilder_Initiator(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
@@ -130,7 +135,8 @@ func TestBuilder_Initiator(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Initiator(otherUserID)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
}
@@ -154,9 +160,11 @@ func TestBuilder_Baggage(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
@@ -172,7 +180,8 @@ func TestBuilder_Baggage(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Initiator(otherUserID)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
req.NoError(err)
}
@@ -189,9 +198,11 @@ func TestBuilder_Reason(t *testing.T) {
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(_ database.InsertProvisionerJobParams) {
@@ -207,7 +218,8 @@ func TestBuilder_Reason(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).Reason(database.BuildReasonAutostart)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
}
@@ -224,8 +236,10 @@ func TestBuilder_ActiveVersion(t *testing.T) {
withTemplate,
withActiveVersion(nil),
withLastBuildNotFound,
withTemplateVersionVariables(activeVersionID, nil),
withParameterSchemas(activeJobID, nil),
withWorkspaceTags(activeVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// previous rich parameters are not queried because there is no previous build.
// Outputs
@@ -247,7 +261,8 @@ func TestBuilder_ActiveVersion(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).ActiveVersion()
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
}
@@ -286,6 +301,14 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
Key: "is_debug_build",
Value: `data.coder_parameter.is_debug_build.value == "true" ? "in-debug-mode" : "no-debug"`,
},
{
Key: "variable_tag",
Value: `var.tag`,
},
{
Key: "another_variable_tag",
Value: `var.tag2`,
},
}
richParameters := []database.TemplateVersionParameter{
@@ -297,6 +320,11 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
{Name: "number_of_oranges", Type: "number", Description: "This is fifth parameter", Mutable: false, DefaultValue: "6", Options: json.RawMessage("[]")},
}
templateVersionVariables := []database.TemplateVersionVariable{
{Name: "tag", Description: "This is a variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value", Value: "my-value"},
{Name: "tag2", Description: "This is another variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value-2", Value: ""},
}
buildParameters := []codersdk.WorkspaceBuildParameter{
{Name: "project", Value: "foobar-foobaz"},
{Name: "is_debug_build", Value: "true"},
@@ -311,22 +339,26 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, templateVersionVariables),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, workspaceTags),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
asrt.Len(job.Tags, 10)
asrt.Len(job.Tags, 12)
expected := database.StringMap{
"actually_no": "false",
"cluster_tag": "best_developers",
"fruits_tag": "10",
"is_debug_build": "in-debug-mode",
"project_tag": "foobar-foobaz+12345",
"team_tag": "godzilla",
"yes_or_no": "true",
"actually_no": "false",
"cluster_tag": "best_developers",
"fruits_tag": "10",
"is_debug_build": "in-debug-mode",
"project_tag": "foobar-foobaz+12345",
"team_tag": "godzilla",
"yes_or_no": "true",
"variable_tag": "my-value",
"another_variable_tag": "default-value-2",
"scope": "user",
"version": "inactive",
@@ -343,7 +375,8 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(buildParameters)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
}
@@ -401,9 +434,11 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
@@ -422,7 +457,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
})
t.Run("UsePreviousParameterValues", func(t *testing.T) {
@@ -445,9 +481,11 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
@@ -466,7 +504,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
})
@@ -495,6 +534,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, schemas),
withWorkspaceTags(inactiveVersionID, nil),
@@ -502,7 +542,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
bldErr := wsbuilder.BuildError{}
req.ErrorAs(err, &bldErr)
asrt.Equal(http.StatusBadRequest, bldErr.Status)
@@ -526,6 +566,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withInactiveVersion(richParameters),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
@@ -536,7 +577,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).RichParameterValues(nextBuildParameters)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
bldErr := wsbuilder.BuildError{}
req.ErrorAs(err, &bldErr)
asrt.Equal(http.StatusBadRequest, bldErr.Status)
@@ -576,9 +618,11 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withTemplateVersionVariables(activeVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(activeJobID, nil),
withWorkspaceTags(activeVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
@@ -599,7 +643,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
RichParameterValues(nextBuildParameters).
VersionID(activeVersionID)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
})
@@ -637,9 +681,11 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withTemplateVersionVariables(activeVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(activeJobID, nil),
withWorkspaceTags(activeVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
@@ -660,7 +706,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
RichParameterValues(nextBuildParameters).
VersionID(activeVersionID)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
})
@@ -696,9 +742,11 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
withTemplate,
withActiveVersion(version2params),
withLastBuildFound,
withTemplateVersionVariables(activeVersionID, nil),
withRichParameters(initialBuildParameters),
withParameterSchemas(activeJobID, nil),
withWorkspaceTags(activeVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
@@ -719,7 +767,8 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart).
RichParameterValues(nextBuildParameters).
VersionID(activeVersionID)
_, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
})
}
@@ -900,6 +949,18 @@ func withParameterSchemas(jobID uuid.UUID, schemas []database.ParameterSchema) f
}
}
func withTemplateVersionVariables(versionID uuid.UUID, params []database.TemplateVersionVariable) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
c := mTx.EXPECT().GetTemplateVersionVariables(gomock.Any(), versionID).
Times(1)
if len(params) > 0 {
c.Return(params, nil)
} else {
c.Return(nil, sql.ErrNoRows)
}
}
}
func withRichParameters(params []database.WorkspaceBuildParameter) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
c := mTx.EXPECT().GetWorkspaceBuildParameters(gomock.Any(), lastBuildID).
@@ -987,3 +1048,9 @@ func expectBuildParameters(
)
}
}
func withProvisionerDaemons(provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().GetEligibleProvisionerDaemonsByProvisionerJobIDs(gomock.Any(), gomock.Any()).Return(provisionerDaemons, nil)
}
}
+1 -1
View File
@@ -15,7 +15,6 @@ import (
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"storj.io/drpc"
"tailscale.com/tailcfg"
@@ -25,6 +24,7 @@ import (
"github.com/coder/coder/v2/codersdk"
drpcsdk "github.com/coder/coder/v2/codersdk/drpc"
tailnetproto "github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/websocket"
)
// ExternalLogSourceID is the statically-defined ID of a log-source that
+5 -31
View File
@@ -15,12 +15,13 @@ import (
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/codersdk/drpc"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionerd/runner"
"github.com/coder/websocket"
)
type LogSource string
@@ -53,6 +54,7 @@ type ProvisionerDaemon struct {
// MatchedProvisioners represents the number of provisioner daemons
// available to take a job at a specific point in time.
// Introduced in Coder version 2.18.0.
type MatchedProvisioners struct {
// Count is the number of provisioner daemons that matched the given
// tags. If the count is 0, it means no provisioner daemons matched the
@@ -161,36 +163,8 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
}
return nil, nil, ReadBodyAsError(res)
}
logs := make(chan ProvisionerJobLog)
closed := make(chan struct{})
go func() {
defer close(closed)
defer close(logs)
defer conn.Close(websocket.StatusGoingAway, "")
var log ProvisionerJobLog
for {
msgType, msg, err := conn.Read(ctx)
if err != nil {
return
}
if msgType != websocket.MessageText {
return
}
err = json.Unmarshal(msg, &log)
if err != nil {
return
}
select {
case <-ctx.Done():
return
case logs <- log:
}
}
}()
return logs, closeFunc(func() error {
<-closed
return nil
}), nil
d := wsjson.NewDecoder[ProvisionerJobLog](conn, websocket.MessageText, c.logger)
return d.Chan(), d, nil
}
// ServeProvisionerDaemonRequest are the parameters to call ServeProvisionerDaemon with
+17 -1
View File
@@ -32,7 +32,7 @@ type TemplateVersion struct {
Archived bool `json:"archived"`
Warnings []TemplateVersionWarning `json:"warnings,omitempty" enums:"DEPRECATED_PARAMETERS"`
MatchedProvisioners MatchedProvisioners `json:"matched_provisioners,omitempty"`
MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"`
}
type TemplateVersionExternalAuth struct {
@@ -224,6 +224,22 @@ func (c *Client) TemplateVersionDryRun(ctx context.Context, version, job uuid.UU
return j, json.NewDecoder(res.Body).Decode(&j)
}
// TemplateVersionDryRunMatchedProvisioners returns the matched provisioners for a
// template version dry-run job.
func (c *Client) TemplateVersionDryRunMatchedProvisioners(ctx context.Context, version, job uuid.UUID) (MatchedProvisioners, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/dry-run/%s/matched-provisioners", version, job), nil)
if err != nil {
return MatchedProvisioners{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return MatchedProvisioners{}, ReadBodyAsError(res)
}
var matched MatchedProvisioners
return matched, json.NewDecoder(res.Body).Decode(&matched)
}
// TemplateVersionDryRunResources returns the resources of a finished template
// version dry-run job.
func (c *Client) TemplateVersionDryRunResources(ctx context.Context, version, job uuid.UUID) ([]WorkspaceResource, error) {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"net"
"nhooyr.io/websocket"
"github.com/coder/websocket"
)
// wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func
+1 -1
View File
@@ -8,10 +8,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"nhooyr.io/websocket"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
)
// TestWebsocketNetConn_LargeWrites tests that we can write large amounts of data thru the netconn
+4 -27
View File
@@ -12,9 +12,10 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/websocket"
)
type WorkspaceAgentStatus string
@@ -454,30 +455,6 @@ func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID,
}
return nil, nil, ReadBodyAsError(res)
}
logChunks := make(chan []WorkspaceAgentLog, 1)
closed := make(chan struct{})
ctx, wsNetConn := WebsocketNetConn(ctx, conn, websocket.MessageText)
decoder := json.NewDecoder(wsNetConn)
go func() {
defer close(closed)
defer close(logChunks)
defer conn.Close(websocket.StatusGoingAway, "")
for {
var logs []WorkspaceAgentLog
err = decoder.Decode(&logs)
if err != nil {
return
}
select {
case <-ctx.Done():
return
case logChunks <- logs:
}
}
}()
return logChunks, closeFunc(func() error {
_ = wsNetConn.Close()
<-closed
return nil
}), nil
d := wsjson.NewDecoder[[]WorkspaceAgentLog](conn, websocket.MessageText, c.logger)
return d.Chan(), d, nil
}
+22 -21
View File
@@ -51,27 +51,28 @@ const (
// WorkspaceBuild is an at-point representation of a workspace state.
// BuildNumbers start at 1 and increase by 1 for each subsequent build
type WorkspaceBuild struct {
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
WorkspaceName string `json:"workspace_name"`
WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"`
WorkspaceOwnerName string `json:"workspace_owner_name"`
WorkspaceOwnerAvatarURL string `json:"workspace_owner_avatar_url"`
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"`
TemplateVersionName string `json:"template_version_name"`
BuildNumber int32 `json:"build_number"`
Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"`
InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"`
InitiatorUsername string `json:"initiator_name"`
Job ProvisionerJob `json:"job"`
Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"`
Resources []WorkspaceResource `json:"resources"`
Deadline NullTime `json:"deadline,omitempty" format:"date-time"`
MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"`
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"`
DailyCost int32 `json:"daily_cost"`
ID uuid.UUID `json:"id" format:"uuid"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"`
WorkspaceName string `json:"workspace_name"`
WorkspaceOwnerID uuid.UUID `json:"workspace_owner_id" format:"uuid"`
WorkspaceOwnerName string `json:"workspace_owner_name"`
WorkspaceOwnerAvatarURL string `json:"workspace_owner_avatar_url"`
TemplateVersionID uuid.UUID `json:"template_version_id" format:"uuid"`
TemplateVersionName string `json:"template_version_name"`
BuildNumber int32 `json:"build_number"`
Transition WorkspaceTransition `json:"transition" enums:"start,stop,delete"`
InitiatorID uuid.UUID `json:"initiator_id" format:"uuid"`
InitiatorUsername string `json:"initiator_name"`
Job ProvisionerJob `json:"job"`
Reason BuildReason `db:"reason" json:"reason" enums:"initiator,autostart,autostop"`
Resources []WorkspaceResource `json:"resources"`
Deadline NullTime `json:"deadline,omitempty" format:"date-time"`
MaxDeadline NullTime `json:"max_deadline,omitempty" format:"date-time"`
Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"`
DailyCost int32 `json:"daily_cost"`
MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"`
}
// WorkspaceResource describes resources used to create a workspace, for instance:
+1 -1
View File
@@ -9,13 +9,13 @@ import (
"slices"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"cdr.dev/slog"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/websocket"
)
var permanentErrorStatuses = []int{
+1 -1
View File
@@ -13,7 +13,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"nhooyr.io/websocket"
"tailscale.com/tailcfg"
"cdr.dev/slog"
@@ -26,6 +25,7 @@ import (
tailnetproto "github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/coder/v2/tailnet/tailnettest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
)
func TestWebsocketDialer_TokenController(t *testing.T) {
+1 -1
View File
@@ -14,7 +14,6 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"tailscale.com/tailcfg"
"tailscale.com/wgengine/capture"
@@ -23,6 +22,7 @@ import (
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/quartz"
"github.com/coder/websocket"
)
var ErrSkipClose = xerrors.New("skip tailnet close")
+74
View File
@@ -0,0 +1,74 @@
package wsjson
import (
"context"
"encoding/json"
"sync/atomic"
"cdr.dev/slog"
"github.com/coder/websocket"
)
type Decoder[T any] struct {
conn *websocket.Conn
typ websocket.MessageType
ctx context.Context
cancel context.CancelFunc
chanCalled atomic.Bool
logger slog.Logger
}
// Chan starts the decoder reading from the websocket and returns a channel for reading the
// resulting values. The chan T is closed if the underlying websocket is closed, or we encounter an
// error. We also close the underlying websocket if we encounter an error reading or decoding.
func (d *Decoder[T]) Chan() <-chan T {
if !d.chanCalled.CompareAndSwap(false, true) {
panic("chan called more than once")
}
values := make(chan T, 1)
go func() {
defer close(values)
defer d.conn.Close(websocket.StatusGoingAway, "")
for {
// we don't use d.ctx here because it only gets canceled after closing the connection
// and a "connection closed" type error is more clear than context canceled.
typ, b, err := d.conn.Read(context.Background())
if err != nil {
// might be benign like EOF, so just log at debug
d.logger.Debug(d.ctx, "error reading from websocket", slog.Error(err))
return
}
if typ != d.typ {
d.logger.Error(d.ctx, "websocket type mismatch while decoding")
return
}
var value T
err = json.Unmarshal(b, &value)
if err != nil {
d.logger.Error(d.ctx, "error unmarshalling", slog.Error(err))
return
}
select {
case values <- value:
// OK
case <-d.ctx.Done():
return
}
}
}()
return values
}
// nolint: revive // complains that Encoder has the same function name
func (d *Decoder[T]) Close() error {
err := d.conn.Close(websocket.StatusNormalClosure, "")
d.cancel()
return err
}
// NewDecoder creates a JSON-over-websocket decoder for type T, which must be deserializable from
// JSON.
func NewDecoder[T any](conn *websocket.Conn, typ websocket.MessageType, logger slog.Logger) *Decoder[T] {
ctx, cancel := context.WithCancel(context.Background())
return &Decoder[T]{conn: conn, ctx: ctx, cancel: cancel, typ: typ, logger: logger}
}
+43
View File
@@ -0,0 +1,43 @@
package wsjson
import (
"context"
"encoding/json"
"golang.org/x/xerrors"
"github.com/coder/websocket"
)
type Encoder[T any] struct {
conn *websocket.Conn
typ websocket.MessageType
}
func (e *Encoder[T]) Encode(v T) error {
w, err := e.conn.Writer(context.Background(), e.typ)
if err != nil {
return xerrors.Errorf("get websocket writer: %w", err)
}
defer w.Close()
j := json.NewEncoder(w)
err = j.Encode(v)
if err != nil {
return xerrors.Errorf("encode json: %w", err)
}
return nil
}
func (e *Encoder[T]) Close(c websocket.StatusCode) error {
return e.conn.Close(c, "")
}
// NewEncoder creates a JSON-over websocket encoder for the type T, which must be JSON-serializable.
// You may then call Encode() to send objects over the websocket. Creating an Encoder closes the
// websocket for reading, turning it into a unidirectional write stream of JSON-encoded objects.
func NewEncoder[T any](conn *websocket.Conn, typ websocket.MessageType) *Encoder[T] {
// Here we close the websocket for reading, so that the websocket library will handle pings and
// close frames.
_ = conn.CloseRead(context.Background())
return &Encoder[T]{conn: conn, typ: typ}
}
+1 -1
View File
@@ -24,7 +24,7 @@ We track the following resources:
| OAuth2ProviderAppSecret<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>app_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>display_secret</td><td>false</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>secret_prefix</td><td>false</td></tr></tbody></table> |
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
@@ -62,11 +62,6 @@ variables and parameters. This is illustrated in the table below:
## Constraints
### Default Values
All template variables and `coder_parameter` data sources **must** provide a
default value. Failure to do so will result in an error.
### Tagged provisioners
It is possible to choose tag combinations that no provisioner can handle. This
@@ -127,6 +122,6 @@ variables, and references to other resources.
**Not supported**
- Function calls: `try(var.foo, "default")`
- Function calls that reference files on disk: `abspath`, `file*`, `pathexpand`
- Resources: `compute_instance.dev.name`
- Data sources other than `coder_parameter`: `data.local_file.hostname.content`
+29
View File
@@ -52,6 +52,11 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -237,6 +242,11 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -856,6 +866,11 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -1114,6 +1129,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -1277,6 +1297,10 @@ Status Code **200**
| `»» tags` | object | false | | |
| `»»» [any property]` | string | false | | |
| `»» worker_id` | string(uuid) | false | | |
| `» matched_provisioners` | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) | false | | |
| `»» available` | integer | false | | Available is the number of provisioner daemons that are available to take jobs. This may be less than the count if some provisioners are busy or have been stopped. |
| `»» count` | integer | false | | Count is the number of provisioner daemons that matched the given tags. If the count is 0, it means no provisioner daemons matched the requested tags. |
| `»» most_recently_seen` | string(date-time) | false | | Most recently seen is the most recently seen time of the set of matched provisioners. If no provisioners matched, this field will be null. |
| `» max_deadline` | string(date-time) | false | | |
| `» reason` | [codersdk.BuildReason](schemas.md#codersdkbuildreason) | false | | |
| `» resources` | array | false | | |
@@ -1500,6 +1524,11 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
+16
View File
@@ -6602,6 +6602,11 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -7300,6 +7305,11 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -7439,6 +7449,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `initiator_id` | string | false | | |
| `initiator_name` | string | false | | |
| `job` | [codersdk.ProvisionerJob](#codersdkprovisionerjob) | false | | |
| `matched_provisioners` | [codersdk.MatchedProvisioners](#codersdkmatchedprovisioners) | false | | |
| `max_deadline` | string | false | | |
| `reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | |
| `resources` | array of [codersdk.WorkspaceResource](#codersdkworkspaceresource) | false | | |
@@ -7926,6 +7937,11 @@ If the schedule is empty, the user will be updated to use the default schedule.|
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
+40
View File
@@ -1944,6 +1944,46 @@ Status Code **200**
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get template version dry-run matched provisioners
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners \
-H 'Accept: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`GET /templateversions/{templateversion}/dry-run/{jobID}/matched-provisioners`
### Parameters
| Name | In | Type | Required | Description |
| ----------------- | ---- | ------------ | -------- | ------------------- |
| `templateversion` | path | string(uuid) | true | Template version ID |
| `jobID` | path | string(uuid) | true | Job ID |
### Example responses
> 200 Response
```json
{
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
}
```
### Responses
| Status | Meaning | Description | Schema |
| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.MatchedProvisioners](schemas.md#codersdkmatchedprovisioners) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get template version dry-run resources by job ID
### Code samples
+30
View File
@@ -91,6 +91,11 @@ of the template will be used.
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -309,6 +314,11 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -551,6 +561,11 @@ of the template will be used.
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -772,6 +787,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -987,6 +1007,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
@@ -1321,6 +1346,11 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
},
"worker_id": "ae5fa6f7-c55b-40c1-b40a-b36ac467652b"
},
"matched_provisioners": {
"available": 0,
"count": 0,
"most_recently_seen": "2019-08-24T14:15:22Z"
},
"max_deadline": "2019-08-24T14:15:22Z",
"reason": "initiator",
"resources": [
+1
View File
@@ -127,6 +127,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"created_by_avatar_url": ActionIgnore,
"created_by_username": ActionIgnore,
"archived": ActionTrack,
"source_example_id": ActionIgnore, // Never changes.
},
&database.User{}: {
"id": ActionTrack,
@@ -7,6 +7,7 @@ import (
"crypto/tls"
"io"
"net/http"
"os/exec"
"strings"
"testing"
"time"
@@ -16,7 +17,8 @@ import (
"github.com/moby/moby/pkg/namesgenerator"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
@@ -28,6 +30,7 @@ import (
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/enterprise/dbcrypt"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisioner/terraform"
"github.com/coder/coder/v2/provisionerd"
provisionerdproto "github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionersdk"
@@ -304,14 +307,31 @@ func CreateOrganization(t *testing.T, client *codersdk.Client, opts CreateOrgani
return org
}
// NewExternalProvisionerDaemon runs an external provisioner daemon in a
// goroutine and returns a closer to stop it. The echo provisioner is used
// here. This is the default provisioner for tests and should be fine for
// most use cases. If you need to test terraform-specific behaviors, use
// NewExternalProvisionerDaemonTerraform instead.
func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer {
t.Helper()
return newExternalProvisionerDaemon(t, client, org, tags, codersdk.ProvisionerTypeEcho)
}
// NewExternalProvisionerDaemonTerraform runs an external provisioner daemon in
// a goroutine and returns a closer to stop it. The terraform provisioner is
// used here. Avoid using this unless you need to test terraform-specific
// behaviors!
func NewExternalProvisionerDaemonTerraform(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer {
t.Helper()
return newExternalProvisionerDaemon(t, client, org, tags, codersdk.ProvisionerTypeTerraform)
}
// nolint // This function is a helper for tests and should not be linted.
func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string, provisionerType codersdk.ProvisionerType) io.Closer {
t.Helper()
// Without this check, the provisioner will silently fail.
entitlements, err := client.Entitlements(context.Background())
if err != nil {
// AGPL instances will throw this error. They cannot use external
// provisioners.
t.Errorf("external provisioners requires a license with entitlements. The client failed to fetch the entitlements, is this an enterprise instance of coderd?")
t.FailNow()
return nil
@@ -319,42 +339,67 @@ func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui
feature := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons]
if !feature.Enabled || feature.Entitlement != codersdk.EntitlementEntitled {
require.NoError(t, xerrors.Errorf("external provisioner daemons require an entitled license"))
t.Errorf("external provisioner daemons require an entitled license")
t.FailNow()
return nil
}
echoClient, echoServer := drpc.MemTransportPipe()
provisionerClient, provisionerSrv := drpc.MemTransportPipe()
ctx, cancelFunc := context.WithCancel(context.Background())
serveDone := make(chan struct{})
t.Cleanup(func() {
_ = echoClient.Close()
_ = echoServer.Close()
_ = provisionerClient.Close()
_ = provisionerSrv.Close()
cancelFunc()
<-serveDone
})
go func() {
defer close(serveDone)
err := echo.Serve(ctx, &provisionersdk.ServeOptions{
Listener: echoServer,
WorkDirectory: t.TempDir(),
})
assert.NoError(t, err)
}()
switch provisionerType {
case codersdk.ProvisionerTypeTerraform:
// Ensure the Terraform binary is present in the path.
// If not, we fail this test rather than downloading it.
terraformPath, err := exec.LookPath("terraform")
require.NoError(t, err, "terraform binary not found in PATH")
t.Logf("using Terraform binary at %s", terraformPath)
go func() {
defer close(serveDone)
assert.NoError(t, terraform.Serve(ctx, &terraform.ServeOptions{
BinaryPath: terraformPath,
CachePath: t.TempDir(),
ServeOptions: &provisionersdk.ServeOptions{
Listener: provisionerSrv,
WorkDirectory: t.TempDir(),
},
}))
}()
case codersdk.ProvisionerTypeEcho:
go func() {
defer close(serveDone)
assert.NoError(t, echo.Serve(ctx, &provisionersdk.ServeOptions{
Listener: provisionerSrv,
WorkDirectory: t.TempDir(),
}))
}()
default:
t.Fatalf("unsupported provisioner type: %s", provisionerType)
return nil
}
daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) {
return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{
ID: uuid.New(),
Name: t.Name(),
Name: testutil.GetRandomName(t),
Organization: org,
Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho},
Provisioners: []codersdk.ProvisionerType{provisionerType},
Tags: tags,
})
}, &provisionerd.Options{
Logger: testutil.Logger(t).Named("provisionerd"),
Logger: testutil.Logger(t).Named("provisionerd").Leveled(slog.LevelDebug),
UpdateInterval: 250 * time.Millisecond,
ForceCancelInterval: 5 * time.Second,
Connector: provisionerd.LocalProvisioners{
string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient),
string(provisionerType): sdkproto.NewDRPCProvisionerClient(provisionerClient),
},
})
closer := coderdtest.NewProvisionerDaemonCloser(daemon)
+81
View File
@@ -0,0 +1,81 @@
package coderd_test
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/testutil"
)
// TestAgentGitSSHKeyCustomRoles tests that the agent can fetch its git ssh key when
// the user has a custom role in a second workspace.
func TestAgentGitSSHKeyCustomRoles(t *testing.T) {
t.Parallel()
owner, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureCustomRoles: 1,
codersdk.FeatureMultipleOrganizations: 1,
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},
})
// When custom roles exist in a second organization
org := coderdenttest.CreateOrganization(t, owner, coderdenttest.CreateOrganizationOptions{
IncludeProvisionerDaemon: true,
})
ctx := testutil.Context(t, testutil.WaitShort)
//nolint:gocritic // required to make orgs
newRole, err := owner.CreateOrganizationRole(ctx, codersdk.Role{
Name: "custom",
OrganizationID: org.ID.String(),
DisplayName: "",
SitePermissions: nil,
OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
codersdk.ResourceTemplate: {codersdk.ActionRead, codersdk.ActionCreate, codersdk.ActionUpdate},
}),
UserPermissions: nil,
})
require.NoError(t, err)
// Create the new user
client, _ := coderdtest.CreateAnotherUser(t, owner, org.ID, rbac.RoleIdentifier{Name: newRole.Name, OrganizationID: org.ID})
// Create the workspace + agent
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, org.ID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
project := coderdtest.CreateTemplate(t, client, org.ID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, project.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
agentKey, err := agentClient.GitSSHKey(ctx)
require.NoError(t, err)
require.NotEmpty(t, agentKey.PrivateKey)
}
+1 -2
View File
@@ -15,12 +15,10 @@ import (
"go.opentelemetry.io/otel/trace"
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"storj.io/drpc/drpcmux"
"storj.io/drpc/drpcserver"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
@@ -35,6 +33,7 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/websocket"
)
func (api *API) provisionerDaemonsEnabledMW(next http.Handler) http.Handler {
+1 -1
View File
@@ -285,7 +285,7 @@ func TestProvisionerDaemonServe(t *testing.T) {
daemons, err := client.ProvisionerDaemons(context.Background())
assert.NoError(t, err, "failed to get provisioner daemons")
return len(daemons) > 0 &&
assert.Equal(t, t.Name(), daemons[0].Name) &&
assert.NotEmpty(t, daemons[0].Name) &&
assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope]) &&
assert.Equal(t, user.UserID.String(), daemons[0].Tags[provisionersdk.TagOwner])
}, testutil.WaitShort, testutil.IntervalMedium)
+1 -1
View File
@@ -109,7 +109,7 @@ func TestWorkspaceBuild(t *testing.T) {
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
_, err = c.Client.CreateWorkspace(ctx, owner.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
_, err = c.Client.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateVersionID: oldVersion.ID,
Name: "abc123",
AutomaticUpdates: codersdk.AutomaticUpdatesNever,
@@ -4,12 +4,12 @@ import (
"net/http"
"github.com/google/uuid"
"nhooyr.io/websocket"
"github.com/coder/coder/v2/apiversion"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/websocket"
)
// @Summary Workspace Proxy Coordinate
+438
View File
@@ -1,7 +1,10 @@
package coderd_test
import (
"bytes"
"context"
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"testing"
@@ -17,8 +20,10 @@ import (
"github.com/coder/coder/v2/coderd/autobuild"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/rbac"
agplschedule "github.com/coder/coder/v2/coderd/schedule"
@@ -31,6 +36,7 @@ import (
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/enterprise/coderd/schedule"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/testutil"
)
@@ -1176,6 +1182,230 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
})
}
// TestWorkspaceTagsTerraform tests that a workspace can be created with tags.
// This is an end-to-end-style test, meaning that we actually run the
// real Terraform provisioner and validate that the workspace is created
// successfully. The workspace itself does not specify any resources, and
// this is fine.
func TestWorkspaceTagsTerraform(t *testing.T) {
t.Parallel()
mainTfTemplate := `
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
provider "coder" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "unrelated" {
name = "unrelated"
type = "list(string)"
default = jsonencode(["a", "b"])
}
%s
`
for _, tc := range []struct {
name string
// tags to apply to the external provisioner
provisionerTags map[string]string
// tags to apply to the create template version request
createTemplateVersionRequestTags map[string]string
// the coder_workspace_tags bit of main.tf.
// you can add more stuff here if you need
tfWorkspaceTags string
templateImportUserVariableValues []codersdk.VariableValue
// if we need to set parameters on workspace build
workspaceBuildParameters []codersdk.WorkspaceBuildParameter
skipCreateWorkspace bool
}{
{
name: "no tags",
tfWorkspaceTags: ``,
},
{
name: "empty tags",
tfWorkspaceTags: `
data "coder_workspace_tags" "tags" {
tags = {}
}
`,
},
{
name: "static tag",
provisionerTags: map[string]string{"foo": "bar"},
tfWorkspaceTags: `
data "coder_workspace_tags" "tags" {
tags = {
"foo" = "bar"
}
}`,
},
{
name: "tag variable",
provisionerTags: map[string]string{"foo": "bar"},
tfWorkspaceTags: `
variable "foo" {
default = "bar"
}
data "coder_workspace_tags" "tags" {
tags = {
"foo" = var.foo
}
}`,
},
{
name: "tag param",
provisionerTags: map[string]string{"foo": "bar"},
tfWorkspaceTags: `
data "coder_parameter" "foo" {
name = "foo"
type = "string"
default = "bar"
}
data "coder_workspace_tags" "tags" {
tags = {
"foo" = data.coder_parameter.foo.value
}
}`,
},
{
name: "tag param with default from var",
provisionerTags: map[string]string{"foo": "bar"},
tfWorkspaceTags: `
variable "foo" {
type = string
default = "bar"
}
data "coder_parameter" "foo" {
name = "foo"
type = "string"
default = var.foo
}
data "coder_workspace_tags" "tags" {
tags = {
"foo" = data.coder_parameter.foo.value
}
}`,
},
{
name: "override no tags",
provisionerTags: map[string]string{"foo": "baz"},
createTemplateVersionRequestTags: map[string]string{"foo": "baz"},
tfWorkspaceTags: ``,
},
{
name: "override empty tags",
provisionerTags: map[string]string{"foo": "baz"},
createTemplateVersionRequestTags: map[string]string{"foo": "baz"},
tfWorkspaceTags: `
data "coder_workspace_tags" "tags" {
tags = {}
}`,
},
{
name: "overrides static tag from request",
provisionerTags: map[string]string{"foo": "baz"},
createTemplateVersionRequestTags: map[string]string{"foo": "baz"},
tfWorkspaceTags: `
data "coder_workspace_tags" "tags" {
tags = {
"foo" = "bar"
}
}`,
// When we go to create the workspace, there won't be any provisioner
// matching tag foo=bar.
skipCreateWorkspace: true,
},
{
name: "overrides with dynamic option from var",
provisionerTags: map[string]string{"foo": "bar"},
createTemplateVersionRequestTags: map[string]string{"foo": "bar"},
templateImportUserVariableValues: []codersdk.VariableValue{{Name: "default_foo", Value: "baz"}, {Name: "foo", Value: "bar,baz"}},
workspaceBuildParameters: []codersdk.WorkspaceBuildParameter{{Name: "foo", Value: "bar"}},
tfWorkspaceTags: `
variable "default_foo" {
type = string
}
variable "foo" {
type = string
}
data "coder_parameter" "foo" {
name = "foo"
type = "string"
default = var.default_foo
mutable = false
dynamic "option" {
for_each = toset(split(",", var.foo))
content {
name = option.value
value = option.value
}
}
}
data "coder_workspace_tags" "tags" {
tags = {
"foo" = data.coder_parameter.foo.value
}
}`,
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitSuperLong)
client, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
// We intentionally do not run a built-in provisioner daemon here.
IncludeProvisionerDaemon: false,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},
})
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
_ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags)
// Creating a template as a template admin must succeed
templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)}
tarBytes := testutil.CreateTar(t, templateFiles)
fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarBytes))
require.NoError(t, err, "failed to upload file")
tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
Name: testutil.GetRandomName(t),
FileID: fi.ID,
StorageMethod: codersdk.ProvisionerStorageMethodFile,
Provisioner: codersdk.ProvisionerTypeTerraform,
ProvisionerTags: tc.createTemplateVersionRequestTags,
UserVariableValues: tc.templateImportUserVariableValues,
})
require.NoError(t, err, "failed to create template version")
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID)
tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, tv.ID)
if !tc.skipCreateWorkspace {
// Creating a workspace as a non-privileged user must succeed
ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
TemplateID: tpl.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: tc.workspaceBuildParameters,
})
require.NoError(t, err, "failed to create workspace")
coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID)
}
})
}
}
// Blocked by autostart requirements
func TestExecutorAutostartBlocked(t *testing.T) {
t.Parallel()
@@ -1522,6 +1752,214 @@ func TestAdminViewAllWorkspaces(t *testing.T) {
require.Equal(t, 0, len(memberViewWorkspaces.Workspaces), "member in other org should see 0 workspaces")
}
func TestWorkspaceByOwnerAndName(t *testing.T) {
t.Parallel()
t.Run("Matching Provisioner", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
client, db, userResponse := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},
})
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, userResponse.UserID, rbac.ExpandableScope(rbac.ScopeAll))
require.NoError(t, err)
user, err := client.User(ctx, userSubject.ID)
require.NoError(t, err)
username := user.Username
_ = coderdenttest.NewExternalProvisionerDaemon(t, client, userResponse.OrganizationID, map[string]string{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
})
version := coderdtest.CreateTemplateVersion(t, client, userResponse.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, userResponse.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
// Pending builds should show matching provisioners
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 1)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 1)
// Completed builds should not show matching provisioners, because no provisioner daemon can
// be eligible to process a job that is already completed.
completedBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
require.Equal(t, completedBuild.Status, codersdk.WorkspaceStatusRunning)
require.Equal(t, completedBuild.MatchedProvisioners.Count, 0)
require.Equal(t, completedBuild.MatchedProvisioners.Available, 0)
ws, err := client.WorkspaceByOwnerAndName(ctx, username, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
// Verify the workspace details
require.Equal(t, workspace.ID, ws.ID)
require.Equal(t, workspace.Name, ws.Name)
require.Equal(t, workspace.TemplateID, ws.TemplateID)
require.Equal(t, completedBuild.Status, ws.LatestBuild.Status)
require.Equal(t, ws.LatestBuild.MatchedProvisioners.Count, 0)
require.Equal(t, ws.LatestBuild.MatchedProvisioners.Available, 0)
// Verify that the provisioner daemon is registered in the database
//nolint:gocritic // unit testing
daemons, err := db.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
require.NoError(t, err)
require.Equal(t, 1, len(daemons))
require.Equal(t, provisionersdk.ScopeOrganization, daemons[0].Tags[provisionersdk.TagScope])
})
t.Run("No Matching Provisioner", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
client, db, userResponse := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},
})
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, userResponse.UserID, rbac.ExpandableScope(rbac.ScopeAll))
require.NoError(t, err)
user, err := client.User(ctx, userSubject.ID)
require.NoError(t, err)
username := user.Username
closer := coderdenttest.NewExternalProvisionerDaemon(t, client, userResponse.OrganizationID, map[string]string{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
})
version := coderdtest.CreateTemplateVersion(t, client, userResponse.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, userResponse.OrganizationID, version.ID)
// nolint:gocritic // unit testing
daemons, err := db.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
require.NoError(t, err)
require.Equal(t, len(daemons), 1)
// Simulate a provisioner daemon failure:
err = closer.Close()
require.NoError(t, err)
// Simulate it's subsequent deletion from the database:
// nolint:gocritic // unit testing
_, err = db.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(ctx), database.UpsertProvisionerDaemonParams{
Name: daemons[0].Name,
OrganizationID: daemons[0].OrganizationID,
Tags: daemons[0].Tags,
Provisioners: daemons[0].Provisioners,
Version: daemons[0].Version,
APIVersion: daemons[0].APIVersion,
KeyID: daemons[0].KeyID,
// Simulate the passing of time such that the provisioner daemon is considered stale
// and will be deleted:
CreatedAt: time.Now().Add(-time.Hour * 24 * 8),
LastSeenAt: sql.NullTime{
Time: time.Now().Add(-time.Hour * 24 * 8),
Valid: true,
},
})
require.NoError(t, err)
// nolint:gocritic // unit testing
err = db.DeleteOldProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
require.NoError(t, err)
// Create a workspace that will not be able to provision due to a lack of provisioner daemons:
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 0)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
// nolint:gocritic // unit testing
_, err = client.WorkspaceByOwnerAndName(dbauthz.As(ctx, userSubject), username, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 0)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
})
t.Run("Unavailable Provisioner", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
client, db, userResponse := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},
})
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, userResponse.UserID, rbac.ExpandableScope(rbac.ScopeAll))
require.NoError(t, err)
user, err := client.User(ctx, userSubject.ID)
require.NoError(t, err)
username := user.Username
closer := coderdenttest.NewExternalProvisionerDaemon(t, client, userResponse.OrganizationID, map[string]string{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
})
version := coderdtest.CreateTemplateVersion(t, client, userResponse.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, userResponse.OrganizationID, version.ID)
// nolint:gocritic // unit testing
daemons, err := db.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
require.NoError(t, err)
require.Equal(t, len(daemons), 1)
// Simulate a provisioner daemon failure:
err = closer.Close()
require.NoError(t, err)
// nolint:gocritic // unit testing
_, err = db.UpsertProvisionerDaemon(dbauthz.AsSystemRestricted(ctx), database.UpsertProvisionerDaemonParams{
Name: daemons[0].Name,
OrganizationID: daemons[0].OrganizationID,
Tags: daemons[0].Tags,
Provisioners: daemons[0].Provisioners,
Version: daemons[0].Version,
APIVersion: daemons[0].APIVersion,
KeyID: daemons[0].KeyID,
// Simulate the passing of time such that the provisioner daemon, though not stale, has been
// has been inactive for a while:
CreatedAt: time.Now().Add(-time.Hour * 24 * 2),
LastSeenAt: sql.NullTime{
Time: time.Now().Add(-time.Hour * 24 * 2),
Valid: true,
},
})
require.NoError(t, err)
// Create a workspace that will not be able to provision due to a lack of provisioner daemons:
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 1)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
// nolint:gocritic // unit testing
_, err = client.WorkspaceByOwnerAndName(dbauthz.As(ctx, userSubject), username, workspace.Name, codersdk.WorkspaceOptions{})
require.NoError(t, err)
require.Equal(t, workspace.LatestBuild.Status, codersdk.WorkspaceStatusPending)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Count, 1)
require.Equal(t, workspace.LatestBuild.MatchedProvisioners.Available, 0)
})
}
func must[T any](value T, err error) T {
if err != nil {
panic(err)
@@ -3,6 +3,8 @@ package dbcrypt
import (
"bytes"
"encoding/base64"
"os"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -89,3 +91,35 @@ func TestCiphersBackwardCompatibility(t *testing.T) {
require.NoError(t, err, "decryption should succeed")
require.Equal(t, msg, string(decrypted), "decrypted message should match original message")
}
// If you're looking here, you're probably in trouble.
// Here's what you need to do:
// 1. Get the current CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS environment variable.
// 2. Run the following command:
// ENCRYPT_ME="<value to encrypt>" CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS="<secret keys here>" go test -v -count=1 ./enterprise/dbcrypt -test.run='^TestHelpMeEncryptSomeValue$'
// 3. Copy the value from the test output and do what you need with it.
func TestHelpMeEncryptSomeValue(t *testing.T) {
t.Parallel()
t.Skip("this only exists if you need to encrypt a value with dbcrypt, it does not actually test anything")
valueToEncrypt := os.Getenv("ENCRYPT_ME")
t.Logf("valueToEncrypt: %q", valueToEncrypt)
keys := os.Getenv("CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS")
require.NotEmpty(t, keys, "Set the CODER_EXTERNAL_TOKEN_ENCRYPTION_KEYS environment variable to use this")
base64Keys := strings.Split(keys, ",")
activeKey := base64Keys[0]
decodedKey, err := base64.StdEncoding.DecodeString(activeKey)
require.NoError(t, err, "the active key should be valid base64")
cipher, err := cipherAES256(decodedKey)
require.NoError(t, err)
t.Logf("cipher digest: %+v", cipher.HexDigest())
encryptedEmptyString, err := cipher.Encrypt([]byte(valueToEncrypt))
require.NoError(t, err)
t.Logf("encrypted and base64-encoded: %q", base64.StdEncoding.EncodeToString(encryptedEmptyString))
}
+15
View File
@@ -261,6 +261,21 @@ func (db *dbCrypt) UpdateExternalAuthLink(ctx context.Context, params database.U
return link, nil
}
func (db *dbCrypt) UpdateExternalAuthLinkRefreshToken(ctx context.Context, params database.UpdateExternalAuthLinkRefreshTokenParams) error {
// We would normally use a sql.NullString here, but sqlc does not want to make
// a params struct with a nullable string.
var digest sql.NullString
if params.OAuthRefreshTokenKeyID != "" {
digest.String = params.OAuthRefreshTokenKeyID
digest.Valid = true
}
if err := db.encryptField(&params.OAuthRefreshToken, &digest); err != nil {
return err
}
return db.Store.UpdateExternalAuthLinkRefreshToken(ctx, params)
}
func (db *dbCrypt) GetCryptoKeys(ctx context.Context) ([]database.CryptoKey, error) {
keys, err := db.Store.GetCryptoKeys(ctx)
if err != nil {
@@ -17,6 +17,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
)
func TestUserLinks(t *testing.T) {
@@ -96,6 +97,31 @@ func TestUserLinks(t *testing.T) {
require.EqualValues(t, expectedClaims, rawLink.Claims)
})
t.Run("UpdateExternalAuthLinkRefreshToken", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
user := dbgen.User(t, crypt, database.User{})
link := dbgen.ExternalAuthLink(t, crypt, database.ExternalAuthLink{
UserID: user.ID,
})
err := crypt.UpdateExternalAuthLinkRefreshToken(ctx, database.UpdateExternalAuthLinkRefreshTokenParams{
OAuthRefreshToken: "",
OAuthRefreshTokenKeyID: link.OAuthRefreshTokenKeyID.String,
UpdatedAt: dbtime.Now(),
ProviderID: link.ProviderID,
UserID: link.UserID,
})
require.NoError(t, err)
rawLink, err := db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
ProviderID: link.ProviderID,
UserID: link.UserID,
})
require.NoError(t, err)
requireEncryptedEquals(t, ciphers[0], rawLink.OAuthRefreshToken, "")
})
t.Run("GetUserLinkByLinkedID", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
+1 -1
View File
@@ -10,7 +10,6 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"tailscale.com/tailcfg"
"cdr.dev/slog"
@@ -19,6 +18,7 @@ import (
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
agpl "github.com/coder/coder/v2/tailnet"
"github.com/coder/websocket"
)
// Client is a HTTP client for a subset of Coder API routes that external
+3
View File
@@ -3,6 +3,9 @@ terraform {
coder = {
source = "coder/coder"
}
cloudinit = {
source = "hashicorp/cloudinit"
}
aws = {
source = "hashicorp/aws"
}
+1 -1
View File
@@ -184,7 +184,7 @@ resource "docker_container" "workspace" {
ip = "host-gateway"
}
volumes {
container_path = "/home/${local.username}"
container_path = "/home/coder"
volume_name = docker_volume.home_volume.name
read_only = false
}
+2 -5
View File
@@ -7,7 +7,7 @@ icon: /icon/docker.png
# Overview
This Coder template presents use of [Workspace Tags](https://coder.com/docs/templates/workspace-tags) [Coder Parameters](https://coder.com/docs/templates/parameters).
This Coder template presents use of [Workspace Tags](https://coder.com/docs/admin/templates/extending-templates/workspace-tags) and [Coder Parameters](https://coder.com/docs/templates/parameters).
# Use case
@@ -18,10 +18,7 @@ By using `coder_workspace_tags` and `coder_parameter`s, template administrators
# Notes
- You will need to have an [external provisioner](https://coder.com/docs/admin/provisioners#external-provisioners) with the correct tagset running in order to import this template.
- When specifying values for the `coder_workspace_tags` data source, you are restricted to using a subset of Terraform's capabilities.
- You must specify default values for all data sources and variables referenced by the `coder_workspace_tags` data source.
See [Workspace Tags](https://coder.com/docs/templates/workspace-tags) for more information.
- When specifying values for the `coder_workspace_tags` data source, you are restricted to using a subset of Terraform's capabilities. See [here](https://coder.com/docs/admin/templates/extending-templates/workspace-tags) for more details.
## Development
+22 -11
View File
@@ -39,7 +39,7 @@ replace github.com/dlclark/regexp2 => github.com/dlclark/regexp2 v1.7.0
// There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here:
// https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20241003034647-02286e537fc2
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20241218201526-b53d914d625f
// This is replaced to include
// 1. a fix for a data race: c.f. https://github.com/tailscale/wireguard-go/pull/25
@@ -174,15 +174,15 @@ require (
go.uber.org/atomic v1.11.0
go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29
go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516
golang.org/x/crypto v0.29.0
golang.org/x/crypto v0.31.0
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
golang.org/x/mod v0.22.0
golang.org/x/net v0.31.0
golang.org/x/net v0.32.0
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.9.0
golang.org/x/sys v0.27.0
golang.org/x/term v0.26.0
golang.org/x/text v0.20.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.28.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
golang.org/x/tools v0.27.0
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
google.golang.org/api v0.209.0
@@ -192,7 +192,6 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc
nhooyr.io/websocket v1.8.7
storj.io/drpc v0.0.33
tailscale.com v1.46.1
)
@@ -205,6 +204,7 @@ require (
github.com/charmbracelet/bubbletea v1.2.1
github.com/charmbracelet/lipgloss v1.0.0
github.com/coder/serpent v0.10.0
github.com/coder/websocket v1.8.12
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.21.2
github.com/go-jose/go-jose/v4 v4.0.2
@@ -256,9 +256,9 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/DataDog/appsec-internal-go v1.8.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0 // indirect
github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 // indirect
github.com/DataDog/datadog-go/v5 v5.5.0 // indirect
github.com/DataDog/go-tuf v1.1.0-0.5.2 // indirect
github.com/DataDog/gostackparse v0.7.0 // indirect
github.com/DataDog/sketches-go v1.4.5 // indirect
@@ -444,3 +444,14 @@ require (
inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
require (
github.com/aquasecurity/trivy-iac v0.8.0
github.com/zclconf/go-cty-yaml v1.0.3
)
require (
github.com/DataDog/go-sqllexer v0.0.14 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
)
+32 -71
View File
@@ -26,14 +26,16 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/DataDog/appsec-internal-go v1.8.0 h1:1Tfn3LEogntRqZtf88twSApOCAAO3V+NILYhuQIo4J4=
github.com/DataDog/appsec-internal-go v1.8.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0 h1:LplNAmMgZvGU7kKA0+4c1xWOjz828xweW5TCi8Mw9Q0=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0/go.mod h1:4Vo3SJ24uzfKHUHLoFa8t8o+LH+7TCQ7sPcZDtOpSP4=
github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 h1:nOrRNCHyriM/EjptMrttFOQhRSmvfagESdpyknb5VPg=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0/go.mod h1:MfDvphBMmEMwE3a30h27AtPO7OzmvdoVTiGY1alEmo4=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 h1:5hGO0Z8ih0bRojuq+1ZwLFtdgsfO3TqIjbwJAH12sOQ=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0/go.mod h1:jN5BsZI+VilHJV1Wac/efGxS4TPtXa1Lh9SiUyv93F4=
github.com/DataDog/datadog-go/v5 v5.5.0 h1:G5KHeB8pWBNXT4Jtw0zAkhdxEAWSpWH00geHI6LDrKU=
github.com/DataDog/datadog-go/v5 v5.5.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw=
github.com/DataDog/go-libddwaf/v3 v3.4.0 h1:NJ2W2vhYaOm1OWr1LJCbdgp7ezG/XLJcQKBmjFwhSuM=
github.com/DataDog/go-libddwaf/v3 v3.4.0/go.mod h1:n98d9nZ1gzenRSk53wz8l6d34ikxS+hs62A31Fqmyi4=
github.com/DataDog/go-sqllexer v0.0.14 h1:xUQh2tLr/95LGxDzLmttLgTo/1gzFeOyuwrQa/Iig4Q=
github.com/DataDog/go-sqllexer v0.0.14/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc=
github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4=
github.com/DataDog/go-tuf v1.1.0-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
@@ -76,11 +78,15 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/aquasecurity/trivy-iac v0.8.0 h1:NKFhk/BTwQ0jIh4t74V8+6UIGUvPlaxO9HPlSMQi3fo=
github.com/aquasecurity/trivy-iac v0.8.0/go.mod h1:ARiMeNqcaVWOXJmp8hmtMnNm/Jd836IOmDBUW5r4KEk=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
@@ -157,14 +163,14 @@ github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk=
github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -189,10 +195,6 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAM
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df h1:cbtSn19AtqQha1cxmP2Qvgd3fFMz51AeAEKLJMyEUhc=
github.com/chromedp/cdproto v0.0.0-20241003230502-a4a8f7c660df/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.11.0 h1:1PT6O4g39sBAFjlljIHTpxmCSk8meeYL6+R+oXH4bWA=
@@ -229,10 +231,12 @@ github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM=
github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q=
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuOD6a/zVP3rcxezNsoDseTUw=
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ=
github.com/coder/tailscale v1.1.1-0.20241003034647-02286e537fc2 h1:mBbPFyJ2i9o490IwWGvWgtG0qmvIk45R7GWJpoaXotI=
github.com/coder/tailscale v1.1.1-0.20241003034647-02286e537fc2/go.mod h1:rp6BIJxCp127/hvvDWNkHC9MxAlKvQfoOtBr8s5sCqo=
github.com/coder/tailscale v1.1.1-0.20241218201526-b53d914d625f h1:CctU+8mmHp/Y/cteK/bMJCUfe7c6gDIy3TJGaHaxrbU=
github.com/coder/tailscale v1.1.1-0.20241218201526-b53d914d625f/go.mod h1:LOne094of6xzi3PdF+WyhPvKjK5zVuGADQ8WP46iIrM=
github.com/coder/terraform-provider-coder v1.0.2 h1:xKbnJF/XUxcUJlZoC3ZkNOj4PZvk5Stdkel2TCZluDQ=
github.com/coder/terraform-provider-coder v1.0.2/go.mod h1:1f3EjO+DA9QcIbM7sBSk/Ffw3u7kh6vXNBIQfV59yUk=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Auuw3YOiSyLNHkdMtyCZHPFBx7syN4rk=
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0/go.mod h1:qANbdpqyAGlo2bg+4gQKPj24H1ZWa3bQU2Q5/bV5B3Y=
github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818 h1:bNhUTaKl3q0bFn78bBRq7iIwo72kNTvUD9Ll5TTzDDk=
@@ -338,11 +342,6 @@ github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mN
github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
@@ -390,16 +389,12 @@ github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicb
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@@ -415,17 +410,12 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY=
@@ -460,8 +450,6 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -499,7 +487,6 @@ github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5p
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -523,9 +510,6 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hairyhenderson/go-codeowners v0.6.1 h1:2OLPpLWFMxkCf9hkYzOexnCGD+kj853OqeoKq7S+9us=
@@ -625,9 +609,6 @@ github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA=
github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -636,11 +617,8 @@ github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNq
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -668,7 +646,6 @@ github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -742,12 +719,6 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/mocktools/go-smtp-mock/v2 v2.4.0 h1:u0ky0iyNW/LEMKAFRTsDivHyP8dHYxe/cV3FZC3rRjo=
github.com/mocktools/go-smtp-mock/v2 v2.4.0/go.mod h1:h9AOf/IXLSU2m/1u4zsjtOM/WddPwdOUBz56dV9f81M=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -932,19 +903,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.2.1 h1:6ypy2qcCznxpP4hpORzhtXyTqrBs7cfM9MCCWY8zsmU=
github.com/tinylib/msgp v1.2.1/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a h1:eg5FkNoQp76ZsswyGZ+TjYqA/rhKefxK8BW7XOlQsxo=
github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a/go.mod h1:e/8TmrdreH0sZOw2DFKBaUV7bvDWRq6SeM9PzkuVM68=
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og=
github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -997,6 +961,8 @@ github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ
github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
github.com/zclconf/go-cty-yaml v1.0.3 h1:og/eOQ7lvA/WWhHGFETVWNduJM7Rjsv2RRpx1sdFMLc=
github.com/zclconf/go-cty-yaml v1.0.3/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
@@ -1046,8 +1012,6 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE=
go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -1058,8 +1022,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
@@ -1093,8 +1057,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -1106,8 +1070,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1149,8 +1113,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1158,8 +1122,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1170,9 +1134,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1286,8 +1249,6 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
+1 -1
View File
@@ -26,7 +26,7 @@ func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-
return provisionersdk.ParseErrorf("load module: %s", formatDiagnostics(sess.WorkDirectory, diags))
}
workspaceTags, err := parser.WorkspaceTags(ctx)
workspaceTags, _, err := parser.WorkspaceTags(ctx)
if err != nil {
return provisionersdk.ParseErrorf("can't load workspace tags: %v", err)
}

Some files were not shown because too many files have changed in this diff Show More