Merge branch 'coder:main' into feat/coder-login-secret

This commit is contained in:
Michael Brewer 2024-05-01 00:56:03 -07:00 committed by GitHub
commit 3b8dbc04fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 717 additions and 171 deletions

View File

@ -142,7 +142,7 @@ jobs:
# Check for any typos
- name: Check for typos
uses: crate-ci/typos@v1.20.9
uses: crate-ci/typos@v1.20.10
with:
config: .github/workflows/typos.toml
@ -909,7 +909,8 @@ jobs:
uses: actions/checkout@v4
- name: "Dependency Review"
id: review
uses: actions/dependency-review-action@v4
# TODO: Replace this with the latest release once https://github.com/actions/dependency-review-action/pull/761 is merged.
uses: actions/dependency-review-action@49fbbe0acb033b7824f26d00b005d7d598d76301
with:
allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, CC0-1.0, ISC, MIT, MIT-0, MPL-2.0
allow-dependencies-licenses: "pkg:golang/github.com/pelletier/go-toml/v2"

View File

@ -17,6 +17,9 @@
},
{
"pattern": "tailscale.com"
},
{
"pattern": "wireguard.com"
}
],
"aliveStatusCodes": [200, 0]

View File

@ -128,6 +128,13 @@ jobs:
- name: Setup Node
uses: ./.github/actions/setup-node
# Necessary for signing Windows binaries.
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "11.0"
- name: Install nsis and zstd
run: sudo apt-get install -y nsis zstd
@ -161,10 +168,32 @@ jobs:
AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }}
- name: Setup Windows EV Signing Certificate
run: |
set -euo pipefail
touch /tmp/ev_cert.pem
chmod 600 /tmp/ev_cert.pem
echo "$EV_SIGNING_CERT" > /tmp/ev_cert.pem
wget https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O /tmp/jsign-6.0.jar
env:
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
- name: Test migrations from current ref to main
run: |
make test-migrations
# Setup GCloud for signing Windows binaries.
- name: Authenticate to Google Cloud
id: gcloud_auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }}
token_format: "access_token"
- name: Setup GCloud SDK
uses: "google-github-actions/setup-gcloud@v2"
- name: Build binaries
run: |
set -euo pipefail
@ -179,16 +208,26 @@ jobs:
build/coder_helm_"$version".tgz \
build/provisioner_helm_"$version".tgz
env:
CODER_SIGN_WINDOWS: "1"
CODER_SIGN_DARWIN: "1"
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
AC_APIKEY_ISSUER_ID: ${{ secrets.AC_APIKEY_ISSUER_ID }}
AC_APIKEY_ID: ${{ secrets.AC_APIKEY_ID }}
AC_APIKEY_FILE: /tmp/apple_apikey.p8
EV_KEY: ${{ secrets.EV_KEY }}
EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }}
EV_TSA_URL: ${{ secrets.EV_TSA_URL }}
EV_CERTIFICATE_PATH: /tmp/ev_cert.pem
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
JSIGN_PATH: /tmp/jsign-6.0.jar
- name: Delete Apple Developer certificate and API key
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
- name: Delete Windows EV Signing Cert
run: rm /tmp/ev_cert.pem
- name: Determine base image tag
id: image-base-tag
run: |

View File

@ -10,7 +10,7 @@ import (
"github.com/coder/serpent"
)
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter, defaultOverrides map[string]string) (string, error) {
label := templateVersionParameter.Name
if templateVersionParameter.DisplayName != "" {
label = templateVersionParameter.DisplayName
@ -26,6 +26,11 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
}
defaultValue := templateVersionParameter.DefaultValue
if v, ok := defaultOverrides[templateVersionParameter.Name]; ok {
defaultValue = v
}
var err error
var value string
if templateVersionParameter.Type == "list(string)" {
@ -58,7 +63,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
var richParameterOption *codersdk.TemplateVersionParameterOption
richParameterOption, err = RichSelect(inv, RichSelectOptions{
Options: templateVersionParameter.Options,
Default: templateVersionParameter.DefaultValue,
Default: defaultValue,
HideSearch: true,
})
if err == nil {
@ -69,7 +74,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
} else {
text := "Enter a value"
if !templateVersionParameter.Required {
text += fmt.Sprintf(" (default: %q)", templateVersionParameter.DefaultValue)
text += fmt.Sprintf(" (default: %q)", defaultValue)
}
text += ":"
@ -87,7 +92,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
// If they didn't specify anything, use the default value if set.
if len(templateVersionParameter.Options) == 0 && value == "" {
value = templateVersionParameter.DefaultValue
value = defaultValue
}
return value, nil

View File

@ -165,6 +165,11 @@ func (r *RootCmd) create() *serpent.Command {
return xerrors.Errorf("can't parse given parameter values: %w", err)
}
cliBuildParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
if err != nil {
return xerrors.Errorf("can't parse given parameter defaults: %w", err)
}
var sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
if copyParametersFrom != "" {
sourceWorkspaceParameters, err = client.WorkspaceBuildParameters(inv.Context(), sourceWorkspace.LatestBuild.ID)
@ -178,8 +183,9 @@ func (r *RootCmd) create() *serpent.Command {
TemplateVersionID: templateVersionID,
NewWorkspaceName: workspaceName,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliBuildParameters,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliBuildParameters,
RichParameterDefaults: cliBuildParameterDefaults,
SourceWorkspaceParameters: sourceWorkspaceParameters,
})
@ -262,6 +268,7 @@ func (r *RootCmd) create() *serpent.Command {
cliui.SkipPromptOption(),
)
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...)
return cmd
}
@ -276,9 +283,10 @@ type prepWorkspaceBuildArgs struct {
PromptBuildOptions bool
BuildOptions []codersdk.WorkspaceBuildParameter
PromptRichParameters bool
RichParameters []codersdk.WorkspaceBuildParameter
RichParameterFile string
PromptRichParameters bool
RichParameters []codersdk.WorkspaceBuildParameter
RichParameterFile string
RichParameterDefaults []codersdk.WorkspaceBuildParameter
}
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
@ -311,7 +319,8 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
WithBuildOptions(args.BuildOptions).
WithPromptRichParameters(args.PromptRichParameters).
WithRichParameters(args.RichParameters).
WithRichParametersFile(parameterFile)
WithRichParametersFile(parameterFile).
WithRichParametersDefaults(args.RichParameterDefaults)
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
if err != nil {
return nil, err

View File

@ -315,6 +315,68 @@ func TestCreateWithRichParameters(t *testing.T) {
<-doneChan
})
t.Run("ParametersDefaults", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
"--parameter-default", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
"--parameter-default", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
"--parameter-default", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []string{
firstParameterDescription, firstParameterValue,
secondParameterDescription, secondParameterValue,
immutableParameterDescription, immutableParameterValue,
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
defaultValue := matches[i+1]
pty.ExpectMatch(match)
pty.ExpectMatch(`Enter a value (default: "` + defaultValue + `")`)
pty.WriteLine("")
}
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
<-doneChan
// Verify that the expected default values were used.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: "my-workspace",
})
require.NoError(t, err, "can't list available workspaces")
require.Len(t, workspaces.Workspaces, 1)
workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
require.NoError(t, err)
require.Len(t, buildParameters, 3)
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
})
t.Run("RichParametersFile", func(t *testing.T) {
t.Parallel()

View File

@ -18,14 +18,16 @@ type workspaceParameterFlags struct {
promptBuildOptions bool
buildOptions []string
richParameterFile string
richParameters []string
richParameterFile string
richParameters []string
richParameterDefaults []string
promptRichParameters bool
}
func (wpf *workspaceParameterFlags) allOptions() []serpent.Option {
options := append(wpf.cliBuildOptions(), wpf.cliParameters()...)
options = append(options, wpf.cliParameterDefaults()...)
return append(options, wpf.alwaysPrompt())
}
@ -62,6 +64,17 @@ func (wpf *workspaceParameterFlags) cliParameters() []serpent.Option {
}
}
func (wpf *workspaceParameterFlags) cliParameterDefaults() []serpent.Option {
return serpent.OptionSet{
serpent.Option{
Flag: "parameter-default",
Env: "CODER_RICH_PARAMETER_DEFAULT",
Description: `Rich parameter default values in the format "name=value".`,
Value: serpent.StringArrayOf(&wpf.richParameterDefaults),
},
}
}
func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
return serpent.Option{
Flag: "always-prompt",

View File

@ -26,9 +26,10 @@ type ParameterResolver struct {
lastBuildParameters []codersdk.WorkspaceBuildParameter
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
richParameters []codersdk.WorkspaceBuildParameter
richParametersFile map[string]string
buildOptions []codersdk.WorkspaceBuildParameter
richParameters []codersdk.WorkspaceBuildParameter
richParametersDefaults map[string]string
richParametersFile map[string]string
buildOptions []codersdk.WorkspaceBuildParameter
promptRichParameters bool
promptBuildOptions bool
@ -59,6 +60,16 @@ func (pr *ParameterResolver) WithRichParametersFile(fileMap map[string]string) *
return pr
}
func (pr *ParameterResolver) WithRichParametersDefaults(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
if pr.richParametersDefaults == nil {
pr.richParametersDefaults = make(map[string]string)
}
for _, p := range params {
pr.richParametersDefaults[p.Name] = p.Value
}
return pr
}
func (pr *ParameterResolver) WithPromptRichParameters(promptRichParameters bool) *ParameterResolver {
pr.promptRichParameters = promptRichParameters
return pr
@ -227,7 +238,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
parameterValue, err := cliui.RichParameter(inv, tvp)
parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
if err != nil {
return nil, err
}

View File

@ -99,7 +99,12 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse build options: %w", err)
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameters: %w", err)
}
cliRichParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameter defaults: %w", err)
}
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
@ -108,11 +113,12 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
NewWorkspaceName: workspace.Name,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: parameterFlags.promptRichParameters,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: parameterFlags.promptRichParameters,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
RichParameterDefaults: cliRichParameterDefaults,
})
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, err

View File

@ -20,6 +20,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

View File

@ -19,6 +19,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

View File

@ -19,6 +19,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

View File

@ -21,6 +21,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

4
coderd/apidoc/docs.go generated
View File

@ -8541,6 +8541,10 @@ const docTemplate = `{
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
"type": "string"
},
"deployment_id": {
"description": "DeploymentID is the unique identifier for this deployment.",
"type": "string"
},
"external_url": {
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"

View File

@ -7599,6 +7599,10 @@
"description": "DashboardURL is the URL to hit the deployment's dashboard.\nFor external workspace proxies, this is the coderd they are connected\nto.",
"type": "string"
},
"deployment_id": {
"description": "DeploymentID is the unique identifier for this deployment.",
"type": "string"
},
"external_url": {
"description": "ExternalURL references the current Coder version.\nFor production builds, this will link directly to a release. For development builds, this will link to a commit.",
"type": "string"

View File

@ -64,7 +64,7 @@ func TestValidate(t *testing.T) {
func TestExpiresSoon(t *testing.T) {
t.Parallel()
const threshold = 2
const threshold = 1
for _, c := range azureidentity.Certificates {
block, rest := pem.Decode([]byte(c))

View File

@ -436,6 +436,15 @@ func New(options *Options) *API {
api.AppearanceFetcher.Store(&appearance.DefaultFetcher)
api.PortSharer.Store(&portsharing.DefaultPortSharer)
buildInfo := codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
Version: buildinfo.Version(),
AgentAPIVersion: AgentAPIVersionREST,
DashboardURL: api.AccessURL.String(),
WorkspaceProxy: false,
UpgradeMessage: api.DeploymentValues.CLIUpgradeMessage.String(),
DeploymentID: api.DeploymentID,
}
api.SiteHandler = site.New(&site.Options{
BinFS: binFS,
BinHashes: binHashes,
@ -444,6 +453,7 @@ func New(options *Options) *API {
OAuth2Configs: oauthConfigs,
DocsURL: options.DeploymentValues.DocsURL.String(),
AppearanceFetcher: &api.AppearanceFetcher,
BuildInfo: buildInfo,
})
api.SiteHandler.Experiments.Store(&experiments)
@ -735,7 +745,7 @@ func New(options *Options) *API {
// All CSP errors will be logged
r.Post("/csp/reports", api.logReportCSPViolations)
r.Get("/buildinfo", buildInfo(api.AccessURL, api.DeploymentValues.CLIUpgradeMessage.String()))
r.Get("/buildinfo", buildInfoHandler(buildInfo))
// /regions is overridden in the enterprise version
r.Group(func(r chi.Router) {
r.Use(apiKeyMiddleware)

View File

@ -2,9 +2,7 @@ package coderd
import (
"net/http"
"net/url"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
@ -68,16 +66,10 @@ func (api *API) deploymentStats(rw http.ResponseWriter, r *http.Request) {
// @Tags General
// @Success 200 {object} codersdk.BuildInfoResponse
// @Router /buildinfo [get]
func buildInfo(accessURL *url.URL, upgradeMessage string) http.HandlerFunc {
func buildInfoHandler(resp codersdk.BuildInfoResponse) http.HandlerFunc {
// This is in a handler so that we can generate API docs info.
return func(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
Version: buildinfo.Version(),
AgentAPIVersion: AgentAPIVersionREST,
DashboardURL: accessURL.String(),
WorkspaceProxy: false,
UpgradeMessage: upgradeMessage,
})
httpapi.Write(r.Context(), rw, http.StatusOK, resp)
}
}

View File

@ -563,6 +563,9 @@ func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
// Dynamic defaults
switch codersdk.EnhancedExternalAuthProvider(config.Type) {
case codersdk.EnhancedExternalAuthProviderGitLab:
copyDefaultSettings(config, gitlabDefaults(config))
return
case codersdk.EnhancedExternalAuthProviderBitBucketServer:
copyDefaultSettings(config, bitbucketServerDefaults(config))
return
@ -667,6 +670,44 @@ func bitbucketServerDefaults(config *codersdk.ExternalAuthConfig) codersdk.Exter
return defaults
}
// gitlabDefaults returns a static config if using the gitlab cloud offering.
// The values are dynamic if using a self-hosted gitlab.
// When the decision is not obvious, just defer to the cloud defaults.
// Any user specific fields will override this if provided.
func gitlabDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig {
cloud := codersdk.ExternalAuthConfig{
AuthURL: "https://gitlab.com/oauth/authorize",
TokenURL: "https://gitlab.com/oauth/token",
ValidateURL: "https://gitlab.com/oauth/token/info",
DisplayName: "GitLab",
DisplayIcon: "/icon/gitlab.svg",
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
Scopes: []string{"write_repository"},
}
if config.AuthURL == "" || config.AuthURL == cloud.AuthURL {
return cloud
}
au, err := url.Parse(config.AuthURL)
if err != nil || au.Host == "gitlab.com" {
// If the AuthURL is not a valid URL or is using the cloud,
// use the cloud static defaults.
return cloud
}
// At this point, assume it is self-hosted and use the AuthURL
return codersdk.ExternalAuthConfig{
DisplayName: cloud.DisplayName,
Scopes: cloud.Scopes,
DisplayIcon: cloud.DisplayIcon,
AuthURL: au.ResolveReference(&url.URL{Path: "/oauth/authorize"}).String(),
TokenURL: au.ResolveReference(&url.URL{Path: "/oauth/token"}).String(),
ValidateURL: au.ResolveReference(&url.URL{Path: "/oauth/token/info"}).String(),
Regex: fmt.Sprintf(`^(https?://)?%s(/.*)?$`, strings.ReplaceAll(au.Host, ".", `\.`)),
}
}
func jfrogArtifactoryDefaults(config *codersdk.ExternalAuthConfig) codersdk.ExternalAuthConfig {
defaults := codersdk.ExternalAuthConfig{
DisplayName: "JFrog Artifactory",
@ -789,15 +830,6 @@ var staticDefaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.External
Regex: `^(https?://)?bitbucket\.org(/.*)?$`,
Scopes: []string{"account", "repository:write"},
},
codersdk.EnhancedExternalAuthProviderGitLab: {
AuthURL: "https://gitlab.com/oauth/authorize",
TokenURL: "https://gitlab.com/oauth/token",
ValidateURL: "https://gitlab.com/oauth/token/info",
DisplayName: "GitLab",
DisplayIcon: "/icon/gitlab.svg",
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
Scopes: []string{"write_repository"},
},
codersdk.EnhancedExternalAuthProviderGitHub: {
AuthURL: xgithub.Endpoint.AuthURL,
TokenURL: xgithub.Endpoint.TokenURL,

View File

@ -8,6 +8,112 @@ import (
"github.com/coder/coder/v2/codersdk"
)
func TestGitlabDefaults(t *testing.T) {
t.Parallel()
// The default cloud setup. Copying this here as hard coded
// values.
cloud := codersdk.ExternalAuthConfig{
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
ID: string(codersdk.EnhancedExternalAuthProviderGitLab),
AuthURL: "https://gitlab.com/oauth/authorize",
TokenURL: "https://gitlab.com/oauth/token",
ValidateURL: "https://gitlab.com/oauth/token/info",
DisplayName: "GitLab",
DisplayIcon: "/icon/gitlab.svg",
Regex: `^(https?://)?gitlab\.com(/.*)?$`,
Scopes: []string{"write_repository"},
}
tests := []struct {
name string
input codersdk.ExternalAuthConfig
expected codersdk.ExternalAuthConfig
mutateExpected func(*codersdk.ExternalAuthConfig)
}{
// Cloud
{
name: "OnlyType",
input: codersdk.ExternalAuthConfig{
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
},
expected: cloud,
},
{
// If someone was to manually configure the gitlab cli.
name: "CloudByConfig",
input: codersdk.ExternalAuthConfig{
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
AuthURL: "https://gitlab.com/oauth/authorize",
},
expected: cloud,
},
{
// Changing some of the defaults of the cloud option
name: "CloudWithChanges",
input: codersdk.ExternalAuthConfig{
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
// Adding an extra query param intentionally to break simple
// string comparisons.
AuthURL: "https://gitlab.com/oauth/authorize?foo=bar",
DisplayName: "custom",
Regex: ".*",
},
expected: cloud,
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
config.AuthURL = "https://gitlab.com/oauth/authorize?foo=bar"
config.DisplayName = "custom"
config.Regex = ".*"
},
},
// Self-hosted
{
// Dynamically figures out the Validate, Token, and Regex fields.
name: "SelfHostedOnlyAuthURL",
input: codersdk.ExternalAuthConfig{
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
AuthURL: "https://gitlab.company.org/oauth/authorize?foo=bar",
},
expected: cloud,
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
config.AuthURL = "https://gitlab.company.org/oauth/authorize?foo=bar"
config.ValidateURL = "https://gitlab.company.org/oauth/token/info"
config.TokenURL = "https://gitlab.company.org/oauth/token"
config.Regex = `^(https?://)?gitlab\.company\.org(/.*)?$`
},
},
{
// Strange values
name: "RandomValues",
input: codersdk.ExternalAuthConfig{
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
AuthURL: "https://auth.com/auth",
ValidateURL: "https://validate.com/validate",
TokenURL: "https://token.com/token",
Regex: "random",
},
expected: cloud,
mutateExpected: func(config *codersdk.ExternalAuthConfig) {
config.AuthURL = "https://auth.com/auth"
config.ValidateURL = "https://validate.com/validate"
config.TokenURL = "https://token.com/token"
config.Regex = `random`
},
},
}
for _, c := range tests {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
applyDefaultsToConfig(&c.input)
if c.mutateExpected != nil {
c.mutateExpected(&c.expected)
}
require.Equal(t, c.input, c.expected)
})
}
}
func Test_bitbucketServerConfigDefaults(t *testing.T) {
t.Parallel()

View File

@ -2151,6 +2151,9 @@ type BuildInfoResponse struct {
// UpgradeMessage is the message displayed to users when an outdated client
// is detected.
UpgradeMessage string `json:"upgrade_message"`
// DeploymentID is the unique identifier for this deployment.
DeploymentID string `json:"deployment_id"`
}
type WorkspaceProxyBuildInfo struct {

1
docs/api/general.md generated
View File

@ -55,6 +55,7 @@ curl -X GET http://coder-server:8080/api/v2/buildinfo \
{
"agent_api_version": "string",
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"upgrade_message": "string",
"version": "string",

2
docs/api/schemas.md generated
View File

@ -1178,6 +1178,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
{
"agent_api_version": "string",
"dashboard_url": "string",
"deployment_id": "string",
"external_url": "string",
"upgrade_message": "string",
"version": "string",
@ -1191,6 +1192,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| ------------------- | ------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_api_version` | string | false | | Agent api version is the current version of the Agent API (back versions MAY still be supported). |
| `dashboard_url` | string | false | | Dashboard URL is the URL to hit the deployment's dashboard. For external workspace proxies, this is the coderd they are connected to. |
| `deployment_id` | string | false | | Deployment ID is the unique identifier for this deployment. |
| `external_url` | string | false | | External URL references the current Coder version. For production builds, this will link directly to a release. For development builds, this will link to a commit. |
| `upgrade_message` | string | false | | Upgrade message is the message displayed to users when an outdated client is detected. |
| `version` | string | false | | Version returns the semantic version of the build. |

9
docs/cli/create.md generated
View File

@ -91,3 +91,12 @@ Rich parameter value in the format "name=value".
| Environment | <code>$CODER_RICH_PARAMETER_FILE</code> |
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".

9
docs/cli/restart.md generated
View File

@ -55,6 +55,15 @@ Rich parameter value in the format "name=value".
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".
### --always-prompt
| | |

9
docs/cli/start.md generated
View File

@ -55,6 +55,15 @@ Rich parameter value in the format "name=value".
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".
### --always-prompt
| | |

9
docs/cli/update.md generated
View File

@ -53,6 +53,15 @@ Rich parameter value in the format "name=value".
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".
### --always-prompt
| | |

View File

@ -157,7 +157,6 @@ resource "coder_agent" "dev" {
os = "linux"
dir = local.repo_dir
env = {
GITHUB_TOKEN : data.coder_external_auth.github.access_token,
OIDC_TOKEN : data.coder_workspace.me.owner_oidc_access_token,
}
startup_script_behavior = "blocking"

View File

@ -147,13 +147,14 @@ func NewWithAPI(t *testing.T, options *Options) (
}
type LicenseOptions struct {
AccountType string
AccountID string
Trial bool
AllFeatures bool
GraceAt time.Time
ExpiresAt time.Time
Features license.Features
AccountType string
AccountID string
DeploymentIDs []string
Trial bool
AllFeatures bool
GraceAt time.Time
ExpiresAt time.Time
Features license.Features
}
// AddFullLicense generates a license with all features enabled.
@ -190,6 +191,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
LicenseExpires: jwt.NewNumericDate(options.GraceAt),
AccountType: options.AccountType,
AccountID: options.AccountID,
DeploymentIDs: options.DeploymentIDs,
Trial: options.Trial,
Version: license.CurrentVersion,
AllFeatures: options.AllFeatures,

View File

@ -257,14 +257,16 @@ type Claims struct {
// the end of the grace period (identical to LicenseExpires if there is no grace period).
// The reason we use the standard claim for the end of the grace period is that we want JWT
// processing libraries to consider the token "valid" until then.
LicenseExpires *jwt.NumericDate `json:"license_expires,omitempty"`
AccountType string `json:"account_type,omitempty"`
AccountID string `json:"account_id,omitempty"`
Trial bool `json:"trial"`
AllFeatures bool `json:"all_features"`
Version uint64 `json:"version"`
Features Features `json:"features"`
RequireTelemetry bool `json:"require_telemetry,omitempty"`
LicenseExpires *jwt.NumericDate `json:"license_expires,omitempty"`
AccountType string `json:"account_type,omitempty"`
AccountID string `json:"account_id,omitempty"`
// DeploymentIDs enforces the license can only be used on a set of deployments.
DeploymentIDs []string `json:"deployment_ids,omitempty"`
Trial bool `json:"trial"`
AllFeatures bool `json:"all_features"`
Version uint64 `json:"version"`
Features Features `json:"features"`
RequireTelemetry bool `json:"require_telemetry,omitempty"`
}
// ParseRaw consumes a license and returns the claims.

View File

@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
@ -120,6 +121,15 @@ func (api *API) postLicense(rw http.ResponseWriter, r *http.Request) {
// old licenses with a uuid.
id = uuid.New()
}
if len(claims.DeploymentIDs) > 0 && !slices.Contains(claims.DeploymentIDs, api.AGPL.DeploymentID) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "License cannot be used on this deployment!",
Detail: fmt.Sprintf("The provided license is locked to the following deployments: %q. "+
"Your deployment identifier is %q. Please contact sales.", claims.DeploymentIDs, api.AGPL.DeploymentID),
})
return
}
dl, err := api.Database.InsertLicense(ctx, database.InsertLicenseParams{
UploadedAt: dbtime.Now(),
JWT: addLicense.License,

View File

@ -5,6 +5,7 @@ import (
"net/http"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
@ -36,6 +37,22 @@ func TestPostLicense(t *testing.T) {
assert.EqualValues(t, 1, features[codersdk.FeatureAuditLog])
})
t.Run("InvalidDeploymentID", func(t *testing.T) {
t.Parallel()
// The generated deployment will start out with a different deployment ID.
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})
license := coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
DeploymentIDs: []string{uuid.NewString()},
})
_, err := client.AddLicense(context.Background(), codersdk.AddLicenseRequest{
License: license,
})
errResp := &codersdk.Error{}
require.ErrorAs(t, err, &errResp)
require.Equal(t, http.StatusBadRequest, errResp.StatusCode())
require.Contains(t, errResp.Message, "License cannot be used on this deployment!")
})
t.Run("Unauthorized", func(t *testing.T) {
t.Parallel()
client, _ := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true})

15
go.mod
View File

@ -42,7 +42,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.20240401202854-d329bbdb530d
replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20240430122706-f586aa40c0c1
// Fixes a race-condition in coder/wgtunnel.
// Upstream PR: https://github.com/WireGuard/wireguard-go/pull/85
@ -144,7 +144,7 @@ require (
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02
github.com/imulab/go-scim/pkg/v2 v2.2.0
github.com/jedib0t/go-pretty/v6 v6.5.0
github.com/jmoiron/sqlx v1.3.5
github.com/jmoiron/sqlx v1.4.0
github.com/justinas/nosurf v1.1.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
@ -153,7 +153,7 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/mitchellh/go-wordwrap v1.0.1
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c
github.com/moby/moby v26.0.1+incompatible
github.com/moby/moby v26.1.0+incompatible
github.com/muesli/termenv v0.15.2
github.com/open-policy-agent/opa v0.58.0
github.com/ory/dockertest/v3 v3.10.0
@ -200,7 +200,7 @@ require (
golang.org/x/tools v0.20.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
google.golang.org/api v0.175.0
google.golang.org/api v0.176.1
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.33.0
gopkg.in/DataDog/dd-trace-go.v1 v1.61.0
@ -222,8 +222,8 @@ require (
)
require (
cloud.google.com/go/auth v0.2.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.1 // indirect
cloud.google.com/go/auth v0.3.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
github.com/DataDog/go-libddwaf/v2 v2.3.1 // indirect
github.com/alecthomas/chroma/v2 v2.13.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
@ -237,7 +237,7 @@ require (
require (
cloud.google.com/go/logging v1.9.0 // indirect
cloud.google.com/go/longrunning v0.5.5 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
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.4.1 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect
@ -307,7 +307,6 @@ require (
github.com/go-openapi/swag v0.22.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-test/deep v1.0.8 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/gobwas/glob v0.2.3 // indirect

39
go.sum
View File

@ -1,18 +1,18 @@
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 h1:KHblWIE/KHOwQ6lEbMZt6YpcGve2FEZ1sDtrW1Am5UI=
cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/auth v0.2.2 h1:gmxNJs4YZYcw6YvKRtVBaF2fyUE6UrWPyzU8jHvYfmI=
cloud.google.com/go/auth v0.2.2/go.mod h1:2bDNJWtWziDT3Pu1URxHHbkHE/BbOCuyUiKIGcNvafo=
cloud.google.com/go/auth/oauth2adapt v0.2.1 h1:VSPmMmUlT8CkIZ2PzD9AlLN+R3+D1clXMWHHa6vG/Ag=
cloud.google.com/go/auth/oauth2adapt v0.2.1/go.mod h1:tOdK/k+D2e4GEwfBRA48dKNQiDsqIXxLh7VU319eV0g=
cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs=
cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/logging v1.9.0 h1:iEIOXFO9EmSiTjDmfpbRjOxECO7R8C7b8IXUGOj7xZw=
cloud.google.com/go/logging v1.9.0/go.mod h1:1Io0vnZv4onoUnsVUQY3HZ3Igb1nBchky0A0y7BBBhE=
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
@ -217,8 +217,8 @@ github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0=
github.com/coder/serpent v0.7.0/go.mod h1:REkJ5ZFHQUWFTPLExhXYZ1CaHFjxvGNRlLXLdsI08YA=
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.20240401202854-d329bbdb530d h1:IMvBC1GrCIiZFxpOYRQacZtdjnmsdWNAMilPz+kvdG4=
github.com/coder/tailscale v1.1.1-0.20240401202854-d329bbdb530d/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4=
github.com/coder/tailscale v1.1.1-0.20240430122706-f586aa40c0c1 h1:cu5YyztCk8FAOvP1sR3b/2D96EfvBAzKUu0B/Cqhg8U=
github.com/coder/tailscale v1.1.1-0.20240430122706-f586aa40c0c1/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4=
github.com/coder/terraform-provider-coder v0.21.0 h1:aoDmFJULYZpS66EIAZuNY4IxElaDkdRaWMWp9ScD2R8=
github.com/coder/terraform-provider-coder v0.21.0/go.mod h1:hqxd15PJeftFBOnGBBPN6WfNQutZtnahwwPeV8U6TyA=
github.com/coder/wgtunnel v0.1.13-0.20231127054351-578bfff9b92a h1:KhR9LUVllMZ+e9lhubZ1HNrtJDgH5YLoTvpKwmrGag4=
@ -380,9 +380,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
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-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
@ -582,8 +581,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
@ -638,7 +637,6 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
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/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@ -670,9 +668,8 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
@ -703,8 +700,8 @@ github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/moby v26.0.1+incompatible h1:vCKs/AM0lLYnMxFwpf8ycsOekPPPcGn0s0Iczqv3/ec=
github.com/moby/moby v26.0.1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
github.com/moby/moby v26.1.0+incompatible h1:mjepCwMH0KpCgPvrXjqqyCeTCHgzO7p9TwZ2nQMI2qU=
github.com/moby/moby v26.1.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -1158,8 +1155,8 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/api v0.175.0 h1:9bMDh10V9cBuU8N45Wlc3cKkItfqMRV0Fi8UscLEtbY=
google.golang.org/api v0.175.0/go.mod h1:Rra+ltKu14pps/4xTycZfobMgLpbosoaaL7c+SEMrO8=
google.golang.org/api v0.176.1 h1:DJSXnV6An+NhJ1J+GWtoF2nHEuqB1VNoTfnIbjNvwD4=
google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=

View File

@ -35,6 +35,7 @@ os="${GOOS:-linux}"
arch="${GOARCH:-amd64}"
slim="${CODER_SLIM_BUILD:-0}"
sign_darwin="${CODER_SIGN_DARWIN:-0}"
sign_windows="${CODER_SIGN_WINDOWS:-0}"
output_path=""
agpl="${CODER_BUILD_AGPL:-0}"
boringcrypto=${CODER_BUILD_BORINGCRYPTO:-0}
@ -106,6 +107,11 @@ if [[ "$sign_darwin" == 1 ]]; then
requiredenvs AC_CERTIFICATE_FILE AC_CERTIFICATE_PASSWORD_FILE
fi
if [[ "$sign_windows" == 1 ]]; then
dependencies java
requiredenvs JSIGN_PATH EV_KEYSTORE EV_KEY EV_CERTIFICATE_PATH EV_TSA_URL GCLOUD_ACCESS_TOKEN
fi
ldflags=(
-X "'github.com/coder/coder/v2/buildinfo.tag=$version'"
)
@ -176,4 +182,8 @@ if [[ "$sign_darwin" == 1 ]] && [[ "$os" == "darwin" ]]; then
execrelative ./sign_darwin.sh "$output_path" 1>&2
fi
if [[ "$sign_windows" == 1 ]] && [[ "$os" == "windows" ]]; then
execrelative ./sign_windows.sh "$output_path" 1>&2
fi
echo "$output_path"

View File

@ -4,6 +4,9 @@
# [#pr-deployments](https://codercom.slack.com/archives/C05DNE982E8) Slack channel
set -euo pipefail
# shellcheck source=scripts/lib.sh
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
cdroot
# default settings
dryRun=false
@ -64,6 +67,9 @@ if $confirm; then
fi
fi
# Authenticate gh CLI
gh_auth
# get branch name and pr number
branchName=$(gh pr view --json headRefName | jq -r .headRefName)
prNumber=$(gh pr view --json number | jq -r .number)

View File

@ -130,6 +130,22 @@ requiredenvs() {
fi
}
gh_auth() {
local fail=0
if [[ "${CODER:-}" == "true" ]]; then
if ! output=$(coder external-auth access-token github 2>&1); then
log "ERROR: Could not authenticate with GitHub."
log "$output"
fail=1
else
GITHUB_TOKEN=$(coder external-auth access-token github)
export GITHUB_TOKEN
fi
else
log "Please authenticate gh CLI by running 'gh auth login'"
fi
}
# maybedryrun prints the given program and flags, and then, if the first
# argument is 0, executes it. The reason the first argument should be 0 is that
# it is expected that you have a dry_run variable in your script that is set to

View File

@ -113,6 +113,9 @@ done
# Check dependencies.
dependencies gh jq sort
# Authenticate gh CLI
gh_auth
if [[ -z $increment ]]; then
# Default to patch versions.
increment="patch"

View File

@ -31,6 +31,9 @@ range="${from_ref}..${to_ref}"
# Check dependencies.
dependencies gh
# Authenticate gh CLI
gh_auth
COMMIT_METADATA_BREAKING=0
declare -a COMMIT_METADATA_COMMITS
declare -A COMMIT_METADATA_TITLE COMMIT_METADATA_HUMAN_TITLE COMMIT_METADATA_CATEGORY COMMIT_METADATA_AUTHORS
@ -145,7 +148,6 @@ main() {
done
} | sort -t- -n | head -n 1
)
# Get the labels for all PRs merged since the last release, this is
# inexact based on date, so a few PRs part of the previous release may
# be included.

View File

@ -57,6 +57,9 @@ done
# Check dependencies.
dependencies gh sort
# Authticate gh CLI
gh_auth
if [[ -z ${old_version} ]]; then
error "No old version specified"
fi

View File

@ -71,6 +71,9 @@ done
# Check dependencies
dependencies gh
# Authenticate gh CLI
gh_auth
# Remove the "v" prefix.
version="${version#v}"
if [[ "$version" == "" ]]; then

35
scripts/sign_windows.sh Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
# This script signs the provided windows binary with an Extended Validation
# code signing certificate.
#
# Usage: ./sign_windows.sh path/to/binary
#
# On success, the input file will be signed using the EV cert.
#
# Depends on the jsign utility (and thus Java). Requires the following environment variables
# to be set:
# - $JSIGN_PATH: The path to the jsign jar.
# - $EV_KEYSTORE: The name of the keyring containing the private key
# - $EV_KEY: The name of the key.
# - $EV_CERTIFICATE_PATH: The path to the certificate.
# - $EV_TSA_URL: The url of the timestamp server to use.
set -euo pipefail
# shellcheck source=scripts/lib.sh
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
# Check dependencies
dependencies java
requiredenvs JSIGN_PATH EV_KEYSTORE EV_KEY EV_CERTIFICATE_PATH EV_TSA_URL GCLOUD_ACCESS_TOKEN
java -jar "$JSIGN_PATH" \
--storetype GOOGLECLOUD \
--storepass "$GCLOUD_ACCESS_TOKEN" \
--keystore "$EV_KEYSTORE" \
--alias "$EV_KEY" \
--certfile "$EV_CERTIFICATE_PATH" \
--tsmode RFC3161 \
--tsaurl "$EV_TSA_URL" \
"$@" \
1>&2

View File

@ -34,7 +34,6 @@ import (
"golang.org/x/sync/singleflight"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/appearance"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
@ -78,6 +77,7 @@ type Options struct {
SiteFS fs.FS
OAuth2Configs *httpmw.OAuth2Configs
DocsURL string
BuildInfo codersdk.BuildInfoResponse
AppearanceFetcher *atomic.Pointer[appearance.Fetcher]
}
@ -149,12 +149,7 @@ func New(opts *Options) *Handler {
// static files.
OnlyFiles(opts.SiteFS))),
)
buildInfo := codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
Version: buildinfo.Version(),
}
buildInfoResponse, err := json.Marshal(buildInfo)
buildInfoResponse, err := json.Marshal(opts.BuildInfo)
if err != nil {
panic("failed to marshal build info: " + err.Error())
}

View File

@ -165,6 +165,7 @@ export interface BuildInfoResponse {
readonly workspace_proxy: boolean;
readonly agent_api_version: string;
readonly upgrade_message: string;
readonly deployment_id: string;
}
// From codersdk/insights.go

View File

@ -0,0 +1,31 @@
import FormHelperText, {
type FormHelperTextProps,
} from "@mui/material/FormHelperText";
import type { ComponentProps, FC } from "react";
import { Stack } from "components/Stack/Stack";
/**
* Use these components as the label in FormControlLabel when implementing radio
* buttons, checkboxes, or switches to ensure proper styling.
*/
export const StackLabel: FC<ComponentProps<typeof Stack>> = (props) => {
return (
<Stack
spacing={0.5}
css={{ paddingLeft: 12, fontWeight: 500 }}
{...props}
/>
);
};
export const StackLabelHelperText: FC<FormHelperTextProps> = (props) => {
return (
<FormHelperText
css={{
marginTop: 0,
}}
{...props}
/>
);
};

View File

@ -12,9 +12,11 @@ import LaunchIcon from "@mui/icons-material/LaunchOutlined";
import DocsIcon from "@mui/icons-material/MenuBook";
import Divider from "@mui/material/Divider";
import MenuItem from "@mui/material/MenuItem";
import Tooltip from "@mui/material/Tooltip";
import type { FC } from "react";
import { Link } from "react-router-dom";
import type * as TypesGen from "api/typesGenerated";
import { CopyButton } from "components/CopyButton/CopyButton";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { usePopover } from "components/Popover/Popover";
import { Stack } from "components/Stack/Stack";
@ -161,15 +163,51 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
<Divider css={{ marginBottom: "0 !important" }} />
<Stack css={styles.info} spacing={0}>
<a
title="Browse Source Code"
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
target="_blank"
rel="noreferrer"
>
{buildInfo?.version} <LaunchIcon />
</a>
<Tooltip title="Coder Version">
<a
title="Browse Source Code"
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
target="_blank"
rel="noreferrer"
>
{buildInfo?.version} <LaunchIcon />
</a>
</Tooltip>
{Boolean(buildInfo?.deployment_id) && (
<div
css={css`
font-size: 12px;
display: flex;
align-items: center;
`}
>
<Tooltip title="Deployment Identifier">
<div
css={css`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`}
>
{buildInfo?.deployment_id}
</div>
</Tooltip>
<CopyButton
text={buildInfo!.deployment_id}
buttonStyles={css`
width: 16px;
height: 16px;
svg {
width: 16px;
height: 16px;
}
`}
/>
</div>
)}
<div css={styles.footerText}>{Language.copyrightText}</div>
</Stack>

View File

@ -21,12 +21,7 @@ export const TemplateScheduleAutostart: FC<TemplateScheduleAutostartProps> = ({
onChange,
}) => {
return (
<Stack
direction="column"
width="100%"
alignItems="center"
css={{ marginBottom: "20px" }}
>
<Stack width="100%" alignItems="start" spacing={1}>
<Stack
direction="row"
spacing={0}
@ -49,6 +44,7 @@ export const TemplateScheduleAutostart: FC<TemplateScheduleAutostartProps> = ({
}[]
).map((day) => (
<Button
fullWidth
key={day.key}
css={{ borderRadius: 0 }}
// TODO: Adding a background color would also help

View File

@ -1,4 +1,3 @@
import { useTheme } from "@emotion/react";
import Checkbox from "@mui/material/Checkbox";
import FormControlLabel from "@mui/material/FormControlLabel";
import MenuItem from "@mui/material/MenuItem";
@ -14,6 +13,10 @@ import {
FormFields,
} from "components/Form/Form";
import { Stack } from "components/Stack/Stack";
import {
StackLabel,
StackLabelHelperText,
} from "components/StackLabel/StackLabel";
import { getFormHelpers } from "utils/formUtils";
import {
calculateAutostopRequirementDaysValue,
@ -51,7 +54,8 @@ const DORMANT_AUTODELETION_DEFAULT = 30;
* The default form field space is 4 but since this form is quite heavy I think
* increase the space can make it feels lighter.
*/
const FORM_FIELDS_SPACING = 6;
const FORM_FIELDS_SPACING = 8;
const DORMANT_FIELDSET_SPACING = 4;
export interface TemplateScheduleForm {
template: Template;
@ -151,7 +155,6 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
form,
error,
);
const theme = useTheme();
const now = new Date();
const weekFromNow = new Date(now);
@ -404,34 +407,30 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
/>
</Stack>
<Stack direction="row" alignItems="center">
<Checkbox
id="allow-user-autostop"
size="small"
disabled={isSubmitting || !allowAdvancedScheduling}
onChange={async () => {
await form.setFieldValue(
"allow_user_autostop",
!form.values.allow_user_autostop,
);
}}
name="allow_user_autostop"
checked={form.values.allow_user_autostop}
/>
<Stack spacing={0.5}>
<strong>Enforce these settings across all workspaces</strong>
<span
css={{
fontSize: 12,
color: theme.palette.text.secondary,
<FormControlLabel
control={
<Checkbox
id="allow-user-autostop"
size="small"
disabled={isSubmitting || !allowAdvancedScheduling}
onChange={async (_, checked) => {
await form.setFieldValue("allow_user_autostop", checked);
}}
>
Workspaces by default allow users to set custom autostop timers.
Use this to apply the template settings to all workspaces under
this template.
</span>
</Stack>
</Stack>
name="allow_user_autostop"
checked={form.values.allow_user_autostop}
/>
}
label={
<StackLabel>
Allow users to customize autostop duration for workspaces.
<StackLabelHelperText>
By default, workspaces will inherit the Autostop timer from
this template. Enabling this option allows users to set custom
Autostop timers on their workspaces or turn off the timer.
</StackLabelHelperText>
</StackLabel>
}
/>
</FormFields>
</FormSection>
@ -439,27 +438,30 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
title="Autostart"
description="Allow users to set custom autostart and autostop scheduling options for workspaces created from this template."
>
<Stack direction="column">
<Stack direction="row" alignItems="center">
<Checkbox
id="allow_user_autostart"
size="small"
disabled={isSubmitting || !allowAdvancedScheduling}
onChange={async () => {
await form.setFieldValue(
"allow_user_autostart",
!form.values.allow_user_autostart,
);
}}
name="allow_user_autostart"
checked={form.values.allow_user_autostart}
/>
<Stack spacing={0.5}>
<strong>
<Stack>
<FormControlLabel
control={
<Checkbox
id="allow_user_autostart"
size="small"
disabled={isSubmitting || !allowAdvancedScheduling}
onChange={async () => {
await form.setFieldValue(
"allow_user_autostart",
!form.values.allow_user_autostart,
);
}}
name="allow_user_autostart"
checked={form.values.allow_user_autostart}
/>
}
label={
<StackLabel>
Allow users to automatically start workspaces on a schedule.
</strong>
</Stack>
</Stack>
</StackLabel>
}
/>
{allowAdvancedScheduling && (
<TemplateScheduleAutostart
enabled={Boolean(form.values.allow_user_autostart)}
@ -482,19 +484,20 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
<>
<FormSection
title="Dormancy"
description="Coder's Dormancy Threshold determines when workspaces become dormant due to inactivity, requiring manual activation for access."
description="When enabled, Coder will mark workspaces as dormant after a period of time with no connections. Dormant workspaces can be auto-deleted (see below) or manually reviewed by the workspace owner or admins."
>
<FormFields spacing={FORM_FIELDS_SPACING}>
<Stack>
<Stack spacing={DORMANT_FIELDSET_SPACING}>
<FormControlLabel
control={
<Switch
size="small"
name="dormancyThreshold"
checked={form.values.inactivity_cleanup_enabled}
onChange={handleToggleInactivityCleanup}
/>
}
label="Enable Dormancy Threshold"
label={<StackLabel>Enable Dormancy Threshold</StackLabel>}
/>
<TextField
{...getFieldHelpers("time_til_dormant_ms", {
@ -514,16 +517,33 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
/>
</Stack>
<Stack>
<Stack spacing={DORMANT_FIELDSET_SPACING}>
<FormControlLabel
control={
<Switch
size="small"
name="dormancyAutoDeletion"
checked={form.values.dormant_autodeletion_cleanup_enabled}
onChange={handleToggleDormantAutoDeletion}
/>
}
label="Enable Dormancy Auto-Deletion"
label={
<StackLabel>
Enable Dormancy Auto-Deletion
<StackLabelHelperText>
When enabled, Coder will permanently delete dormant
workspaces after a period of time.{" "}
<span
css={(theme) => ({
fontWeight: 500,
color: theme.palette.text.primary,
})}
>
Once a workspace is deleted it cannot be recovered.
</span>
</StackLabelHelperText>
</StackLabel>
}
/>
<TextField
{...getFieldHelpers("time_til_dormant_autodelete_ms", {
@ -544,16 +564,25 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
/>
</Stack>
<Stack>
<Stack spacing={DORMANT_FIELDSET_SPACING}>
<FormControlLabel
control={
<Switch
size="small"
name="failureCleanupEnabled"
checked={form.values.failure_cleanup_enabled}
onChange={handleToggleFailureCleanup}
/>
}
label="Enable Failure Cleanup"
label={
<StackLabel>
Enable Failure Cleanup
<StackLabelHelperText>
When enabled, Coder will attempt to stop workspaces that
are in a failed state after a specified number of days.
</StackLabelHelperText>
</StackLabel>
}
/>
<TextField
{...getFieldHelpers("failure_ttl_ms", {

View File

@ -201,6 +201,7 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = {
dashboard_url: "https:///mock-url",
workspace_proxy: false,
upgrade_message: "My custom upgrade message",
deployment_id: "510d407f-e521-4180-b559-eab4a6d802b8",
};
export const MockSupportLinks: TypesGen.LinkConfig[] = [