Skip to content

Commit 939938e

Browse files
committed
Merge remote-tracking branch 'origin/main' into cj/workspaces-last-used-at-timestamptz
2 parents 19663d7 + 5de5d20 commit 939938e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1126
-340
lines changed

.github/actions/upload-datadog/action.yaml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Upload tests to datadog
1+
name: Upload tests to Datadog
22
if: always()
33
inputs:
44
api-key:
@@ -7,8 +7,26 @@ inputs:
77
runs:
88
using: "composite"
99
steps:
10+
- name: Set work dir
11+
shell: bash
12+
run: |
13+
WORK_DIR=${{ runner.temp }}/datadog-ci
14+
mkdir -p $WORK_DIR
15+
echo "WORK_DIR=$WORK_DIR" >> $GITHUB_ENV
16+
# The npm install was taking 30s to 1m, accounting for 20+% of the total
17+
# job time.
18+
- name: Cache datadog-ci
19+
uses: buildjet/cache@v3
20+
with:
21+
path: |
22+
${{ env.WORK_DIR }}
23+
key: datadog-ci-${{ runner.os }}
24+
restore-keys: |
25+
datadog-ci-${{ runner.os }}-
26+
datadog-ci-
1027
- shell: bash
1128
run: |
29+
cd ${{ env.WORK_DIR }}
1230
owner=${{ github.repository_owner }}
1331
echo "owner: $owner"
1432
if [[ $owner != "coder" ]]; then
@@ -20,8 +38,8 @@ runs:
2038
echo "No API key provided, skipping..."
2139
exit 0
2240
fi
23-
npm install -g @datadog/datadog-ci@2.10.0
24-
datadog-ci junit upload --service coder ./gotests.xml \
41+
npm install @datadog/datadog-ci@2.10.0
42+
npm x -- datadog-ci junit upload --service coder ./gotests.xml \
2543
--tags os:${{runner.os}} --tags runner_name:${{runner.name}}
2644
env:
2745
DATADOG_API_KEY: ${{ inputs.api-key }}

cli/testdata/coder_templates_init_--help.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ USAGE:
66
Get started with a templated template.
77

88
OPTIONS:
9-
--id aws-ecs-container|aws-linux|aws-windows|azure-linux|do-linux|docker|docker-with-dotfiles|fly-docker-image|gcp-linux|gcp-vm-container|gcp-windows|kubernetes
9+
--id aws-ecs-container|aws-linux|aws-windows|azure-linux|do-linux|docker|docker-with-dotfiles|gcp-linux|gcp-vm-container|gcp-windows|kubernetes
1010
Specify a given example template by ID.
1111

1212
———

coderd/schedule/cron/cron.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,19 @@ func (s Schedule) Min() time.Duration {
173173
return durMin
174174
}
175175

176+
// TimeParsed returns the parsed time.Time of the minute and hour fields. If the
177+
// time cannot be represented in a valid time.Time, a zero time is returned.
178+
func (s Schedule) TimeParsed() time.Time {
179+
minute := strings.Fields(s.cronStr)[0]
180+
hour := strings.Fields(s.cronStr)[1]
181+
maybeTime := fmt.Sprintf("%s:%s", hour, minute)
182+
t, err := time.ParseInLocation("15:4", maybeTime, s.sched.Location)
183+
if err != nil {
184+
return time.Time{}
185+
}
186+
return t
187+
}
188+
176189
// Time returns a humanized form of the minute and hour fields.
177190
func (s Schedule) Time() string {
178191
minute := strings.Fields(s.cronStr)[0]

codersdk/deployment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ var FeatureNames = []FeatureName{
6464
FeatureExternalProvisionerDaemons,
6565
FeatureAppearance,
6666
FeatureAdvancedTemplateScheduling,
67+
FeatureTemplateAutostopRequirement,
6768
FeatureWorkspaceProxy,
6869
FeatureUserRoleManagement,
6970
FeatureExternalTokenEncryption,

docs/cli/templates_init.md

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/images/display-apps.png

25.1 KB
Loading

docs/templates/index.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,29 @@ By default, all templates allow developers to connect over SSH and a web
411411
terminal. See [Configuring Web IDEs](../ides/web-ides.md) to learn how to give
412412
users access to additional web applications.
413413

414+
Template administrators can hide apps like the web-based Terminal or VS Code
415+
Desktop with the
416+
[`display_apps`](https://registry.terraform.io/providers/coder/coder/0.11.2/docs/resources/agent#display_apps)
417+
configuration in the
418+
[`coder_agent`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent)
419+
resource. For example, the following configuration block will hide all default
420+
Coder apps except the web terminal.
421+
422+
```hcl
423+
display_apps {
424+
vscode = false
425+
vscode_insiders = false
426+
ssh_helper = false
427+
port_forwarding_helper = false
428+
web_terminal = true
429+
}
430+
```
431+
432+
Example use cases for `display_apps` are JetBrains users or zero-trust
433+
deployments who do not want nor should have access to a local VS Code IDE.
434+
435+
![display-apps](../images/display-apps.png)
436+
414437
### Data source
415438

416439
When a workspace is being started or stopped, the `coder_workspace` data source

enterprise/coderd/coderd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
472472
codersdk.FeatureAdvancedTemplateScheduling: true,
473473
// FeatureTemplateAutostopRequirement depends on
474474
// FeatureAdvancedTemplateScheduling.
475-
codersdk.FeatureTemplateAutostopRequirement: api.DefaultQuietHoursSchedule != "",
475+
codersdk.FeatureTemplateAutostopRequirement: api.AGPL.Experiments.Enabled(codersdk.ExperimentTemplateAutostopRequirement) && api.DefaultQuietHoursSchedule != "",
476476
codersdk.FeatureWorkspaceProxy: true,
477477
codersdk.FeatureUserRoleManagement: true,
478478
})

enterprise/coderd/users.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (api *API) userQuietHoursSchedule(rw http.ResponseWriter, r *http.Request)
6868
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserQuietHoursScheduleResponse{
6969
RawSchedule: opts.Schedule.String(),
7070
UserSet: opts.UserSet,
71-
Time: opts.Schedule.Time(),
71+
Time: opts.Schedule.TimeParsed().Format("15:40"),
7272
Timezone: opts.Schedule.Location().String(),
7373
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
7474
})
@@ -114,7 +114,7 @@ func (api *API) putUserQuietHoursSchedule(rw http.ResponseWriter, r *http.Reques
114114
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserQuietHoursScheduleResponse{
115115
RawSchedule: opts.Schedule.String(),
116116
UserSet: opts.UserSet,
117-
Time: opts.Schedule.Time(),
117+
Time: opts.Schedule.TimeParsed().Format("15:40"),
118118
Timezone: opts.Schedule.Location().String(),
119119
Next: opts.Schedule.Next(time.Now().In(opts.Schedule.Location())),
120120
})

enterprise/coderd/users_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ func TestUserQuietHours(t *testing.T) {
2121
t.Run("OK", func(t *testing.T) {
2222
t.Parallel()
2323

24-
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 0 0 * * *"
24+
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 0 1 * * *"
2525
defaultScheduleParsed, err := cron.Daily(defaultQuietHoursSchedule)
2626
require.NoError(t, err)
2727
nextTime := defaultScheduleParsed.Next(time.Now().In(defaultScheduleParsed.Location()))
2828
if time.Until(nextTime) < time.Hour {
2929
// Use a different default schedule instead, because we want to avoid
3030
// the schedule "ticking over" during this test run.
31-
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 0 12 * * *"
31+
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 0 13 * * *"
3232
defaultScheduleParsed, err = cron.Daily(defaultQuietHoursSchedule)
3333
require.NoError(t, err)
3434
}
@@ -55,7 +55,7 @@ func TestUserQuietHours(t *testing.T) {
5555
require.NoError(t, err)
5656
require.Equal(t, defaultScheduleParsed.String(), sched1.RawSchedule)
5757
require.False(t, sched1.UserSet)
58-
require.Equal(t, defaultScheduleParsed.Time(), sched1.Time)
58+
require.Equal(t, defaultScheduleParsed.TimeParsed().Format("15:40"), sched1.Time)
5959
require.Equal(t, defaultScheduleParsed.Location().String(), sched1.Timezone)
6060
require.WithinDuration(t, defaultScheduleParsed.Next(time.Now()), sched1.Next, 15*time.Second)
6161

@@ -78,7 +78,7 @@ func TestUserQuietHours(t *testing.T) {
7878
require.NoError(t, err)
7979
require.Equal(t, customScheduleParsed.String(), sched2.RawSchedule)
8080
require.True(t, sched2.UserSet)
81-
require.Equal(t, customScheduleParsed.Time(), sched2.Time)
81+
require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched2.Time)
8282
require.Equal(t, customScheduleParsed.Location().String(), sched2.Timezone)
8383
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched2.Next, 15*time.Second)
8484

@@ -87,7 +87,7 @@ func TestUserQuietHours(t *testing.T) {
8787
require.NoError(t, err)
8888
require.Equal(t, customScheduleParsed.String(), sched3.RawSchedule)
8989
require.True(t, sched3.UserSet)
90-
require.Equal(t, customScheduleParsed.Time(), sched3.Time)
90+
require.Equal(t, customScheduleParsed.TimeParsed().Format("15:40"), sched3.Time)
9191
require.Equal(t, customScheduleParsed.Location().String(), sched3.Timezone)
9292
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched3.Next, 15*time.Second)
9393

examples/examples.gen.json

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,5 @@
133133
"kubernetes"
134134
],
135135
"markdown": "\n# Getting started\n\nThis template creates a deplyment running the `codercom/enterprise-base:ubuntu` image.\n\n## Prerequisites\n\nThis template uses [`kubernetes_deployment`](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) terraform resource, which requires the `coder` service account to have permission to create deploymnets. For example if you are using [helm](https://coder.com/docs/v2/latest/install/kubernetes#install-coder-with-helm) to install Coder, you should set `coder.serviceAccount.enableDeployments=true` in your `values.yaml`\n\n```diff\ncoder:\nserviceAccount:\n workspacePerms: true\n- enableDeployments: false\n+ enableDeployments: true\n annotations: {}\n name: coder\n```\n\n\u003e Note: This is only required for Coder versions \u003c 0.28.0, as this will be the default value for Coder versions \u003e= 0.28.0\n\n## Authentication\n\nThis template can authenticate using in-cluster authentication, or using a kubeconfig local to the\nCoder host. For additional authentication options, consult the [Kubernetes provider\ndocumentation](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs).\n\n### kubeconfig on Coder host\n\nIf the Coder host has a local `~/.kube/config`, you can use this to authenticate\nwith Coder. Make sure this is done with same user that's running the `coder` service.\n\nTo use this authentication, set the parameter `use_kubeconfig` to true.\n\n### In-cluster authentication\n\nIf the Coder host runs in a Pod on the same Kubernetes cluster as you are creating workspaces in,\nyou can use in-cluster authentication.\n\nTo use this authentication, set the parameter `use_kubeconfig` to false.\n\nThe Terraform provisioner will automatically use the service account associated with the pod to\nauthenticate to Kubernetes. Be sure to bind a [role with appropriate permission](#rbac) to the\nservice account. For example, assuming the Coder host runs in the same namespace as you intend\nto create workspaces:\n\n```yaml\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: coder\n\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n name: coder\nsubjects:\n - kind: ServiceAccount\n name: coder\nroleRef:\n kind: Role\n name: coder\n apiGroup: rbac.authorization.k8s.io\n```\n\nThen start the Coder host with `serviceAccountName: coder` in the pod spec.\n\n### Authenticate against external clusters\n\nYou may want to deploy workspaces on a cluster outside of the Coder control plane. Refer to the [Coder docs](https://coder.com/docs/v2/latest/platforms/kubernetes/additional-clusters) to learn how to modify your template to authenticate against external clusters.\n\n## Namespace\n\nThe target namespace in which the deployment will be deployed is defined via the `coder_workspace`\nvariable. The namespace must exist prior to creating workspaces.\n\n## Persistence\n\nThe `/home/coder` directory in this example is persisted via the attached PersistentVolumeClaim.\nAny data saved outside of this directory will be wiped when the workspace stops.\n\nSince most binary installations and environment configurations live outside of\nthe `/home` directory, we suggest including these in the `startup_script` argument\nof the `coder_agent` resource block, which will run each time the workspace starts up.\n\nFor example, when installing the `aws` CLI, the install script will place the\n`aws` binary in `/usr/local/bin/aws`. To ensure the `aws` CLI is persisted across\nworkspace starts/stops, include the following code in the `coder_agent` resource\nblock of your workspace template:\n\n```terraform\nresource \"coder_agent\" \"main\" {\n startup_script = \u003c\u003c-EOT\n set -e\n # install AWS CLI\n curl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"\n unzip awscliv2.zip\n sudo ./aws/install\n EOT\n}\n```\n\n## code-server\n\n`code-server` is installed via the `startup_script` argument in the `coder_agent`\nresource block. The `coder_app` resource is defined to access `code-server` through\nthe dashboard UI over `localhost:13337`.\n\n## Deployment logs\n\nTo stream kubernetes pods events from the deployment, you can use Coder's [`coder-logstream-kube`](https://github.com/coder/coder-logstream-kube) tool. This can stream logs from the deployment to Coder's workspace startup logs. You just need to install the `coder-logstream-kube` helm chart on the cluster where the deployment is running.\n\n```shell\nhelm repo add coder-logstream-kube https://helm.coder.com/logstream-kube\nhelm install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \\\n --namespace coder \\\n --set url=\u003cyour-coder-url-including-http-or-https\u003e\n```\n\nFor detailed instructions, see [Deployment logs](https://coder.com/docs/v2/latest/platforms/kubernetes/deployment-logs)\n"
136-
},
137-
{
138-
"id": "fly-docker-image",
139-
"url": "",
140-
"name": "Develop on a Fly.io container",
141-
"description": "Run workspaces as Firecracker VMs on Fly.io",
142-
"icon": "/icon/fly.io.svg",
143-
"tags": [
144-
"docker",
145-
"fly.io"
146-
],
147-
"markdown": "\n# Coder Fly.io Template\n\nThis template provisions a [code-server](https://github.com/coder/code-server) instance on [fly.io](https://fly.io) using the [codercom/code-server](https://hub.docker.com/r/codercom/code-server) image.\n\n## Prerequisites\n\n- [flyctl](https://fly.io/docs/getting-started/installing-flyctl/) installed.\n- [Coder](https://coder.com/) already setup and running with coder-cli installed locally.\n\n## Getting started\n\n1. Run `coder templates init` and select this template. Follow the instructions that appear.\n2. cd into the directory that was created. (e.g. `cd fly-docker-image`)\n3. Create the new template by running the following command from the `fly-docker-image` directory:\n\n```bash\ncoder templates create fly-docker-image \\\n --var fly_api_token=$(flyctl auth token) \\\n --var fly_org=personal\n```\n\n\u003e If the Coder server is also running as a fly.io app, then instead of setting variable `fly_api_token` you can also set a fly.io secret with the name `FLY_API_TOKEN`\n\u003e\n\u003e ```bash\n\u003e flyctl secrets set FLY_API_TOKEN=$(flyctl auth token) --app \u003cyour-coder-app-name\u003e\n\u003e ```\n\n\u003e Read our blog [post](coder.com/blog/deploying-coder-on-fly-io) to learn more about how to deploy Coder on fly.io.\n\n4. Navigate to the Coder dashboard and create a new workspace using the template.\n\nThis is all. You should now have a code-server instance running on fly.io.\n"
148136
}
149137
]

examples/examples.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ var (
3434
//go:embed templates/gcp-vm-container
3535
//go:embed templates/gcp-windows
3636
//go:embed templates/kubernetes
37-
//go:embed templates/fly-docker-image
3837
files embed.FS
3938

4039
exampleBasePath = "https://github.com/coder/coder/tree/main/examples/templates/"

examples/templates/kubernetes/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ icon: /icon/k8s.png
77

88
# Getting started
99

10-
This template creates a deplyment running the `codercom/enterprise-base:ubuntu` image.
10+
This template creates a deployment running the `codercom/enterprise-base:ubuntu` image.
1111

1212
## Prerequisites
1313

site/src/AppRouter.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const CliAuthenticationPage = lazy(
3333
const AccountPage = lazy(
3434
() => import("./pages/UserSettingsPage/AccountPage/AccountPage"),
3535
);
36+
const SchedulePage = lazy(
37+
() => import("./pages/UserSettingsPage/SchedulePage/SchedulePage"),
38+
);
3639
const SecurityPage = lazy(
3740
() => import("./pages/UserSettingsPage/SecurityPage/SecurityPage"),
3841
);
@@ -292,6 +295,7 @@ export const AppRouter: FC = () => {
292295

293296
<Route path="settings" element={<SettingsLayout />}>
294297
<Route path="account" element={<AccountPage />} />
298+
<Route path="schedule" element={<SchedulePage />} />
295299
<Route path="security" element={<SecurityPage />} />
296300
<Route path="ssh-keys" element={<SSHKeysPage />} />
297301
<Route path="tokens">

site/src/api/api.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,21 @@ export const updateProfile = async (
665665
return response.data;
666666
};
667667

668+
export const getUserQuietHoursSchedule = async (
669+
userId: TypesGen.User["id"],
670+
): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
671+
const response = await axios.get(`/api/v2/users/${userId}/quiet-hours`);
672+
return response.data;
673+
};
674+
675+
export const updateUserQuietHoursSchedule = async (
676+
userId: TypesGen.User["id"],
677+
data: TypesGen.UpdateUserQuietHoursScheduleRequest,
678+
): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
679+
const response = await axios.put(`/api/v2/users/${userId}/quiet-hours`, data);
680+
return response.data;
681+
};
682+
668683
export const activateUser = async (
669684
userId: TypesGen.User["id"],
670685
): Promise<TypesGen.User> => {

site/src/api/queries/settings.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as API from "api/api";
2+
import {
3+
type UserQuietHoursScheduleResponse,
4+
type UpdateUserQuietHoursScheduleRequest,
5+
} from "api/typesGenerated";
6+
import { type QueryClient, type QueryOptions } from "@tanstack/react-query";
7+
8+
export const userQuietHoursScheduleKey = (userId: string) => [
9+
"settings",
10+
userId,
11+
"quietHours",
12+
];
13+
14+
export const userQuietHoursSchedule = (
15+
userId: string,
16+
): QueryOptions<UserQuietHoursScheduleResponse> => {
17+
return {
18+
queryKey: userQuietHoursScheduleKey(userId),
19+
queryFn: () => API.getUserQuietHoursSchedule(userId),
20+
};
21+
};
22+
23+
export const updateUserQuietHoursSchedule = (
24+
userId: string,
25+
queryClient: QueryClient,
26+
) => {
27+
return {
28+
mutationFn: (request: UpdateUserQuietHoursScheduleRequest) =>
29+
API.updateUserQuietHoursSchedule(userId, request),
30+
onSuccess: async () => {
31+
await queryClient.invalidateQueries(userQuietHoursScheduleKey(userId));
32+
},
33+
};
34+
};

site/src/api/queries/templates.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,17 @@ export const templateExamples = (orgId: string) => {
4949
queryFn: () => API.getTemplateExamples(orgId),
5050
};
5151
};
52+
53+
export const templateVersion = (versionId: string) => {
54+
return {
55+
queryKey: ["templateVersion", versionId],
56+
queryFn: () => API.getTemplateVersion(versionId),
57+
};
58+
};
59+
60+
export const templateVersions = (templateId: string) => {
61+
return {
62+
queryKey: ["templateVersions", templateId],
63+
queryFn: () => API.getTemplateVersions(templateId),
64+
};
65+
};

0 commit comments

Comments
 (0)