mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: allow users to pause prebuilt workspace reconciliation (#18700)
This PR provides two commands: * `coder prebuilds pause` * `coder prebuilds resume` These allow the suspension of all prebuilds activity, intended for use if prebuilds are misbehaving.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/serpent"
|
||||
|
||||
"github.com/coder/coder/v2/cli"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func (r *RootCmd) prebuilds() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "prebuilds",
|
||||
Short: "Manage Coder prebuilds",
|
||||
Long: "Administrators can use these commands to manage prebuilt workspace settings.\n" + cli.FormatExamples(
|
||||
cli.Example{
|
||||
Description: "Pause Coder prebuilt workspace reconciliation.",
|
||||
Command: "coder prebuilds pause",
|
||||
},
|
||||
cli.Example{
|
||||
Description: "Resume Coder prebuilt workspace reconciliation if it has been paused.",
|
||||
Command: "coder prebuilds resume",
|
||||
},
|
||||
),
|
||||
Aliases: []string{"prebuild"},
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
return inv.Command.HelpHandler(inv)
|
||||
},
|
||||
Children: []*serpent.Command{
|
||||
r.pausePrebuilds(),
|
||||
r.resumePrebuilds(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) pausePrebuilds() *serpent.Command {
|
||||
client := new(codersdk.Client)
|
||||
cmd := &serpent.Command{
|
||||
Use: "pause",
|
||||
Short: "Pause prebuilds",
|
||||
Middleware: serpent.Chain(
|
||||
serpent.RequireNArgs(0),
|
||||
r.InitClient(client),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
err := client.PutPrebuildsSettings(inv.Context(), codersdk.PrebuildsSettings{
|
||||
ReconciliationPaused: true,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("unable to pause prebuilds: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Prebuilds are now paused.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (r *RootCmd) resumePrebuilds() *serpent.Command {
|
||||
client := new(codersdk.Client)
|
||||
cmd := &serpent.Command{
|
||||
Use: "resume",
|
||||
Short: "Resume prebuilds",
|
||||
Middleware: serpent.Chain(
|
||||
serpent.RequireNArgs(0),
|
||||
r.InitClient(client),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
err := client.PutPrebuildsSettings(inv.Context(), codersdk.PrebuildsSettings{
|
||||
ReconciliationPaused: false,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("unable to resume prebuilds: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Prebuilds are now resumed.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
)
|
||||
|
||||
func TestPrebuildsPause(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
inv, conf := newCLI(t, "prebuilds", "pause")
|
||||
var buf bytes.Buffer
|
||||
inv.Stderr = &buf
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the output message
|
||||
assert.Contains(t, buf.String(), "Prebuilds are now paused.")
|
||||
|
||||
// Verify the settings were actually updated
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
settings, err := client.GetPrebuildsSettings(inv.Context())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, settings.ReconciliationPaused)
|
||||
})
|
||||
|
||||
t.Run("UnauthorizedUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient, admin := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Create a regular user without admin privileges
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID)
|
||||
|
||||
inv, conf := newCLI(t, "prebuilds", "pause")
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.Error(t, err)
|
||||
var sdkError *codersdk.Error
|
||||
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoLicense", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
DontAddLicense: true,
|
||||
})
|
||||
|
||||
inv, conf := newCLI(t, "prebuilds", "pause")
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.Error(t, err)
|
||||
// Should fail without license
|
||||
var sdkError *codersdk.Error
|
||||
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("AlreadyPaused", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// First pause
|
||||
inv1, conf := newCLI(t, "prebuilds", "pause")
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
err := inv1.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try to pause again
|
||||
inv2, conf2 := newCLI(t, "prebuilds", "pause")
|
||||
clitest.SetupConfig(t, client, conf2)
|
||||
err = inv2.Run()
|
||||
require.NoError(t, err) // Should succeed even if already paused
|
||||
|
||||
// Verify still paused
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
settings, err := client.GetPrebuildsSettings(inv2.Context())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, settings.ReconciliationPaused)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrebuildsResume(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// First pause prebuilds
|
||||
inv1, conf := newCLI(t, "prebuilds", "pause")
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
err := inv1.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then resume
|
||||
inv2, conf2 := newCLI(t, "prebuilds", "resume")
|
||||
var buf bytes.Buffer
|
||||
inv2.Stderr = &buf
|
||||
clitest.SetupConfig(t, client, conf2)
|
||||
|
||||
err = inv2.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the output message
|
||||
assert.Contains(t, buf.String(), "Prebuilds are now resumed.")
|
||||
|
||||
// Verify the settings were actually updated
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
settings, err := client.GetPrebuildsSettings(inv2.Context())
|
||||
require.NoError(t, err)
|
||||
assert.False(t, settings.ReconciliationPaused)
|
||||
})
|
||||
|
||||
t.Run("ResumeWhenNotPaused", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Resume without first pausing
|
||||
inv, conf := newCLI(t, "prebuilds", "resume")
|
||||
var buf bytes.Buffer
|
||||
inv.Stderr = &buf
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should succeed and show the message
|
||||
assert.Contains(t, buf.String(), "Prebuilds are now resumed.")
|
||||
|
||||
// Verify still not paused
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
settings, err := client.GetPrebuildsSettings(inv.Context())
|
||||
require.NoError(t, err)
|
||||
assert.False(t, settings.ReconciliationPaused)
|
||||
})
|
||||
|
||||
t.Run("UnauthorizedUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient, admin := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Create a regular user without admin privileges
|
||||
client, _ := coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID)
|
||||
|
||||
inv, conf := newCLI(t, "prebuilds", "resume")
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.Error(t, err)
|
||||
var sdkError *codersdk.Error
|
||||
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoLicense", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
DontAddLicense: true,
|
||||
})
|
||||
|
||||
inv, conf := newCLI(t, "prebuilds", "resume")
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.Error(t, err)
|
||||
// Should fail without license
|
||||
var sdkError *codersdk.Error
|
||||
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
|
||||
assert.Equal(t, http.StatusForbidden, sdkError.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrebuildsCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Help", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
inv, conf := newCLI(t, "prebuilds", "--help")
|
||||
var buf bytes.Buffer
|
||||
inv.Stdout = &buf
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify help output contains expected information
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Manage Coder prebuilds")
|
||||
assert.Contains(t, output, "pause")
|
||||
assert.Contains(t, output, "resume")
|
||||
assert.Contains(t, output, "Administrators can use these commands")
|
||||
})
|
||||
|
||||
t.Run("NoSubcommand", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
inv, conf := newCLI(t, "prebuilds")
|
||||
var buf bytes.Buffer
|
||||
inv.Stdout = &buf
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should show help when no subcommand is provided
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Manage Coder prebuilds")
|
||||
assert.Contains(t, output, "pause")
|
||||
assert.Contains(t, output, "resume")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrebuildsSettingsAPI(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("GetSettings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Get initial settings
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
settings, err := client.GetPrebuildsSettings(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.False(t, settings.ReconciliationPaused)
|
||||
|
||||
// Pause prebuilds
|
||||
inv1, conf := newCLI(t, "prebuilds", "pause")
|
||||
//nolint:gocritic // Only owners can change deployment settings
|
||||
clitest.SetupConfig(t, client, conf)
|
||||
err = inv1.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get settings again
|
||||
settings, err = client.GetPrebuildsSettings(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, settings.ReconciliationPaused)
|
||||
|
||||
// Resume prebuilds
|
||||
inv2, conf2 := newCLI(t, "prebuilds", "resume")
|
||||
clitest.SetupConfig(t, client, conf2)
|
||||
err = inv2.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get settings one more time
|
||||
settings, err = client.GetPrebuildsSettings(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.False(t, settings.ReconciliationPaused)
|
||||
})
|
||||
}
|
||||
@@ -16,6 +16,7 @@ func (r *RootCmd) enterpriseOnly() []*serpent.Command {
|
||||
r.features(),
|
||||
r.licenses(),
|
||||
r.groups(),
|
||||
r.prebuilds(),
|
||||
r.provisionerDaemons(),
|
||||
r.provisionerd(),
|
||||
}
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ SUBCOMMANDS:
|
||||
features List Enterprise features
|
||||
groups Manage groups
|
||||
licenses Add, delete, and list licenses
|
||||
prebuilds Manage Coder prebuilds
|
||||
provisioner View and manage provisioner daemons and jobs
|
||||
server Start a Coder server
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder prebuilds
|
||||
|
||||
Manage Coder prebuilds
|
||||
|
||||
Aliases: prebuild
|
||||
|
||||
Administrators can use these commands to manage prebuilt workspace settings.
|
||||
- Pause Coder prebuilt workspace reconciliation.:
|
||||
|
||||
$ coder prebuilds pause
|
||||
|
||||
- Resume Coder prebuilt workspace reconciliation if it has been paused.:
|
||||
|
||||
$ coder prebuilds resume
|
||||
|
||||
SUBCOMMANDS:
|
||||
pause Pause prebuilds
|
||||
resume Resume prebuilds
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
@@ -0,0 +1,9 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder prebuilds pause
|
||||
|
||||
Pause prebuilds
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
@@ -0,0 +1,9 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder prebuilds resume
|
||||
|
||||
Resume prebuilds
|
||||
|
||||
———
|
||||
Run `coder --help` for a list of global options.
|
||||
Reference in New Issue
Block a user