From 6c6240bd9527cc364f8d38a430f5ebb963525db5 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 24 Jan 2025 09:11:27 +0000 Subject: [PATCH 01/37] chore: add workspace reached resource threshold notification --- .../000288_oom_and_ood_notification.down.sql | 1 + .../000288_oom_and_ood_notification.up.sql | 16 ++++ coderd/notifications/events.go | 17 ++-- coderd/notifications/notifications_test.go | 14 ++++ ...kspaceReachedResourceThreshold.html.golden | 79 +++++++++++++++++++ ...kspaceReachedResourceThreshold.json.golden | 29 +++++++ 6 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 coderd/database/migrations/000288_oom_and_ood_notification.down.sql create mode 100644 coderd/database/migrations/000288_oom_and_ood_notification.up.sql create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden diff --git a/coderd/database/migrations/000288_oom_and_ood_notification.down.sql b/coderd/database/migrations/000288_oom_and_ood_notification.down.sql new file mode 100644 index 0000000000000..fcb420c575866 --- /dev/null +++ b/coderd/database/migrations/000288_oom_and_ood_notification.down.sql @@ -0,0 +1 @@ +DELETE FROM notification_templates WHERE id = 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a'; diff --git a/coderd/database/migrations/000288_oom_and_ood_notification.up.sql b/coderd/database/migrations/000288_oom_and_ood_notification.up.sql new file mode 100644 index 0000000000000..293b6850f272b --- /dev/null +++ b/coderd/database/migrations/000288_oom_and_ood_notification.up.sql @@ -0,0 +1,16 @@ +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ( + 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a', + 'Workspace Reached Resource Threshold', + E'Workspace "{{.Labels.workspace}}" reached resource threshold', + E'Hi {{.UserName}},\n\n'|| + E'Your workspace **{{.Labels.workspace}}** has reached the {{.Labels.threshold_type}} threshold set at **{{.Labels.threshold}}**.', + 'Workspace Events', + '[ + { + "label": "View workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + } + ]'::jsonb +); diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 754d2e5c7f745..5e50aaffc7129 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -7,14 +7,15 @@ import "github.com/google/uuid" // Workspace-related events. var ( - TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff") - TemplateWorkspaceManuallyUpdated = uuid.MustParse("d089fe7b-d5c5-4c0c-aaf5-689859f7d392") - TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed") - TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9") - TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0") - TemplateWorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b") - TemplateWorkspaceMarkedForDeletion = uuid.MustParse("51ce2fdf-c9ca-4be1-8d70-628674f9bc42") - TemplateWorkspaceManualBuildFailed = uuid.MustParse("2faeee0f-26cb-4e96-821c-85ccb9f71513") + TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff") + TemplateWorkspaceManuallyUpdated = uuid.MustParse("d089fe7b-d5c5-4c0c-aaf5-689859f7d392") + TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed") + TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9") + TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0") + TemplateWorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b") + TemplateWorkspaceMarkedForDeletion = uuid.MustParse("51ce2fdf-c9ca-4be1-8d70-628674f9bc42") + TemplateWorkspaceManualBuildFailed = uuid.MustParse("2faeee0f-26cb-4e96-821c-85ccb9f71513") + TemplateWorkspaceReachedResourceThreshold = uuid.MustParse("a9d027b4-ac49-4fb1-9f6d-45af15f64e7a") ) // Account-related events. diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 62fa50f453cfa..bb57f9124958f 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1064,6 +1064,20 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, }, + { + name: "TemplateWorkspaceReachedResourceThreshold", + id: notifications.TemplateWorkspaceReachedResourceThreshold, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "workspace": "bobby-workspace", + "threshold_type": "memory usage", + "threshold": "90%", + }, + }, + }, } // We must have a test case for every notification_template. This is enforced below: diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden new file mode 100644 index 0000000000000..8e42cf6729b7e --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden @@ -0,0 +1,79 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Workspace "bobby-workspace" reached resource threshold +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +Your workspace bobby-workspace has reached the memory usage threshold set a= +t 90%. + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Codestin Search App + + +
+
+ 3D"Cod= +
+

+ Workspace "bobby-workspace" reached resource threshold +

+
+

Hi Bobby,

+ +

Your workspace bobby-workspace has reached the memory u= +sage threshold set at 90%.

+
+
+ =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden new file mode 100644 index 0000000000000..4c5c540343ba0 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden @@ -0,0 +1,29 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Reached Resource Threshold", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "threshold": "90%", + "threshold_type": "memory usage", + "workspace": "bobby-workspace" + }, + "data": null + }, + "title": "Workspace \"bobby-workspace\" reached resource threshold", + "title_markdown": "Workspace \"bobby-workspace\" reached resource threshold", + "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the memory usage threshold set at 90%.", + "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the memory usage threshold set at **90%**." +} \ No newline at end of file From b3081de89a92d61b908744b861d4d42a9aeeed23 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 30 Jan 2025 09:40:23 +0000 Subject: [PATCH 02/37] chore: split out into two notifications --- .../000288_oom_and_ood_notification.down.sql | 1 + .../000288_oom_and_ood_notification.up.sql | 23 +++++- coderd/notifications/events.go | 19 ++--- coderd/notifications/notifications_test.go | 23 ++++-- .../TemplateWorkspaceOutOfDisk.html.golden | 79 +++++++++++++++++++ .../TemplateWorkspaceOutOfMemory.html.golden | 79 +++++++++++++++++++ .../TemplateWorkspaceOutOfDisk.json.golden | 28 +++++++ .../TemplateWorkspaceOutOfMemory.json.golden | 28 +++++++ 8 files changed, 263 insertions(+), 17 deletions(-) create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfMemory.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfMemory.json.golden diff --git a/coderd/database/migrations/000288_oom_and_ood_notification.down.sql b/coderd/database/migrations/000288_oom_and_ood_notification.down.sql index fcb420c575866..a7d54ccf6ec7a 100644 --- a/coderd/database/migrations/000288_oom_and_ood_notification.down.sql +++ b/coderd/database/migrations/000288_oom_and_ood_notification.down.sql @@ -1 +1,2 @@ +DELETE FROM notification_templates WHERE id = 'f047f6a3-5713-40f7-85aa-0394cce9fa3a'; DELETE FROM notification_templates WHERE id = 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a'; diff --git a/coderd/database/migrations/000288_oom_and_ood_notification.up.sql b/coderd/database/migrations/000288_oom_and_ood_notification.up.sql index 293b6850f272b..a8b7ce6b29987 100644 --- a/coderd/database/migrations/000288_oom_and_ood_notification.up.sql +++ b/coderd/database/migrations/000288_oom_and_ood_notification.up.sql @@ -2,10 +2,27 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ( 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a', - 'Workspace Reached Resource Threshold', - E'Workspace "{{.Labels.workspace}}" reached resource threshold', + 'Workspace Out Of Memory', + E'Your workspace "{{.Labels.workspace}}" is low on memory', E'Hi {{.UserName}},\n\n'|| - E'Your workspace **{{.Labels.workspace}}** has reached the {{.Labels.threshold_type}} threshold set at **{{.Labels.threshold}}**.', + E'Your workspace **{{.Labels.workspace}}** has reached the memory usage threshold set at **{{.Labels.threshold}}**.', + 'Workspace Events', + '[ + { + "label": "View workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + } + ]'::jsonb +); + +INSERT INTO notification_templates + (id, name, title_template, body_template, "group", actions) +VALUES ( + 'f047f6a3-5713-40f7-85aa-0394cce9fa3a', + 'Workspace Out Of Disk', + E'Your workspace "{{.Labels.workspace}}" is low on disk', + E'Hi {{.UserName}},\n\n'|| + E'Your workspace **{{.Labels.workspace}}** has reached the usage threshold set at **{{.Labels.threshold}}** for volume `{{.Labels.volume}}`.', 'Workspace Events', '[ { diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 5e50aaffc7129..5141f0f20cc52 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -7,15 +7,16 @@ import "github.com/google/uuid" // Workspace-related events. var ( - TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff") - TemplateWorkspaceManuallyUpdated = uuid.MustParse("d089fe7b-d5c5-4c0c-aaf5-689859f7d392") - TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed") - TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9") - TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0") - TemplateWorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b") - TemplateWorkspaceMarkedForDeletion = uuid.MustParse("51ce2fdf-c9ca-4be1-8d70-628674f9bc42") - TemplateWorkspaceManualBuildFailed = uuid.MustParse("2faeee0f-26cb-4e96-821c-85ccb9f71513") - TemplateWorkspaceReachedResourceThreshold = uuid.MustParse("a9d027b4-ac49-4fb1-9f6d-45af15f64e7a") + TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff") + TemplateWorkspaceManuallyUpdated = uuid.MustParse("d089fe7b-d5c5-4c0c-aaf5-689859f7d392") + TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed") + TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9") + TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0") + TemplateWorkspaceAutoUpdated = uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b") + TemplateWorkspaceMarkedForDeletion = uuid.MustParse("51ce2fdf-c9ca-4be1-8d70-628674f9bc42") + TemplateWorkspaceManualBuildFailed = uuid.MustParse("2faeee0f-26cb-4e96-821c-85ccb9f71513") + TemplateWorkspaceOutOfMemory = uuid.MustParse("a9d027b4-ac49-4fb1-9f6d-45af15f64e7a") + TemplateWorkspaceOutOfDisk = uuid.MustParse("f047f6a3-5713-40f7-85aa-0394cce9fa3a") ) // Account-related events. diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index bb57f9124958f..02e86af43b10b 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1065,16 +1065,29 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, { - name: "TemplateWorkspaceReachedResourceThreshold", - id: notifications.TemplateWorkspaceReachedResourceThreshold, + name: "TemplateWorkspaceOutOfMemory", + id: notifications.TemplateWorkspaceOutOfMemory, payload: types.MessagePayload{ UserName: "Bobby", UserEmail: "bobby@coder.com", UserUsername: "bobby", Labels: map[string]string{ - "workspace": "bobby-workspace", - "threshold_type": "memory usage", - "threshold": "90%", + "workspace": "bobby-workspace", + "threshold": "90%", + }, + }, + }, + { + name: "TemplateWorkspaceOutOfDisk", + id: notifications.TemplateWorkspaceOutOfDisk, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "workspace": "bobby-workspace", + "threshold": "90%", + "volume": "/home/coder", }, }, }, diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden new file mode 100644 index 0000000000000..beaa91315ebcc --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden @@ -0,0 +1,79 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Your workspace "bobby-workspace" is low on disk +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +Your workspace bobby-workspace has reached the volume usage threshold set a= +t 90%. + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Codestin Search App + + +
+
+ 3D"Cod= +
+

+ Your workspace "bobby-workspace" is low on disk +

+
+

Hi Bobby,

+ +

Your workspace bobby-workspace has reached the volume u= +sage threshold set at 90%.

+
+
+ =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfMemory.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfMemory.html.golden new file mode 100644 index 0000000000000..1aa27cb4cce89 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfMemory.html.golden @@ -0,0 +1,79 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Your workspace "bobby-workspace" is low on memory +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +Your workspace bobby-workspace has reached the memory usage threshold set a= +t 90%. + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Codestin Search App + + +
+
+ 3D"Cod= +
+

+ Your workspace "bobby-workspace" is low on memory +

+
+

Hi Bobby,

+ +

Your workspace bobby-workspace has reached the memory u= +sage threshold set at 90%.

+
+
+ =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden new file mode 100644 index 0000000000000..b6fbc3424408a --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden @@ -0,0 +1,28 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Out Of Disk", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "threshold": "90%", + "workspace": "bobby-workspace" + }, + "data": null + }, + "title": "Your workspace \"bobby-workspace\" is low on disk", + "title_markdown": "Your workspace \"bobby-workspace\" is low on disk", + "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the volume usage threshold set at 90%.", + "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the volume usage threshold set at **90%**." +} \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfMemory.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfMemory.json.golden new file mode 100644 index 0000000000000..a0fce437e3c56 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfMemory.json.golden @@ -0,0 +1,28 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Out Of Memory", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "threshold": "90%", + "workspace": "bobby-workspace" + }, + "data": null + }, + "title": "Your workspace \"bobby-workspace\" is low on memory", + "title_markdown": "Your workspace \"bobby-workspace\" is low on memory", + "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the memory usage threshold set at 90%.", + "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the memory usage threshold set at **90%**." +} \ No newline at end of file From a9c8676deb71bb5c600bbc2cadfeafcad7ea4d7c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 30 Jan 2025 09:53:52 +0000 Subject: [PATCH 03/37] chore: update golden files --- .../smtp/TemplateWorkspaceOutOfDisk.html.golden | 9 +++++---- .../webhook/TemplateWorkspaceOutOfDisk.json.golden | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden index beaa91315ebcc..542c1e4385b15 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden @@ -12,8 +12,8 @@ Content-Type: text/plain; charset=UTF-8 Hi Bobby, -Your workspace bobby-workspace has reached the volume usage threshold set a= -t 90%. +Your workspace bobby-workspace has reached the usage threshold set at 90% f= +or volume /home/coder. View workspace: http://test.com/@bobby/bobby-workspace @@ -48,8 +48,9 @@ argin: 8px 0 32px; line-height: 1.5;">

Hi Bobby,

-

Your workspace bobby-workspace has reached the volume u= -sage threshold set at 90%.

+

Your workspace bobby-workspace has reached the usage th= +reshold set at 90% for volume /home/coder.

=20 diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden index b6fbc3424408a..40dfbd0b75456 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden @@ -17,12 +17,13 @@ ], "labels": { "threshold": "90%", + "volume": "/home/coder", "workspace": "bobby-workspace" }, "data": null }, "title": "Your workspace \"bobby-workspace\" is low on disk", "title_markdown": "Your workspace \"bobby-workspace\" is low on disk", - "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the volume usage threshold set at 90%.", - "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the volume usage threshold set at **90%**." + "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the usage threshold set at 90% for volume /home/coder.", + "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the usage threshold set at **90%** for volume `/home/coder`." } \ No newline at end of file From 1a84f9684ecc2068220844a39b4c8210e7e745e8 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 29 Jan 2025 11:46:26 +0000 Subject: [PATCH 04/37] chore: begin impl of processing logic for oom/ood --- agent/agenttest/client.go | 8 + agent/proto/agent.pb.go | 673 ++++++++++++++---- agent/proto/agent.proto | 26 + agent/proto/agent_drpc.pb.go | 42 +- coderd/agentapi/api.go | 15 + coderd/agentapi/workspacemonitor.go | 279 ++++++++ coderd/agentapi/workspacemonitor_test.go | 352 +++++++++ coderd/database/dbauthz/dbauthz.go | 12 + coderd/database/dbmem/dbmem.go | 27 + coderd/database/dbmetrics/querymetrics.go | 21 + coderd/database/dbmock/dbmock.go | 44 ++ coderd/database/dump.sql | 21 + .../000289_create_workspace_monitors.down.sql | 2 + .../000289_create_workspace_monitors.up.sql | 19 + coderd/database/models.go | 126 ++++ coderd/database/querier.go | 3 + coderd/database/queries.sql.go | 110 +++ coderd/database/queries/workspacemonitors.sql | 31 + coderd/workspaceagentsrpc.go | 2 + 19 files changed, 1665 insertions(+), 148 deletions(-) create mode 100644 coderd/agentapi/workspacemonitor.go create mode 100644 coderd/agentapi/workspacemonitor_test.go create mode 100644 coderd/database/migrations/000289_create_workspace_monitors.down.sql create mode 100644 coderd/database/migrations/000289_create_workspace_monitors.up.sql create mode 100644 coderd/database/queries/workspacemonitors.sql diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 6b2581e7831f2..da5a5988cba2f 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -315,6 +315,14 @@ func (f *FakeAgentAPI) ScriptCompleted(_ context.Context, req *agentproto.Worksp return &agentproto.WorkspaceAgentScriptCompletedResponse{}, nil } +func (f *FakeAgentAPI) UpdateWorkspaceMonitor(_ context.Context, req *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { + f.Lock() + // TODO: Figure out a good way of mocking the logic + f.Unlock() + + return &agentproto.WorkspaceMonitorUpdateResponse{}, nil +} + func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI { return &FakeAgentAPI{ t: t, diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 8063b42f3b622..ff13f989b467d 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -2304,6 +2304,91 @@ func (x *Timing) GetStatus() Timing_Status { return Timing_OK } +type WorkspaceMonitorUpdateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Datapoints []*WorkspaceMonitorUpdateRequest_Datapoint `protobuf:"bytes,1,rep,name=datapoints,proto3" json:"datapoints,omitempty"` +} + +func (x *WorkspaceMonitorUpdateRequest) Reset() { + *x = WorkspaceMonitorUpdateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceMonitorUpdateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMonitorUpdateRequest) ProtoMessage() {} + +func (x *WorkspaceMonitorUpdateRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[28] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceMonitorUpdateRequest.ProtoReflect.Descriptor instead. +func (*WorkspaceMonitorUpdateRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{28} +} + +func (x *WorkspaceMonitorUpdateRequest) GetDatapoints() []*WorkspaceMonitorUpdateRequest_Datapoint { + if x != nil { + return x.Datapoints + } + return nil +} + +type WorkspaceMonitorUpdateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *WorkspaceMonitorUpdateResponse) Reset() { + *x = WorkspaceMonitorUpdateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceMonitorUpdateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMonitorUpdateResponse) ProtoMessage() {} + +func (x *WorkspaceMonitorUpdateResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[29] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceMonitorUpdateResponse.ProtoReflect.Descriptor instead. +func (*WorkspaceMonitorUpdateResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{29} +} + type WorkspaceApp_Healthcheck struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2317,7 +2402,7 @@ type WorkspaceApp_Healthcheck struct { func (x *WorkspaceApp_Healthcheck) Reset() { *x = WorkspaceApp_Healthcheck{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[28] + mi := &file_agent_proto_agent_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2330,7 +2415,7 @@ func (x *WorkspaceApp_Healthcheck) String() string { func (*WorkspaceApp_Healthcheck) ProtoMessage() {} func (x *WorkspaceApp_Healthcheck) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[28] + mi := &file_agent_proto_agent_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2381,7 +2466,7 @@ type WorkspaceAgentMetadata_Result struct { func (x *WorkspaceAgentMetadata_Result) Reset() { *x = WorkspaceAgentMetadata_Result{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[29] + mi := &file_agent_proto_agent_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2394,7 +2479,7 @@ func (x *WorkspaceAgentMetadata_Result) String() string { func (*WorkspaceAgentMetadata_Result) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Result) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[29] + mi := &file_agent_proto_agent_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2453,7 +2538,7 @@ type WorkspaceAgentMetadata_Description struct { func (x *WorkspaceAgentMetadata_Description) Reset() { *x = WorkspaceAgentMetadata_Description{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[30] + mi := &file_agent_proto_agent_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2466,7 +2551,7 @@ func (x *WorkspaceAgentMetadata_Description) String() string { func (*WorkspaceAgentMetadata_Description) ProtoMessage() {} func (x *WorkspaceAgentMetadata_Description) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[30] + mi := &file_agent_proto_agent_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2531,7 +2616,7 @@ type Stats_Metric struct { func (x *Stats_Metric) Reset() { *x = Stats_Metric{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[33] + mi := &file_agent_proto_agent_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2544,7 +2629,7 @@ func (x *Stats_Metric) String() string { func (*Stats_Metric) ProtoMessage() {} func (x *Stats_Metric) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[33] + mi := &file_agent_proto_agent_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2600,7 +2685,7 @@ type Stats_Metric_Label struct { func (x *Stats_Metric_Label) Reset() { *x = Stats_Metric_Label{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[34] + mi := &file_agent_proto_agent_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2613,7 +2698,7 @@ func (x *Stats_Metric_Label) String() string { func (*Stats_Metric_Label) ProtoMessage() {} func (x *Stats_Metric_Label) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[34] + mi := &file_agent_proto_agent_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2655,7 +2740,7 @@ type BatchUpdateAppHealthRequest_HealthUpdate struct { func (x *BatchUpdateAppHealthRequest_HealthUpdate) Reset() { *x = BatchUpdateAppHealthRequest_HealthUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_agent_proto_agent_proto_msgTypes[35] + mi := &file_agent_proto_agent_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2668,7 +2753,7 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) String() string { func (*BatchUpdateAppHealthRequest_HealthUpdate) ProtoMessage() {} func (x *BatchUpdateAppHealthRequest_HealthUpdate) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_agent_proto_msgTypes[35] + mi := &file_agent_proto_agent_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2698,6 +2783,187 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) GetHealth() AppHealth { return AppHealth_APP_HEALTH_UNSPECIFIED } +type WorkspaceMonitorUpdateRequest_Datapoint struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CollectedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=collected_at,json=collectedAt,proto3" json:"collected_at,omitempty"` + Memory *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` + Volume []*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage `protobuf:"bytes,3,rep,name=volume,proto3" json:"volume,omitempty"` +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint) Reset() { + *x = WorkspaceMonitorUpdateRequest_Datapoint{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMonitorUpdateRequest_Datapoint) ProtoMessage() {} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[38] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceMonitorUpdateRequest_Datapoint.ProtoReflect.Descriptor instead. +func (*WorkspaceMonitorUpdateRequest_Datapoint) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0} +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint) GetCollectedAt() *timestamppb.Timestamp { + if x != nil { + return x.CollectedAt + } + return nil +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint) GetMemory() *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage { + if x != nil { + return x.Memory + } + return nil +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint) GetVolume() []*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage { + if x != nil { + return x.Volume + } + return nil +} + +type WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + Used int32 `protobuf:"varint,2,opt,name=used,proto3" json:"used,omitempty"` + Total int32 `protobuf:"varint,3,opt,name=total,proto3" json:"total,omitempty"` +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) Reset() { + *x = WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) ProtoMessage() {} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[39] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage.ProtoReflect.Descriptor instead. +func (*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0, 0} +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) GetUsed() int32 { + if x != nil { + return x.Used + } + return 0 +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + +type WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Used int32 `protobuf:"varint,1,opt,name=used,proto3" json:"used,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) Reset() { + *x = WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{} + if protoimpl.UnsafeEnabled { + mi := &file_agent_proto_agent_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) ProtoMessage() {} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_agent_proto_msgTypes[40] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage.ProtoReflect.Descriptor instead. +func (*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) Descriptor() ([]byte, []int) { + return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0, 1} +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) GetUsed() int32 { + if x != nil { + return x.Used + } + return 0 +} + +func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + var File_agent_proto_agent_proto protoreflect.FileDescriptor var file_agent_proto_agent_proto_rawDesc = []byte{ @@ -3092,79 +3358,121 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, - 0x4e, 0x10, 0x03, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, - 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, - 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, - 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, - 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xef, 0x07, 0x0a, 0x05, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, - 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, - 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, - 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, - 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, - 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x4e, 0x10, 0x03, 0x22, 0x85, 0x04, 0x0a, 0x1d, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x57, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x8a, + 0x03, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x5b, 0x0a, 0x06, 0x6d, + 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, + 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x5b, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x06, 0x76, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x1a, 0x4b, 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x20, 0x0a, 0x1e, 0x57, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x63, 0x0a, + 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, + 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, + 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, + 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, + 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, + 0x10, 0x04, 0x32, 0xe8, 0x08, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, + 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, + 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, + 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, + 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, + 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, + 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, - 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, - 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, - 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, - 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, - 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, - 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, - 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, - 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, - 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, + 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, + 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, + 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, + 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x2d, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, + 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3180,7 +3488,7 @@ func file_agent_proto_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 9) -var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_agent_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 41) var file_agent_proto_agent_proto_goTypes = []interface{}{ (AppHealth)(0), // 0: coder.agent.v2.AppHealth (WorkspaceApp_SharingLevel)(0), // 1: coder.agent.v2.WorkspaceApp.SharingLevel @@ -3219,83 +3527,94 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ (*WorkspaceAgentScriptCompletedRequest)(nil), // 34: coder.agent.v2.WorkspaceAgentScriptCompletedRequest (*WorkspaceAgentScriptCompletedResponse)(nil), // 35: coder.agent.v2.WorkspaceAgentScriptCompletedResponse (*Timing)(nil), // 36: coder.agent.v2.Timing - (*WorkspaceApp_Healthcheck)(nil), // 37: coder.agent.v2.WorkspaceApp.Healthcheck - (*WorkspaceAgentMetadata_Result)(nil), // 38: coder.agent.v2.WorkspaceAgentMetadata.Result - (*WorkspaceAgentMetadata_Description)(nil), // 39: coder.agent.v2.WorkspaceAgentMetadata.Description - nil, // 40: coder.agent.v2.Manifest.EnvironmentVariablesEntry - nil, // 41: coder.agent.v2.Stats.ConnectionsByProtoEntry - (*Stats_Metric)(nil), // 42: coder.agent.v2.Stats.Metric - (*Stats_Metric_Label)(nil), // 43: coder.agent.v2.Stats.Metric.Label - (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 44: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - (*durationpb.Duration)(nil), // 45: google.protobuf.Duration - (*proto.DERPMap)(nil), // 46: coder.tailnet.v2.DERPMap - (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp + (*WorkspaceMonitorUpdateRequest)(nil), // 37: coder.agent.v2.WorkspaceMonitorUpdateRequest + (*WorkspaceMonitorUpdateResponse)(nil), // 38: coder.agent.v2.WorkspaceMonitorUpdateResponse + (*WorkspaceApp_Healthcheck)(nil), // 39: coder.agent.v2.WorkspaceApp.Healthcheck + (*WorkspaceAgentMetadata_Result)(nil), // 40: coder.agent.v2.WorkspaceAgentMetadata.Result + (*WorkspaceAgentMetadata_Description)(nil), // 41: coder.agent.v2.WorkspaceAgentMetadata.Description + nil, // 42: coder.agent.v2.Manifest.EnvironmentVariablesEntry + nil, // 43: coder.agent.v2.Stats.ConnectionsByProtoEntry + (*Stats_Metric)(nil), // 44: coder.agent.v2.Stats.Metric + (*Stats_Metric_Label)(nil), // 45: coder.agent.v2.Stats.Metric.Label + (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 46: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + (*WorkspaceMonitorUpdateRequest_Datapoint)(nil), // 47: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint + (*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage)(nil), // 48: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.VolumeUsage + (*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage)(nil), // 49: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.MemoryUsage + (*durationpb.Duration)(nil), // 50: google.protobuf.Duration + (*proto.DERPMap)(nil), // 51: coder.tailnet.v2.DERPMap + (*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp } var file_agent_proto_agent_proto_depIdxs = []int32{ 1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel - 37, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck + 39, // 1: coder.agent.v2.WorkspaceApp.healthcheck:type_name -> coder.agent.v2.WorkspaceApp.Healthcheck 2, // 2: coder.agent.v2.WorkspaceApp.health:type_name -> coder.agent.v2.WorkspaceApp.Health - 45, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration - 38, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result - 39, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 40, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry - 46, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap + 50, // 3: coder.agent.v2.WorkspaceAgentScript.timeout:type_name -> google.protobuf.Duration + 40, // 4: coder.agent.v2.WorkspaceAgentMetadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 41, // 5: coder.agent.v2.WorkspaceAgentMetadata.description:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 42, // 6: coder.agent.v2.Manifest.environment_variables:type_name -> coder.agent.v2.Manifest.EnvironmentVariablesEntry + 51, // 7: coder.agent.v2.Manifest.derp_map:type_name -> coder.tailnet.v2.DERPMap 10, // 8: coder.agent.v2.Manifest.scripts:type_name -> coder.agent.v2.WorkspaceAgentScript 9, // 9: coder.agent.v2.Manifest.apps:type_name -> coder.agent.v2.WorkspaceApp - 39, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description - 41, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry - 42, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric + 41, // 10: coder.agent.v2.Manifest.metadata:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Description + 43, // 11: coder.agent.v2.Stats.connections_by_proto:type_name -> coder.agent.v2.Stats.ConnectionsByProtoEntry + 44, // 12: coder.agent.v2.Stats.metrics:type_name -> coder.agent.v2.Stats.Metric 16, // 13: coder.agent.v2.UpdateStatsRequest.stats:type_name -> coder.agent.v2.Stats - 45, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration + 50, // 14: coder.agent.v2.UpdateStatsResponse.report_interval:type_name -> google.protobuf.Duration 4, // 15: coder.agent.v2.Lifecycle.state:type_name -> coder.agent.v2.Lifecycle.State - 47, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp + 52, // 16: coder.agent.v2.Lifecycle.changed_at:type_name -> google.protobuf.Timestamp 19, // 17: coder.agent.v2.UpdateLifecycleRequest.lifecycle:type_name -> coder.agent.v2.Lifecycle - 44, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + 46, // 18: coder.agent.v2.BatchUpdateAppHealthRequest.updates:type_name -> coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate 5, // 19: coder.agent.v2.Startup.subsystems:type_name -> coder.agent.v2.Startup.Subsystem 23, // 20: coder.agent.v2.UpdateStartupRequest.startup:type_name -> coder.agent.v2.Startup - 38, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result + 40, // 21: coder.agent.v2.Metadata.result:type_name -> coder.agent.v2.WorkspaceAgentMetadata.Result 25, // 22: coder.agent.v2.BatchUpdateMetadataRequest.metadata:type_name -> coder.agent.v2.Metadata - 47, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp + 52, // 23: coder.agent.v2.Log.created_at:type_name -> google.protobuf.Timestamp 6, // 24: coder.agent.v2.Log.level:type_name -> coder.agent.v2.Log.Level 28, // 25: coder.agent.v2.BatchCreateLogsRequest.logs:type_name -> coder.agent.v2.Log 33, // 26: coder.agent.v2.GetAnnouncementBannersResponse.announcement_banners:type_name -> coder.agent.v2.BannerConfig 36, // 27: coder.agent.v2.WorkspaceAgentScriptCompletedRequest.timing:type_name -> coder.agent.v2.Timing - 47, // 28: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp - 47, // 29: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp + 52, // 28: coder.agent.v2.Timing.start:type_name -> google.protobuf.Timestamp + 52, // 29: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp 7, // 30: coder.agent.v2.Timing.stage:type_name -> coder.agent.v2.Timing.Stage 8, // 31: coder.agent.v2.Timing.status:type_name -> coder.agent.v2.Timing.Status - 45, // 32: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration - 47, // 33: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp - 45, // 34: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration - 45, // 35: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration - 3, // 36: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type - 43, // 37: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label - 0, // 38: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth - 13, // 39: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest - 15, // 40: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest - 17, // 41: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest - 20, // 42: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest - 21, // 43: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest - 24, // 44: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest - 26, // 45: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest - 29, // 46: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest - 31, // 47: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest - 34, // 48: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest - 12, // 49: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest - 14, // 50: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner - 18, // 51: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse - 19, // 52: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle - 22, // 53: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse - 23, // 54: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup - 27, // 55: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse - 30, // 56: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse - 32, // 57: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse - 35, // 58: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse - 49, // [49:59] is the sub-list for method output_type - 39, // [39:49] is the sub-list for method input_type - 39, // [39:39] is the sub-list for extension type_name - 39, // [39:39] is the sub-list for extension extendee - 0, // [0:39] is the sub-list for field type_name + 47, // 32: coder.agent.v2.WorkspaceMonitorUpdateRequest.datapoints:type_name -> coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint + 50, // 33: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration + 52, // 34: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp + 50, // 35: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration + 50, // 36: coder.agent.v2.WorkspaceAgentMetadata.Description.timeout:type_name -> google.protobuf.Duration + 3, // 37: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type + 45, // 38: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label + 0, // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth + 52, // 40: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp + 49, // 41: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.memory:type_name -> coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.MemoryUsage + 48, // 42: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.volume:type_name -> coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.VolumeUsage + 13, // 43: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest + 15, // 44: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest + 17, // 45: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest + 20, // 46: coder.agent.v2.Agent.UpdateLifecycle:input_type -> coder.agent.v2.UpdateLifecycleRequest + 21, // 47: coder.agent.v2.Agent.BatchUpdateAppHealths:input_type -> coder.agent.v2.BatchUpdateAppHealthRequest + 24, // 48: coder.agent.v2.Agent.UpdateStartup:input_type -> coder.agent.v2.UpdateStartupRequest + 26, // 49: coder.agent.v2.Agent.BatchUpdateMetadata:input_type -> coder.agent.v2.BatchUpdateMetadataRequest + 29, // 50: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest + 31, // 51: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest + 34, // 52: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest + 37, // 53: coder.agent.v2.Agent.UpdateWorkspaceMonitor:input_type -> coder.agent.v2.WorkspaceMonitorUpdateRequest + 12, // 54: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest + 14, // 55: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner + 18, // 56: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse + 19, // 57: coder.agent.v2.Agent.UpdateLifecycle:output_type -> coder.agent.v2.Lifecycle + 22, // 58: coder.agent.v2.Agent.BatchUpdateAppHealths:output_type -> coder.agent.v2.BatchUpdateAppHealthResponse + 23, // 59: coder.agent.v2.Agent.UpdateStartup:output_type -> coder.agent.v2.Startup + 27, // 60: coder.agent.v2.Agent.BatchUpdateMetadata:output_type -> coder.agent.v2.BatchUpdateMetadataResponse + 30, // 61: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse + 32, // 62: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse + 35, // 63: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse + 38, // 64: coder.agent.v2.Agent.UpdateWorkspaceMonitor:output_type -> coder.agent.v2.WorkspaceMonitorUpdateResponse + 54, // [54:65] is the sub-list for method output_type + 43, // [43:54] is the sub-list for method input_type + 43, // [43:43] is the sub-list for extension type_name + 43, // [43:43] is the sub-list for extension extendee + 0, // [0:43] is the sub-list for field type_name } func init() { file_agent_proto_agent_proto_init() } @@ -3641,7 +3960,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceApp_Healthcheck); i { + switch v := v.(*WorkspaceMonitorUpdateRequest); i { case 0: return &v.state case 1: @@ -3653,7 +3972,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceAgentMetadata_Result); i { + switch v := v.(*WorkspaceMonitorUpdateResponse); i { case 0: return &v.state case 1: @@ -3665,6 +3984,30 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceApp_Healthcheck); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceAgentMetadata_Result); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*WorkspaceAgentMetadata_Description); i { case 0: return &v.state @@ -3676,7 +4019,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric); i { case 0: return &v.state @@ -3688,7 +4031,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Stats_Metric_Label); i { case 0: return &v.state @@ -3700,7 +4043,7 @@ func file_agent_proto_agent_proto_init() { return nil } } - file_agent_proto_agent_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + file_agent_proto_agent_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*BatchUpdateAppHealthRequest_HealthUpdate); i { case 0: return &v.state @@ -3712,6 +4055,42 @@ func file_agent_proto_agent_proto_init() { return nil } } + file_agent_proto_agent_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceMonitorUpdateRequest_Datapoint); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_agent_proto_agent_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -3719,7 +4098,7 @@ func file_agent_proto_agent_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_agent_proto_agent_proto_rawDesc, NumEnums: 9, - NumMessages: 36, + NumMessages: 41, NumExtensions: 0, NumServices: 1, }, diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index f307066fcbfdf..c187e289f8131 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -295,6 +295,31 @@ message Timing { Status status = 6; } +message WorkspaceMonitorUpdateRequest { + message Datapoint { + message VolumeUsage { + string path = 1; + int32 used = 2; + int32 total = 3; + } + + message MemoryUsage { + int32 used = 1; + int32 total = 2; + } + + google.protobuf.Timestamp collected_at = 1; + MemoryUsage memory = 2; + repeated VolumeUsage volume = 3; + } + + repeated Datapoint datapoints = 1; +} + +message WorkspaceMonitorUpdateResponse { + +} + service Agent { rpc GetManifest(GetManifestRequest) returns (Manifest); rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner); @@ -306,4 +331,5 @@ service Agent { rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse); rpc GetAnnouncementBanners(GetAnnouncementBannersRequest) returns (GetAnnouncementBannersResponse); rpc ScriptCompleted(WorkspaceAgentScriptCompletedRequest) returns (WorkspaceAgentScriptCompletedResponse); + rpc UpdateWorkspaceMonitor(WorkspaceMonitorUpdateRequest) returns (WorkspaceMonitorUpdateResponse); } diff --git a/agent/proto/agent_drpc.pb.go b/agent/proto/agent_drpc.pb.go index 7bb1957230d76..a86b03cfaef25 100644 --- a/agent/proto/agent_drpc.pb.go +++ b/agent/proto/agent_drpc.pb.go @@ -48,6 +48,7 @@ type DRPCAgentClient interface { BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) + UpdateWorkspaceMonitor(ctx context.Context, in *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) } type drpcAgentClient struct { @@ -150,6 +151,15 @@ func (c *drpcAgentClient) ScriptCompleted(ctx context.Context, in *WorkspaceAgen return out, nil } +func (c *drpcAgentClient) UpdateWorkspaceMonitor(ctx context.Context, in *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) { + out := new(WorkspaceMonitorUpdateResponse) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/UpdateWorkspaceMonitor", drpcEncoding_File_agent_proto_agent_proto{}, in, out) + if err != nil { + return nil, err + } + return out, nil +} + type DRPCAgentServer interface { GetManifest(context.Context, *GetManifestRequest) (*Manifest, error) GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error) @@ -161,6 +171,7 @@ type DRPCAgentServer interface { BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) ScriptCompleted(context.Context, *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) + UpdateWorkspaceMonitor(context.Context, *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) } type DRPCAgentUnimplementedServer struct{} @@ -205,9 +216,13 @@ func (s *DRPCAgentUnimplementedServer) ScriptCompleted(context.Context, *Workspa return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } +func (s *DRPCAgentUnimplementedServer) UpdateWorkspaceMonitor(context.Context, *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) { + return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) +} + type DRPCAgentDescription struct{} -func (DRPCAgentDescription) NumMethods() int { return 10 } +func (DRPCAgentDescription) NumMethods() int { return 11 } func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) { switch n { @@ -301,6 +316,15 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, in1.(*WorkspaceAgentScriptCompletedRequest), ) }, DRPCAgentServer.ScriptCompleted, true + case 10: + return "/coder.agent.v2.Agent/UpdateWorkspaceMonitor", drpcEncoding_File_agent_proto_agent_proto{}, + func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { + return srv.(DRPCAgentServer). + UpdateWorkspaceMonitor( + ctx, + in1.(*WorkspaceMonitorUpdateRequest), + ) + }, DRPCAgentServer.UpdateWorkspaceMonitor, true default: return "", nil, nil, nil, false } @@ -469,3 +493,19 @@ func (x *drpcAgent_ScriptCompletedStream) SendAndClose(m *WorkspaceAgentScriptCo } return x.CloseSend() } + +type DRPCAgent_UpdateWorkspaceMonitorStream interface { + drpc.Stream + SendAndClose(*WorkspaceMonitorUpdateResponse) error +} + +type drpcAgent_UpdateWorkspaceMonitorStream struct { + drpc.Stream +} + +func (x *drpcAgent_UpdateWorkspaceMonitorStream) SendAndClose(m *WorkspaceMonitorUpdateResponse) error { + if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { + return err + } + return x.CloseSend() +} diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 62fe6fad8d4de..8e0d480c3312f 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -21,6 +21,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/externalauth" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/prometheusmetrics" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspacestats" @@ -29,6 +30,7 @@ import ( "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/tailnet" tailnetproto "github.com/coder/coder/v2/tailnet/proto" + "github.com/coder/quartz" ) // API implements the DRPC agent API interface from agent/proto. This struct is @@ -44,6 +46,7 @@ type API struct { *MetadataAPI *LogsAPI *ScriptsAPI + *WorkspaceMonitorAPI *tailnet.DRPCService mu sync.Mutex @@ -58,7 +61,9 @@ type Options struct { Ctx context.Context Log slog.Logger + Clock quartz.Clock Database database.Store + NotificationsEnqueuer notifications.Enqueuer Pubsub pubsub.Pubsub DerpMapFn func() *tailcfg.DERPMap TailnetCoordinator *atomic.Pointer[tailnet.Coordinator] @@ -81,6 +86,10 @@ type Options struct { } func New(opts Options) *API { + if opts.Clock == nil { + opts.Clock = quartz.NewReal() + } + api := &API{ opts: opts, mu: sync.Mutex{}, @@ -145,6 +154,12 @@ func New(opts Options) *API { Database: opts.Database, } + api.WorkspaceMonitorAPI = &WorkspaceMonitorAPI{ + Clock: opts.Clock, + Database: opts.Database, + NotificationsEnqueuer: opts.NotificationsEnqueuer, + } + api.DRPCService = &tailnet.DRPCService{ CoordPtr: opts.TailnetCoordinator, Logger: opts.Log, diff --git a/coderd/agentapi/workspacemonitor.go b/coderd/agentapi/workspacemonitor.go new file mode 100644 index 0000000000000..24531efc9579d --- /dev/null +++ b/coderd/agentapi/workspacemonitor.go @@ -0,0 +1,279 @@ +package agentapi + +import ( + "context" + "database/sql" + "slices" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbtime" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/quartz" + "github.com/google/uuid" + "golang.org/x/xerrors" +) + +type WorkspaceMonitorAPI struct { + WorkspaceID uuid.UUID + + Clock quartz.Clock + Database database.Store + NotificationsEnqueuer notifications.Enqueuer + + MemoryMonitorEnabled bool + MemoryUsageThreshold int32 + VolumeUsageThresholds map[string]int32 + + // How many datapoints in a row are required to + // put the monitor in an alert state. + ConsecutiveNOKs int + + // How many datapoints in total are required to + // put the monitor in an alert state. + MinimumNOKs int +} + +func (m *WorkspaceMonitorAPI) UpdateWorkspaceMonitor(ctx context.Context, req *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { + res := &agentproto.WorkspaceMonitorUpdateResponse{} + + if m.MemoryMonitorEnabled { + if err := m.monitorMemory(ctx, req.Datapoints); err != nil { + return nil, xerrors.Errorf("monitor memory: %w", err) + } + } + + if err := m.monitorVolumes(ctx, req.Datapoints); err != nil { + return nil, xerrors.Errorf("monitor volumes: %w", err) + } + + return res, nil +} + +func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint) error { + memoryMonitor, err := m.getOrInsertMemoryMonitor(ctx) + if err != nil { + return xerrors.Errorf("get or insert memory monitor: %w", err) + } + + memoryUsageDatapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage, 0, len(datapoints)) + for _, datapoint := range datapoints { + memoryUsageDatapoints = append(memoryUsageDatapoints, datapoint.Memory) + } + + memoryUsageStates := m.calculateMemoryUsageStates(memoryUsageDatapoints) + + oldState := memoryMonitor.State + newState := m.nextState(oldState, memoryUsageStates) + + err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + State: newState, + UpdatedAt: dbtime.Time(m.Clock.Now()), + }) + if err != nil { + return xerrors.Errorf("update workspace monitor: %w", err) + } + + if oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK { + workspace, err := m.Database.GetWorkspaceByID(ctx, m.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by id: %w", err) + } + + _, err = m.NotificationsEnqueuer.Enqueue( + dbauthz.AsNotifier(ctx), + workspace.OwnerID, + notifications.TemplateWorkspaceReachedResourceThreshold, + map[string]string{}, + "workspace-monitor-memory", + ) + if err != nil { + return xerrors.Errorf("notify workspace OOM: %w", err) + } + } + + return nil +} + +func (m *WorkspaceMonitorAPI) getOrInsertMemoryMonitor(ctx context.Context) (database.WorkspaceMonitor, error) { + memoryMonitor, err := m.Database.GetWorkspaceMonitor(ctx, database.GetWorkspaceMonitorParams{ + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + }) + if err != nil { + if xerrors.Is(err, sql.ErrNoRows) { + return m.Database.InsertWorkspaceMonitor( + ctx, + database.InsertWorkspaceMonitorParams{ + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + State: database.WorkspaceMonitorStateOK, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + DebouncedUntil: dbtime.Now(), + }, + ) + } + + return database.WorkspaceMonitor{}, err + } + + return memoryMonitor, nil +} + +func (m *WorkspaceMonitorAPI) monitorVolumes(ctx context.Context, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint) error { + volumes := make(map[string][]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) + + for _, datapoint := range datapoints { + for _, volume := range datapoint.Volume { + volumeDatapoints := volumes[volume.Path] + volumeDatapoints = append(volumeDatapoints, volume) + volumes[volume.Path] = volumeDatapoints + } + } + + for path, volume := range volumes { + if err := m.monitorVolume(ctx, path, volume); err != nil { + return xerrors.Errorf("monitor volume: %w", err) + } + } + + return nil +} + +func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) error { + volumeMonitor, err := m.getOrInsertVolumeMonitor(ctx, path) + if err != nil { + return xerrors.Errorf("get or insert volume monitor: %w", err) + } + + volumeUsageStates := m.calculateVolumeUsageStates(path, datapoints) + + oldState := volumeMonitor.State + newState := m.nextState(oldState, volumeUsageStates) + + err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: path}, + State: newState, + UpdatedAt: dbtime.Time(m.Clock.Now()), + }) + if err != nil { + return xerrors.Errorf("update workspace monitor: %w", err) + } + + if oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK { + workspace, err := m.Database.GetWorkspaceByID(ctx, m.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by id: %w", err) + } + + _, err = m.NotificationsEnqueuer.Enqueue( + dbauthz.AsNotifier(ctx), + workspace.OwnerID, + notifications.TemplateWorkspaceReachedResourceThreshold, + map[string]string{}, + "workspace-monitor-memory", + ) + if err != nil { + return xerrors.Errorf("notify workspace OOM: %w", err) + } + } + + return nil +} + +func (m *WorkspaceMonitorAPI) getOrInsertVolumeMonitor(ctx context.Context, path string) (database.WorkspaceMonitor, error) { + memoryMonitor, err := m.Database.GetWorkspaceMonitor(ctx, database.GetWorkspaceMonitorParams{ + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: path}, + }) + if err != nil { + if xerrors.Is(err, sql.ErrNoRows) { + return m.Database.InsertWorkspaceMonitor( + ctx, + database.InsertWorkspaceMonitorParams{ + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: path}, + State: database.WorkspaceMonitorStateOK, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + DebouncedUntil: dbtime.Now(), + }, + ) + } + + return database.WorkspaceMonitor{}, err + } + + return memoryMonitor, nil +} + +func (m *WorkspaceMonitorAPI) nextState(oldState database.WorkspaceMonitorState, states []database.WorkspaceMonitorState) database.WorkspaceMonitorState { + // If we do not have an OK in the last `X` datapoints, then we are + // in an alert state. + lastXStates := states[len(states)-m.ConsecutiveNOKs:] + if !slices.Contains(lastXStates, database.WorkspaceMonitorStateOK) { + return database.WorkspaceMonitorStateNOK + } + + nokCount := 0 + for _, state := range states { + if state == database.WorkspaceMonitorStateNOK { + nokCount += 1 + } + } + + // If there are enough NOK datapoints, we should be in an alert state. + if nokCount >= m.MinimumNOKs { + return database.WorkspaceMonitorStateNOK + } + + // If there are no NOK datapoints, we should be in an OK state. + if nokCount == 0 { + return database.WorkspaceMonitorStateOK + } + + // Otherwise we stay in the same state as last. + return oldState +} + +func (m *WorkspaceMonitorAPI) calculateMemoryUsageStates(datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) []database.WorkspaceMonitorState { + states := make([]database.WorkspaceMonitorState, 0, len(datapoints)) + + for _, datapoint := range datapoints { + percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) + + state := database.WorkspaceMonitorStateOK + if percent >= m.MemoryUsageThreshold { + state = database.WorkspaceMonitorStateNOK + } + + states = append(states, state) + } + + return states +} + +func (m *WorkspaceMonitorAPI) calculateVolumeUsageStates(path string, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) []database.WorkspaceMonitorState { + states := make([]database.WorkspaceMonitorState, 0, len(datapoints)) + + for _, datapoint := range datapoints { + percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) + + state := database.WorkspaceMonitorStateOK + if percent >= m.VolumeUsageThresholds[path] { + state = database.WorkspaceMonitorStateNOK + } + + states = append(states, state) + } + + return states +} diff --git a/coderd/agentapi/workspacemonitor_test.go b/coderd/agentapi/workspacemonitor_test.go new file mode 100644 index 0000000000000..686c612142d06 --- /dev/null +++ b/coderd/agentapi/workspacemonitor_test.go @@ -0,0 +1,352 @@ +package agentapi_test + +import ( + "context" + "database/sql" + "testing" + "time" + + agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/notifications/notificationstest" + "github.com/coder/quartz" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestWorkspaceMemoryMonitor(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + memoryUsage []int32 + memoryTotal int32 + thresholdPercent int32 + minimumNOKs int + consecutiveNOKs int + previousState database.WorkspaceMonitorState + expectState database.WorkspaceMonitorState + shouldNotify bool + }{ + { + name: "WhenOK/NeverExceedsThreshold", + memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateOK, + expectState: database.WorkspaceMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenOK/ConsecutiveExceedsThreshold", + memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenOK/MinimumExceedsThreshold", + memoryUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryTotal: 10, + thresholdPercent: 80, + minimumNOKs: 4, + consecutiveNOKs: 10, + previousState: database.WorkspaceMonitorStateOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenNOK/NeverExceedsThreshold", + memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateNOK, + expectState: database.WorkspaceMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenNOK/ConsecutiveExceedsThreshold", + memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateNOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: false, + }, + { + name: "WhenNOK/MinimumExceedsThreshold", + memoryUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryTotal: 10, + thresholdPercent: 80, + minimumNOKs: 4, + consecutiveNOKs: 10, + previousState: database.WorkspaceMonitorStateNOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + notifyEnq := notificationstest.FakeEnqueuer{} + mDB := dbmock.NewMockStore(gomock.NewController(t)) + clock := quartz.NewMock(t) + api := &agentapi.WorkspaceMonitorAPI{ + WorkspaceID: uuid.New(), + Clock: clock, + Database: mDB, + NotificationsEnqueuer: ¬ifyEnq, + MinimumNOKs: tt.minimumNOKs, + ConsecutiveNOKs: tt.consecutiveNOKs, + MemoryMonitorEnabled: true, + MemoryUsageThreshold: tt.thresholdPercent, + } + + datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.memoryUsage)) + collectedAt := clock.Now() + for _, usage := range tt.memoryUsage { + collectedAt = collectedAt.Add(15 * time.Second) + datapoints = append(datapoints, &agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + CollectedAt: timestamppb.New(collectedAt), + Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Used: usage, + Total: tt.memoryTotal, + }, + }) + } + + ownerID := uuid.New() + + mDB.EXPECT().GetWorkspaceMonitor(gomock.Any(), database.GetWorkspaceMonitorParams{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + }).Return(database.WorkspaceMonitor{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + State: tt.previousState, + }, nil) + + mDB.EXPECT().UpdateWorkspaceMonitor(gomock.Any(), database.UpdateWorkspaceMonitorParams{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + State: tt.expectState, + UpdatedAt: timestamppb.New(collectedAt).AsTime(), + }) + + if tt.shouldNotify { + mDB.EXPECT().GetWorkspaceByID(gomock.Any(), api.WorkspaceID).Return(database.Workspace{ + ID: api.WorkspaceID, + OwnerID: ownerID, + }, nil) + } + + clock.Set(collectedAt) + _, err := api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: datapoints, + }) + require.NoError(t, err) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceReachedResourceThreshold)) + if tt.shouldNotify { + require.Len(t, sent, 1) + require.Equal(t, ownerID, sent[0].UserID) + } else { + require.Len(t, sent, 0) + } + }) + } + +} + +func TestWorkspaceVolumeMonitor(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + volumePath string + volumeUsage []int32 + volumeTotal int32 + thresholdPercent int32 + previousState database.WorkspaceMonitorState + expectState database.WorkspaceMonitorState + shouldNotify bool + minimumNOKs int + consecutiveNOKs int + }{ + { + name: "WhenOK/NeverExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateOK, + expectState: database.WorkspaceMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenOK/ConsecutiveExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenOK/MinimumExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + minimumNOKs: 4, + consecutiveNOKs: 10, + previousState: database.WorkspaceMonitorStateOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: true, + }, + { + name: "WhenNOK/NeverExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateNOK, + expectState: database.WorkspaceMonitorStateOK, + shouldNotify: false, + }, + { + name: "WhenNOK/ConsecutiveExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceMonitorStateNOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: false, + }, + { + name: "WhenNOK/MinimumExceedsThreshold", + volumePath: "/home/coder", + volumeUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + volumeTotal: 10, + thresholdPercent: 80, + minimumNOKs: 4, + consecutiveNOKs: 10, + previousState: database.WorkspaceMonitorStateNOK, + expectState: database.WorkspaceMonitorStateNOK, + shouldNotify: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + notifyEnq := notificationstest.FakeEnqueuer{} + mDB := dbmock.NewMockStore(gomock.NewController(t)) + clock := quartz.NewMock(t) + api := &agentapi.WorkspaceMonitorAPI{ + WorkspaceID: uuid.New(), + Clock: clock, + Database: mDB, + NotificationsEnqueuer: ¬ifyEnq, + MinimumNOKs: tt.minimumNOKs, + ConsecutiveNOKs: tt.consecutiveNOKs, + VolumeUsageThresholds: map[string]int32{ + tt.volumePath: tt.thresholdPercent, + }, + } + + datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.volumeUsage)) + collectedAt := clock.Now() + for _, volumeUsage := range tt.volumeUsage { + collectedAt = collectedAt.Add(15 * time.Second) + + volumeDatapoints := []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + { + Path: tt.volumePath, + Used: volumeUsage, + Total: tt.volumeTotal, + }, + } + + datapoints = append(datapoints, &agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + CollectedAt: timestamppb.New(collectedAt), + Volume: volumeDatapoints, + }) + } + + ownerID := uuid.New() + + mDB.EXPECT().GetWorkspaceMonitor(gomock.Any(), database.GetWorkspaceMonitorParams{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, + }).Return(database.WorkspaceMonitor{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, + State: tt.previousState, + }, nil) + + mDB.EXPECT().UpdateWorkspaceMonitor(gomock.Any(), database.UpdateWorkspaceMonitorParams{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, + State: tt.expectState, + UpdatedAt: timestamppb.New(collectedAt).AsTime(), + }) + + if tt.shouldNotify { + mDB.EXPECT().GetWorkspaceByID(gomock.Any(), api.WorkspaceID).Return(database.Workspace{ + ID: api.WorkspaceID, + OwnerID: ownerID, + }, nil) + } + + clock.Set(collectedAt) + _, err := api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: datapoints, + }) + require.NoError(t, err) + + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceReachedResourceThreshold)) + if tt.shouldNotify { + require.Len(t, sent, 1) + require.Equal(t, ownerID, sent[0].UserID) + } else { + require.Len(t, sent, 0) + } + }) + } +} diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 2e12cab9d33e0..eac95a6266d1c 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2740,6 +2740,10 @@ func (q *querier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt return q.db.GetWorkspaceModulesCreatedAfter(ctx, createdAt) } +func (q *querier) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + panic("not implemented") +} + func (q *querier) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, func(ctx context.Context, _ interface{}) ([]database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxies(ctx) @@ -3318,6 +3322,10 @@ func (q *querier) InsertWorkspaceModule(ctx context.Context, arg database.Insert return q.db.InsertWorkspaceModule(ctx, arg) } +func (q *querier) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + panic("not implemented") +} + func (q *querier) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { return insert(q.log, q.auth, rbac.ResourceWorkspaceProxy, q.db.InsertWorkspaceProxy)(ctx, arg) } @@ -4137,6 +4145,10 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg) } +func (q *querier) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { + panic("not implemented") +} + func (q *querier) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index b5d3280adde2a..1b3ebe162074c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7143,6 +7143,15 @@ func (q *FakeQuerier) GetWorkspaceModulesCreatedAfter(_ context.Context, created return modules, nil } +func (q *FakeQuerier) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.WorkspaceMonitor{}, err + } + + panic("not implemented") +} + func (q *FakeQuerier) GetWorkspaceProxies(_ context.Context) ([]database.WorkspaceProxy, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -8752,6 +8761,15 @@ func (q *FakeQuerier) InsertWorkspaceModule(_ context.Context, arg database.Inse return workspaceModule, nil } +func (q *FakeQuerier) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.WorkspaceMonitor{}, err + } + + panic("not implemented") +} + func (q *FakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -10510,6 +10528,15 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } +func (q *FakeQuerier) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + panic("not implemented") +} + func (q *FakeQuerier) UpdateWorkspaceNextStartAt(_ context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index ba8a1f9cdc8a6..9ccdd086d7cc2 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1624,6 +1624,13 @@ func (m queryMetricsStore) GetWorkspaceModulesCreatedAfter(ctx context.Context, return r0, r1 } +func (m queryMetricsStore) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("GetWorkspaceMonitor").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { start := time.Now() proxies, err := m.s.GetWorkspaceProxies(ctx) @@ -2065,6 +2072,13 @@ func (m queryMetricsStore) InsertWorkspaceModule(ctx context.Context, arg databa return r0, r1 } +func (m queryMetricsStore) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + start := time.Now() + r0, r1 := m.s.InsertWorkspaceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("InsertWorkspaceMonitor").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { start := time.Now() proxy, err := m.s.InsertWorkspaceProxy(ctx, arg) @@ -2590,6 +2604,13 @@ func (m queryMetricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg da return err } +func (m queryMetricsStore) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { + start := time.Now() + r0 := m.s.UpdateWorkspaceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateWorkspaceMonitor").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { start := time.Now() r0 := m.s.UpdateWorkspaceNextStartAt(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index b7460f1adc69c..0c00b5577a279 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3425,6 +3425,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceModulesCreatedAfter(arg0, arg1 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceModulesCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceModulesCreatedAfter), arg0, arg1) } +// GetWorkspaceMonitor mocks base method. +func (m *MockStore) GetWorkspaceMonitor(arg0 context.Context, arg1 database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceMonitor", arg0, arg1) + ret0, _ := ret[0].(database.WorkspaceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceMonitor indicates an expected call of GetWorkspaceMonitor. +func (mr *MockStoreMockRecorder) GetWorkspaceMonitor(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).GetWorkspaceMonitor), arg0, arg1) +} + // GetWorkspaceProxies mocks base method. func (m *MockStore) GetWorkspaceProxies(arg0 context.Context) ([]database.WorkspaceProxy, error) { m.ctrl.T.Helper() @@ -4372,6 +4387,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceModule(arg0, arg1 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceModule", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceModule), arg0, arg1) } +// InsertWorkspaceMonitor mocks base method. +func (m *MockStore) InsertWorkspaceMonitor(arg0 context.Context, arg1 database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertWorkspaceMonitor", arg0, arg1) + ret0, _ := ret[0].(database.WorkspaceMonitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertWorkspaceMonitor indicates an expected call of InsertWorkspaceMonitor. +func (mr *MockStoreMockRecorder) InsertWorkspaceMonitor(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceMonitor), arg0, arg1) +} + // InsertWorkspaceProxy mocks base method. func (m *MockStore) InsertWorkspaceProxy(arg0 context.Context, arg1 database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { m.ctrl.T.Helper() @@ -5488,6 +5518,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 any) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1) } +// UpdateWorkspaceMonitor mocks base method. +func (m *MockStore) UpdateWorkspaceMonitor(arg0 context.Context, arg1 database.UpdateWorkspaceMonitorParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateWorkspaceMonitor", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateWorkspaceMonitor indicates an expected call of UpdateWorkspaceMonitor. +func (mr *MockStoreMockRecorder) UpdateWorkspaceMonitor(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceMonitor), arg0, arg1) +} + // UpdateWorkspaceNextStartAt mocks base method. func (m *MockStore) UpdateWorkspaceNextStartAt(arg0 context.Context, arg1 database.UpdateWorkspaceNextStartAtParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index c241548e166c2..f2fdfcc3ea21b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -275,6 +275,16 @@ CREATE TYPE workspace_app_open_in AS ENUM ( 'slim-window' ); +CREATE TYPE workspace_monitor_state AS ENUM ( + 'OK', + 'NOK' +); + +CREATE TYPE workspace_monitor_type AS ENUM ( + 'memory', + 'volume' +); + CREATE TYPE workspace_transition AS ENUM ( 'start', 'stop', @@ -1742,6 +1752,17 @@ CREATE TABLE workspace_modules ( created_at timestamp with time zone NOT NULL ); +CREATE TABLE workspace_monitors ( + workspace_id uuid NOT NULL, + monitor_type workspace_monitor_type NOT NULL, + volume_path text, + state workspace_monitor_state NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + debounced_until timestamp with time zone NOT NULL, + CONSTRAINT workspace_monitors_monitor_type_check CHECK ((monitor_type = 'volume'::workspace_monitor_type)) +); + CREATE TABLE workspace_proxies ( id uuid NOT NULL, name text NOT NULL, diff --git a/coderd/database/migrations/000289_create_workspace_monitors.down.sql b/coderd/database/migrations/000289_create_workspace_monitors.down.sql new file mode 100644 index 0000000000000..bcc0a2bea4375 --- /dev/null +++ b/coderd/database/migrations/000289_create_workspace_monitors.down.sql @@ -0,0 +1,2 @@ +DROP TABLE workspace_monitors; +DROP TYPE workspace_monitor_state; diff --git a/coderd/database/migrations/000289_create_workspace_monitors.up.sql b/coderd/database/migrations/000289_create_workspace_monitors.up.sql new file mode 100644 index 0000000000000..236b515bf08ef --- /dev/null +++ b/coderd/database/migrations/000289_create_workspace_monitors.up.sql @@ -0,0 +1,19 @@ +CREATE TYPE workspace_monitor_state AS ENUM ( + 'OK', + 'NOK' +); + +CREATE TYPE workspace_monitor_type AS ENUM ( + 'memory', + 'volume' +); + +CREATE TABLE workspace_monitors ( + workspace_id uuid NOT NULL, + monitor_type workspace_monitor_type NOT NULL, + volume_path text CHECK (monitor_type = 'volume'), + state workspace_monitor_state NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + debounced_until timestamp with time zone NOT NULL +); diff --git a/coderd/database/models.go b/coderd/database/models.go index 4898b7f9e9cf6..ec4d28172a665 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2274,6 +2274,122 @@ func AllWorkspaceAppOpenInValues() []WorkspaceAppOpenIn { } } +type WorkspaceMonitorState string + +const ( + WorkspaceMonitorStateOK WorkspaceMonitorState = "OK" + WorkspaceMonitorStateNOK WorkspaceMonitorState = "NOK" +) + +func (e *WorkspaceMonitorState) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = WorkspaceMonitorState(s) + case string: + *e = WorkspaceMonitorState(s) + default: + return fmt.Errorf("unsupported scan type for WorkspaceMonitorState: %T", src) + } + return nil +} + +type NullWorkspaceMonitorState struct { + WorkspaceMonitorState WorkspaceMonitorState `json:"workspace_monitor_state"` + Valid bool `json:"valid"` // Valid is true if WorkspaceMonitorState is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullWorkspaceMonitorState) Scan(value interface{}) error { + if value == nil { + ns.WorkspaceMonitorState, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.WorkspaceMonitorState.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullWorkspaceMonitorState) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.WorkspaceMonitorState), nil +} + +func (e WorkspaceMonitorState) Valid() bool { + switch e { + case WorkspaceMonitorStateOK, + WorkspaceMonitorStateNOK: + return true + } + return false +} + +func AllWorkspaceMonitorStateValues() []WorkspaceMonitorState { + return []WorkspaceMonitorState{ + WorkspaceMonitorStateOK, + WorkspaceMonitorStateNOK, + } +} + +type WorkspaceMonitorType string + +const ( + WorkspaceMonitorTypeMemory WorkspaceMonitorType = "memory" + WorkspaceMonitorTypeVolume WorkspaceMonitorType = "volume" +) + +func (e *WorkspaceMonitorType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = WorkspaceMonitorType(s) + case string: + *e = WorkspaceMonitorType(s) + default: + return fmt.Errorf("unsupported scan type for WorkspaceMonitorType: %T", src) + } + return nil +} + +type NullWorkspaceMonitorType struct { + WorkspaceMonitorType WorkspaceMonitorType `json:"workspace_monitor_type"` + Valid bool `json:"valid"` // Valid is true if WorkspaceMonitorType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullWorkspaceMonitorType) Scan(value interface{}) error { + if value == nil { + ns.WorkspaceMonitorType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.WorkspaceMonitorType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullWorkspaceMonitorType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.WorkspaceMonitorType), nil +} + +func (e WorkspaceMonitorType) Valid() bool { + switch e { + case WorkspaceMonitorTypeMemory, + WorkspaceMonitorTypeVolume: + return true + } + return false +} + +func AllWorkspaceMonitorTypeValues() []WorkspaceMonitorType { + return []WorkspaceMonitorType{ + WorkspaceMonitorTypeMemory, + WorkspaceMonitorTypeVolume, + } +} + type WorkspaceTransition string const ( @@ -3314,6 +3430,16 @@ type WorkspaceModule struct { CreatedAt time.Time `db:"created_at" json:"created_at"` } +type WorkspaceMonitor struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` + VolumePath sql.NullString `db:"volume_path" json:"volume_path"` + State WorkspaceMonitorState `db:"state" json:"state"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + type WorkspaceProxy struct { ID uuid.UUID `db:"id" json:"id"` Name string `db:"name" json:"name"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 1b7d299ba7975..a6141f9b061d7 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -346,6 +346,7 @@ type sqlcQuerier interface { GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceModule, error) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceModule, error) + GetWorkspaceMonitor(ctx context.Context, arg GetWorkspaceMonitorParams) (WorkspaceMonitor, error) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) // Finds a workspace proxy that has an access URL or app hostname that matches // the provided hostname. This is to check if a hostname matches any workspace @@ -429,6 +430,7 @@ type sqlcQuerier interface { InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error) + InsertWorkspaceMonitor(ctx context.Context, arg InsertWorkspaceMonitorParams) (WorkspaceMonitor, error) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) @@ -515,6 +517,7 @@ type sqlcQuerier interface { UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (WorkspaceTable, error) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error + UpdateWorkspaceMonitor(ctx context.Context, arg UpdateWorkspaceMonitorParams) error UpdateWorkspaceNextStartAt(ctx context.Context, arg UpdateWorkspaceNextStartAtParams) error // This allows editing the properties of a workspace proxy. UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 20800018a3a0e..db261b39ab166 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -14880,6 +14880,116 @@ func (q *sqlQuerier) InsertWorkspaceModule(ctx context.Context, arg InsertWorksp return i, err } +const getWorkspaceMonitor = `-- name: GetWorkspaceMonitor :one +SELECT workspace_id, monitor_type, volume_path, state, created_at, updated_at, debounced_until +FROM workspace_monitors +WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3 +` + +type GetWorkspaceMonitorParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` + VolumePath sql.NullString `db:"volume_path" json:"volume_path"` +} + +func (q *sqlQuerier) GetWorkspaceMonitor(ctx context.Context, arg GetWorkspaceMonitorParams) (WorkspaceMonitor, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceMonitor, arg.WorkspaceID, arg.MonitorType, arg.VolumePath) + var i WorkspaceMonitor + err := row.Scan( + &i.WorkspaceID, + &i.MonitorType, + &i.VolumePath, + &i.State, + &i.CreatedAt, + &i.UpdatedAt, + &i.DebouncedUntil, + ) + return i, err +} + +const insertWorkspaceMonitor = `-- name: InsertWorkspaceMonitor :one +INSERT INTO workspace_monitors ( + workspace_id, + monitor_type, + volume_path, + state, + created_at, + updated_at, + debounced_until +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) RETURNING workspace_id, monitor_type, volume_path, state, created_at, updated_at, debounced_until +` + +type InsertWorkspaceMonitorParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` + VolumePath sql.NullString `db:"volume_path" json:"volume_path"` + State WorkspaceMonitorState `db:"state" json:"state"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) InsertWorkspaceMonitor(ctx context.Context, arg InsertWorkspaceMonitorParams) (WorkspaceMonitor, error) { + row := q.db.QueryRowContext(ctx, insertWorkspaceMonitor, + arg.WorkspaceID, + arg.MonitorType, + arg.VolumePath, + arg.State, + arg.CreatedAt, + arg.UpdatedAt, + arg.DebouncedUntil, + ) + var i WorkspaceMonitor + err := row.Scan( + &i.WorkspaceID, + &i.MonitorType, + &i.VolumePath, + &i.State, + &i.CreatedAt, + &i.UpdatedAt, + &i.DebouncedUntil, + ) + return i, err +} + +const updateWorkspaceMonitor = `-- name: UpdateWorkspaceMonitor :exec +UPDATE workspace_monitors +SET + state = $4, + updated_at = $5, + debounced_until = $6 +WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3 +` + +type UpdateWorkspaceMonitorParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` + VolumePath sql.NullString `db:"volume_path" json:"volume_path"` + State WorkspaceMonitorState `db:"state" json:"state"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) UpdateWorkspaceMonitor(ctx context.Context, arg UpdateWorkspaceMonitorParams) error { + _, err := q.db.ExecContext(ctx, updateWorkspaceMonitor, + arg.WorkspaceID, + arg.MonitorType, + arg.VolumePath, + arg.State, + arg.UpdatedAt, + arg.DebouncedUntil, + ) + return err +} + const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path diff --git a/coderd/database/queries/workspacemonitors.sql b/coderd/database/queries/workspacemonitors.sql new file mode 100644 index 0000000000000..d6ec499e4aec7 --- /dev/null +++ b/coderd/database/queries/workspacemonitors.sql @@ -0,0 +1,31 @@ +-- name: GetWorkspaceMonitor :one +SELECT * +FROM workspace_monitors +WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3; + +-- name: InsertWorkspaceMonitor :one +INSERT INTO workspace_monitors ( + workspace_id, + monitor_type, + volume_path, + state, + created_at, + updated_at, + debounced_until +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) RETURNING *; + +-- name: UpdateWorkspaceMonitor :exec +UPDATE workspace_monitors +SET + state = $4, + updated_at = $5, + debounced_until = $6 +WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3; diff --git a/coderd/workspaceagentsrpc.go b/coderd/workspaceagentsrpc.go index cbb3a1bc44b8a..c794c9c14349b 100644 --- a/coderd/workspaceagentsrpc.go +++ b/coderd/workspaceagentsrpc.go @@ -143,7 +143,9 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) { Ctx: api.ctx, Log: logger, + Clock: api.Clock, Database: api.Database, + NotificationsEnqueuer: api.NotificationsEnqueuer, Pubsub: api.Pubsub, DerpMapFn: api.DERPMap, TailnetCoordinator: &api.TailnetCoordinator, From 78ede467f003eb56fd2cca410ed4ba6263e22a05 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 29 Jan 2025 16:36:35 +0000 Subject: [PATCH 05/37] chore: appease the linter for now --- agent/agenttest/client.go | 4 +-- coderd/agentapi/workspacemonitor.go | 37 +++++++++++++++--------- coderd/agentapi/workspacemonitor_test.go | 10 +++---- coderd/database/dbauthz/dbauthz.go | 16 ++++++++-- coderd/database/dbmem/dbmem.go | 6 ++-- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index da5a5988cba2f..5839be09401d0 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -315,10 +315,8 @@ func (f *FakeAgentAPI) ScriptCompleted(_ context.Context, req *agentproto.Worksp return &agentproto.WorkspaceAgentScriptCompletedResponse{}, nil } -func (f *FakeAgentAPI) UpdateWorkspaceMonitor(_ context.Context, req *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { - f.Lock() +func (*FakeAgentAPI) UpdateWorkspaceMonitor(_ context.Context, _ *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { // TODO: Figure out a good way of mocking the logic - f.Unlock() return &agentproto.WorkspaceMonitorUpdateResponse{}, nil } diff --git a/coderd/agentapi/workspacemonitor.go b/coderd/agentapi/workspacemonitor.go index 24531efc9579d..c90f5ff9a10ae 100644 --- a/coderd/agentapi/workspacemonitor.go +++ b/coderd/agentapi/workspacemonitor.go @@ -5,14 +5,15 @@ import ( "database/sql" "slices" + "github.com/google/uuid" + "golang.org/x/xerrors" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/quartz" - "github.com/google/uuid" - "golang.org/x/xerrors" ) type WorkspaceMonitorAPI struct { @@ -66,24 +67,28 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a oldState := memoryMonitor.State newState := m.nextState(oldState, memoryUsageStates) + shouldNotify := oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - State: newState, - UpdatedAt: dbtime.Time(m.Clock.Now()), + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + VolumePath: sql.NullString{Valid: false}, + State: newState, + UpdatedAt: dbtime.Time(m.Clock.Now()), + DebouncedUntil: dbtime.Time(m.Clock.Now()), }) if err != nil { return xerrors.Errorf("update workspace monitor: %w", err) } - if oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK { + if shouldNotify { workspace, err := m.Database.GetWorkspaceByID(ctx, m.WorkspaceID) if err != nil { return xerrors.Errorf("get workspace by id: %w", err) } _, err = m.NotificationsEnqueuer.Enqueue( + // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, notifications.TemplateWorkspaceReachedResourceThreshold, @@ -110,6 +115,7 @@ func (m *WorkspaceMonitorAPI) getOrInsertMemoryMonitor(ctx context.Context) (dat database.InsertWorkspaceMonitorParams{ WorkspaceID: m.WorkspaceID, MonitorType: database.WorkspaceMonitorTypeMemory, + VolumePath: sql.NullString{Valid: false}, State: database.WorkspaceMonitorStateOK, CreatedAt: dbtime.Now(), UpdatedAt: dbtime.Now(), @@ -154,25 +160,28 @@ func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, da oldState := volumeMonitor.State newState := m.nextState(oldState, volumeUsageStates) + shouldNotify := oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: path}, - State: newState, - UpdatedAt: dbtime.Time(m.Clock.Now()), + WorkspaceID: m.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: path}, + State: newState, + UpdatedAt: dbtime.Time(m.Clock.Now()), + DebouncedUntil: dbtime.Time(m.Clock.Now()), }) if err != nil { return xerrors.Errorf("update workspace monitor: %w", err) } - if oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK { + if shouldNotify { workspace, err := m.Database.GetWorkspaceByID(ctx, m.WorkspaceID) if err != nil { return xerrors.Errorf("get workspace by id: %w", err) } _, err = m.NotificationsEnqueuer.Enqueue( + // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, notifications.TemplateWorkspaceReachedResourceThreshold, @@ -226,7 +235,7 @@ func (m *WorkspaceMonitorAPI) nextState(oldState database.WorkspaceMonitorState, nokCount := 0 for _, state := range states { if state == database.WorkspaceMonitorStateNOK { - nokCount += 1 + nokCount++ } } diff --git a/coderd/agentapi/workspacemonitor_test.go b/coderd/agentapi/workspacemonitor_test.go index 686c612142d06..3f68d8e6c125b 100644 --- a/coderd/agentapi/workspacemonitor_test.go +++ b/coderd/agentapi/workspacemonitor_test.go @@ -6,6 +6,11 @@ import ( "testing" "time" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/timestamppb" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/agentapi" "github.com/coder/coder/v2/coderd/database" @@ -13,10 +18,6 @@ import ( "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/quartz" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - "google.golang.org/protobuf/types/known/timestamppb" ) func TestWorkspaceMemoryMonitor(t *testing.T) { @@ -174,7 +175,6 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { } }) } - } func TestWorkspaceVolumeMonitor(t *testing.T) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index eac95a6266d1c..8fd90e69c7408 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2741,7 +2741,11 @@ func (q *querier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt } func (q *querier) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return database.WorkspaceMonitor{}, err + } + + return q.db.GetWorkspaceMonitor(ctx, arg) } func (q *querier) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { @@ -3323,7 +3327,10 @@ func (q *querier) InsertWorkspaceModule(ctx context.Context, arg database.Insert } func (q *querier) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + return database.WorkspaceMonitor{}, err + } + return q.db.InsertWorkspaceMonitor(ctx, arg) } func (q *querier) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { @@ -4146,7 +4153,10 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up } func (q *querier) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { - panic("not implemented") + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { + return err + } + return q.db.UpdateWorkspaceMonitor(ctx, arg) } func (q *querier) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 1b3ebe162074c..fef7e70c5b14f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7143,7 +7143,7 @@ func (q *FakeQuerier) GetWorkspaceModulesCreatedAfter(_ context.Context, created return modules, nil } -func (q *FakeQuerier) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { +func (*FakeQuerier) GetWorkspaceMonitor(_ context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { err := validateDatabaseType(arg) if err != nil { return database.WorkspaceMonitor{}, err @@ -8761,7 +8761,7 @@ func (q *FakeQuerier) InsertWorkspaceModule(_ context.Context, arg database.Inse return workspaceModule, nil } -func (q *FakeQuerier) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { +func (*FakeQuerier) InsertWorkspaceMonitor(_ context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { err := validateDatabaseType(arg) if err != nil { return database.WorkspaceMonitor{}, err @@ -10528,7 +10528,7 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } -func (q *FakeQuerier) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { +func (*FakeQuerier) UpdateWorkspaceMonitor(_ context.Context, arg database.UpdateWorkspaceMonitorParams) error { err := validateDatabaseType(arg) if err != nil { return err From 0d2b970452c39e1a219cbf4ef898ed8b059d887a Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 30 Jan 2025 10:08:53 +0000 Subject: [PATCH 06/37] chore: use latest changes to #247, start debounce logic --- coderd/agentapi/workspacemonitor.go | 33 +++++++++++++++---- coderd/agentapi/workspacemonitor_test.go | 24 +++++++------- .../000289_create_workspace_monitors.down.sql | 1 + 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/coderd/agentapi/workspacemonitor.go b/coderd/agentapi/workspacemonitor.go index c90f5ff9a10ae..cde9db3f10200 100644 --- a/coderd/agentapi/workspacemonitor.go +++ b/coderd/agentapi/workspacemonitor.go @@ -3,7 +3,9 @@ package agentapi import ( "context" "database/sql" + "fmt" "slices" + "time" "github.com/google/uuid" "golang.org/x/xerrors" @@ -27,6 +29,8 @@ type WorkspaceMonitorAPI struct { MemoryUsageThreshold int32 VolumeUsageThresholds map[string]int32 + Debounce time.Duration + // How many datapoints in a row are required to // put the monitor in an alert state. ConsecutiveNOKs int @@ -69,13 +73,18 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a newState := m.nextState(oldState, memoryUsageStates) shouldNotify := oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK + var debouncedUntil = m.Clock.Now() + if shouldNotify { + debouncedUntil = debouncedUntil.Add(m.Debounce) + } + err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ WorkspaceID: m.WorkspaceID, MonitorType: database.WorkspaceMonitorTypeMemory, VolumePath: sql.NullString{Valid: false}, State: newState, UpdatedAt: dbtime.Time(m.Clock.Now()), - DebouncedUntil: dbtime.Time(m.Clock.Now()), + DebouncedUntil: dbtime.Time(debouncedUntil), }) if err != nil { return xerrors.Errorf("update workspace monitor: %w", err) @@ -91,8 +100,11 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, - notifications.TemplateWorkspaceReachedResourceThreshold, - map[string]string{}, + notifications.TemplateWorkspaceOutOfMemory, + map[string]string{ + "workspace": workspace.Name, + "threshold": fmt.Sprintf("%d%%", m.MemoryUsageThreshold), + }, "workspace-monitor-memory", ) if err != nil { @@ -162,13 +174,18 @@ func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, da newState := m.nextState(oldState, volumeUsageStates) shouldNotify := oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK + var debouncedUntil = m.Clock.Now() + if shouldNotify { + debouncedUntil = debouncedUntil.Add(m.Debounce) + } + err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ WorkspaceID: m.WorkspaceID, MonitorType: database.WorkspaceMonitorTypeVolume, VolumePath: sql.NullString{Valid: true, String: path}, State: newState, UpdatedAt: dbtime.Time(m.Clock.Now()), - DebouncedUntil: dbtime.Time(m.Clock.Now()), + DebouncedUntil: dbtime.Time(debouncedUntil), }) if err != nil { return xerrors.Errorf("update workspace monitor: %w", err) @@ -184,8 +201,12 @@ func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, da // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, - notifications.TemplateWorkspaceReachedResourceThreshold, - map[string]string{}, + notifications.TemplateWorkspaceOutOfDisk, + map[string]string{ + "workspace": workspace.Name, + "threshold": fmt.Sprintf("%d%%", m.VolumeUsageThresholds[path]), + "volume": path, + }, "workspace-monitor-memory", ) if err != nil { diff --git a/coderd/agentapi/workspacemonitor_test.go b/coderd/agentapi/workspacemonitor_test.go index 3f68d8e6c125b..f041c47f585b0 100644 --- a/coderd/agentapi/workspacemonitor_test.go +++ b/coderd/agentapi/workspacemonitor_test.go @@ -147,10 +147,11 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }, nil) mDB.EXPECT().UpdateWorkspaceMonitor(gomock.Any(), database.UpdateWorkspaceMonitorParams{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - State: tt.expectState, - UpdatedAt: timestamppb.New(collectedAt).AsTime(), + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + State: tt.expectState, + UpdatedAt: collectedAt, + DebouncedUntil: collectedAt, }) if tt.shouldNotify { @@ -166,7 +167,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }) require.NoError(t, err) - sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceReachedResourceThreshold)) + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) if tt.shouldNotify { require.Len(t, sent, 1) require.Equal(t, ownerID, sent[0].UserID) @@ -320,11 +321,12 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { }, nil) mDB.EXPECT().UpdateWorkspaceMonitor(gomock.Any(), database.UpdateWorkspaceMonitorParams{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, - State: tt.expectState, - UpdatedAt: timestamppb.New(collectedAt).AsTime(), + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, + State: tt.expectState, + UpdatedAt: collectedAt, + DebouncedUntil: collectedAt, }) if tt.shouldNotify { @@ -340,7 +342,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { }) require.NoError(t, err) - sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceReachedResourceThreshold)) + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) if tt.shouldNotify { require.Len(t, sent, 1) require.Equal(t, ownerID, sent[0].UserID) diff --git a/coderd/database/migrations/000289_create_workspace_monitors.down.sql b/coderd/database/migrations/000289_create_workspace_monitors.down.sql index bcc0a2bea4375..5aab6243dd407 100644 --- a/coderd/database/migrations/000289_create_workspace_monitors.down.sql +++ b/coderd/database/migrations/000289_create_workspace_monitors.down.sql @@ -1,2 +1,3 @@ DROP TABLE workspace_monitors; DROP TYPE workspace_monitor_state; +DROP TYPE workspace_monitor_type; From 0df2fd527847b34a017957b929227d95222753ab Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 30 Jan 2025 17:32:57 +0000 Subject: [PATCH 07/37] chore: add more tests --- coderd/database/dbauthz/dbauthz_test.go | 68 +++++++++++++++++++ coderd/database/dbgen/dbgen.go | 17 +++++ coderd/database/dbmem/dbmem.go | 46 +++++++++++-- coderd/database/dump.sql | 2 +- .../000289_create_workspace_monitors.up.sql | 7 +- .../000289_create_workspace_monitors.up.sql | 15 ++++ coderd/database/queries.sql.go | 5 +- coderd/database/queries/workspacemonitors.sql | 5 +- 8 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index fdbbcc8b34ca6..cf52237b08cb7 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4544,3 +4544,71 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() { }).Asserts(rbac.ResourceOauth2AppCodeToken.WithOwner(user.ID.String()), policy.ActionDelete) })) } + +func (s *MethodTestSuite) TestWorkspaceMonitor() { + s.Run("GetWorkspaceMonitor", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + org := dbgen.Organization(s.T(), db, database.Organization{}) + template := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + OwnerID: user.ID, + OrganizationID: org.ID, + TemplateID: template.ID, + }) + monitor := dbgen.WorkspaceMonitor(s.T(), db, database.WorkspaceMonitor{ + WorkspaceID: workspace.ID, + MonitorType: database.WorkspaceMonitorTypeMemory, + VolumePath: sql.NullString{}, + }) + + check.Args(database.GetWorkspaceMonitorParams{ + WorkspaceID: monitor.WorkspaceID, + MonitorType: monitor.MonitorType, + VolumePath: monitor.VolumePath, + }).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("InsertWorkspaceMonitor", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + org := dbgen.Organization(s.T(), db, database.Organization{}) + template := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + OwnerID: user.ID, + OrganizationID: org.ID, + TemplateID: template.ID, + }) + + check.Args(database.InsertWorkspaceMonitorParams{ + WorkspaceID: workspace.ID, + MonitorType: database.WorkspaceMonitorTypeMemory, + State: database.WorkspaceMonitorStateOK, + }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + })) + s.Run("UpdateWorkspaceMonitor", s.Subtest(func(db database.Store, check *expects) { + user := dbgen.User(s.T(), db, database.User{}) + org := dbgen.Organization(s.T(), db, database.Organization{}) + template := dbgen.Template(s.T(), db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + OwnerID: user.ID, + OrganizationID: org.ID, + TemplateID: template.ID, + }) + monitor := dbgen.WorkspaceMonitor(s.T(), db, database.WorkspaceMonitor{ + WorkspaceID: workspace.ID, + }) + + check.Args(database.UpdateWorkspaceMonitorParams{ + WorkspaceID: monitor.WorkspaceID, + MonitorType: monitor.MonitorType, + State: database.WorkspaceMonitorStateNOK, + }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) + })) +} diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 566540dcb2906..5fc817b76a79f 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -370,6 +370,23 @@ func WorkspaceBuildParameters(t testing.TB, db database.Store, orig []database.W return params } +func WorkspaceMonitor(t testing.TB, db database.Store, orig database.WorkspaceMonitor) database.WorkspaceMonitor { + t.Helper() + + monitor, err := db.InsertWorkspaceMonitor(genCtx, database.InsertWorkspaceMonitorParams{ + WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), + MonitorType: takeFirst(orig.MonitorType, database.WorkspaceMonitorTypeMemory), + VolumePath: takeFirst(orig.VolumePath, sql.NullString{}), + State: takeFirst(orig.State, database.WorkspaceMonitorStateOK), + CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), + UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), + DebouncedUntil: takeFirst(orig.DebouncedUntil, dbtime.Now()), + }) + require.NoError(t, err, "insert monitor") + + return monitor +} + func User(t testing.TB, db database.Store, orig database.User) database.User { user, err := db.InsertUser(genCtx, database.InsertUserParams{ ID: takeFirst(orig.ID, uuid.New()), diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index fef7e70c5b14f..07aa055e5db0a 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -235,6 +235,7 @@ type data struct { workspaceResourceMetadata []database.WorkspaceResourceMetadatum workspaceResources []database.WorkspaceResource workspaceModules []database.WorkspaceModule + workspaceMonitors []database.WorkspaceMonitor workspaces []database.WorkspaceTable workspaceProxies []database.WorkspaceProxy customRoles []database.CustomRole @@ -7143,13 +7144,24 @@ func (q *FakeQuerier) GetWorkspaceModulesCreatedAfter(_ context.Context, created return modules, nil } -func (*FakeQuerier) GetWorkspaceMonitor(_ context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { +func (q *FakeQuerier) GetWorkspaceMonitor(_ context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { err := validateDatabaseType(arg) if err != nil { return database.WorkspaceMonitor{}, err } - panic("not implemented") + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, monitor := range q.workspaceMonitors { + if monitor.WorkspaceID == arg.WorkspaceID && + monitor.MonitorType == arg.MonitorType && + monitor.VolumePath == arg.VolumePath { + return monitor, nil + } + } + + return database.WorkspaceMonitor{}, sql.ErrNoRows } func (q *FakeQuerier) GetWorkspaceProxies(_ context.Context) ([]database.WorkspaceProxy, error) { @@ -8761,13 +8773,18 @@ func (q *FakeQuerier) InsertWorkspaceModule(_ context.Context, arg database.Inse return workspaceModule, nil } -func (*FakeQuerier) InsertWorkspaceMonitor(_ context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { +func (q *FakeQuerier) InsertWorkspaceMonitor(_ context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { err := validateDatabaseType(arg) if err != nil { return database.WorkspaceMonitor{}, err } - panic("not implemented") + q.mutex.Lock() + defer q.mutex.Unlock() + + workspaceMonitor := database.WorkspaceMonitor(arg) + q.workspaceMonitors = append(q.workspaceMonitors, workspaceMonitor) + return workspaceMonitor, nil } func (q *FakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { @@ -10528,13 +10545,30 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } -func (*FakeQuerier) UpdateWorkspaceMonitor(_ context.Context, arg database.UpdateWorkspaceMonitorParams) error { +func (q *FakeQuerier) UpdateWorkspaceMonitor(_ context.Context, arg database.UpdateWorkspaceMonitorParams) error { err := validateDatabaseType(arg) if err != nil { return err } - panic("not implemented") + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, monitor := range q.workspaceMonitors { + if monitor.WorkspaceID != arg.WorkspaceID || + monitor.MonitorType != arg.MonitorType || + monitor.VolumePath != arg.VolumePath { + continue + } + + monitor.DebouncedUntil = arg.DebouncedUntil + monitor.UpdatedAt = arg.UpdatedAt + monitor.State = arg.State + q.workspaceMonitors[index] = monitor + return nil + } + + return sql.ErrNoRows } func (q *FakeQuerier) UpdateWorkspaceNextStartAt(_ context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f2fdfcc3ea21b..678363d1e11f8 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1760,7 +1760,7 @@ CREATE TABLE workspace_monitors ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, debounced_until timestamp with time zone NOT NULL, - CONSTRAINT workspace_monitors_monitor_type_check CHECK ((monitor_type = 'volume'::workspace_monitor_type)) + CONSTRAINT workspace_monitor_volume_path_exclusion CHECK (((volume_path = NULL::text) OR (monitor_type = 'volume'::workspace_monitor_type))) ); CREATE TABLE workspace_proxies ( diff --git a/coderd/database/migrations/000289_create_workspace_monitors.up.sql b/coderd/database/migrations/000289_create_workspace_monitors.up.sql index 236b515bf08ef..a60cd9232a41a 100644 --- a/coderd/database/migrations/000289_create_workspace_monitors.up.sql +++ b/coderd/database/migrations/000289_create_workspace_monitors.up.sql @@ -11,9 +11,14 @@ CREATE TYPE workspace_monitor_type AS ENUM ( CREATE TABLE workspace_monitors ( workspace_id uuid NOT NULL, monitor_type workspace_monitor_type NOT NULL, - volume_path text CHECK (monitor_type = 'volume'), + volume_path text, state workspace_monitor_state NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, debounced_until timestamp with time zone NOT NULL ); + +ALTER TABLE workspace_monitors +ADD CONSTRAINT workspace_monitor_volume_path_exclusion CHECK ( + volume_path = NULL OR monitor_type = 'volume' +); diff --git a/coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql b/coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql new file mode 100644 index 0000000000000..05ff05e2b0343 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql @@ -0,0 +1,15 @@ +INSERT INTO workspace_monitors ( + workspace_id, + monitor_type, + state, + created_at, + updated_at, + debounced_until +) VALUES ( + (SELECT id FROM workspaces WHERE deleted = FALSE LIMIT 1), + 'memory', + 'OK', + NOW(), + NOW(), + NOW() +); diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index db261b39ab166..faab301c6a99f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -14883,7 +14883,10 @@ func (q *sqlQuerier) InsertWorkspaceModule(ctx context.Context, arg InsertWorksp const getWorkspaceMonitor = `-- name: GetWorkspaceMonitor :one SELECT workspace_id, monitor_type, volume_path, state, created_at, updated_at, debounced_until FROM workspace_monitors -WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3 +WHERE + workspace_id = $1 AND + monitor_type = $2 AND + volume_path IS NOT DISTINCT FROM $3 ` type GetWorkspaceMonitorParams struct { diff --git a/coderd/database/queries/workspacemonitors.sql b/coderd/database/queries/workspacemonitors.sql index d6ec499e4aec7..05b994bf9eb59 100644 --- a/coderd/database/queries/workspacemonitors.sql +++ b/coderd/database/queries/workspacemonitors.sql @@ -1,7 +1,10 @@ -- name: GetWorkspaceMonitor :one SELECT * FROM workspace_monitors -WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3; +WHERE + workspace_id = $1 AND + monitor_type = $2 AND + volume_path IS NOT DISTINCT FROM $3; -- name: InsertWorkspaceMonitor :one INSERT INTO workspace_monitors ( From 854d81ad54248365835b32c4d02d0e532d0b2230 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 30 Jan 2025 18:22:36 +0000 Subject: [PATCH 08/37] chore: remove mock db for workspace monitor agentapi test --- coderd/agentapi/workspacemonitor_test.go | 118 +++++++----------- coderd/database/dump.sql | 2 +- .../000289_create_workspace_monitors.up.sql | 2 +- 3 files changed, 45 insertions(+), 77 deletions(-) diff --git a/coderd/agentapi/workspacemonitor_test.go b/coderd/agentapi/workspacemonitor_test.go index f041c47f585b0..b4ea597c8a6ea 100644 --- a/coderd/agentapi/workspacemonitor_test.go +++ b/coderd/agentapi/workspacemonitor_test.go @@ -6,20 +6,46 @@ import ( "testing" "time" - "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" "google.golang.org/protobuf/types/known/timestamppb" agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/agentapi" "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbmock" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/notifications/notificationstest" "github.com/coder/quartz" ) +func workspaceMonitorAPI(t *testing.T) (*agentapi.WorkspaceMonitorAPI, database.User, *quartz.Mock, *notificationstest.FakeEnqueuer) { + t.Helper() + + db, _ := dbtestutil.NewDB(t) + user := dbgen.User(t, db, database.User{}) + org := dbgen.Organization(t, db, database.Organization{}) + template := dbgen.Template(t, db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + OrganizationID: org.ID, + TemplateID: template.ID, + OwnerID: user.ID, + }) + + notifyEnq := ¬ificationstest.FakeEnqueuer{} + clock := quartz.NewMock(t) + + return &agentapi.WorkspaceMonitorAPI{ + WorkspaceID: workspace.ID, + Clock: clock, + Database: db, + NotificationsEnqueuer: notifyEnq, + }, user, clock, notifyEnq +} + func TestWorkspaceMemoryMonitor(t *testing.T) { t.Parallel() @@ -108,19 +134,11 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - notifyEnq := notificationstest.FakeEnqueuer{} - mDB := dbmock.NewMockStore(gomock.NewController(t)) - clock := quartz.NewMock(t) - api := &agentapi.WorkspaceMonitorAPI{ - WorkspaceID: uuid.New(), - Clock: clock, - Database: mDB, - NotificationsEnqueuer: ¬ifyEnq, - MinimumNOKs: tt.minimumNOKs, - ConsecutiveNOKs: tt.consecutiveNOKs, - MemoryMonitorEnabled: true, - MemoryUsageThreshold: tt.thresholdPercent, - } + api, user, clock, notifyEnq := workspaceMonitorAPI(t) + api.MinimumNOKs = tt.minimumNOKs + api.ConsecutiveNOKs = tt.consecutiveNOKs + api.MemoryMonitorEnabled = true + api.MemoryUsageThreshold = tt.thresholdPercent datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.memoryUsage)) collectedAt := clock.Now() @@ -135,32 +153,12 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }) } - ownerID := uuid.New() - - mDB.EXPECT().GetWorkspaceMonitor(gomock.Any(), database.GetWorkspaceMonitorParams{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - }).Return(database.WorkspaceMonitor{ + dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ WorkspaceID: api.WorkspaceID, MonitorType: database.WorkspaceMonitorTypeMemory, State: tt.previousState, - }, nil) - - mDB.EXPECT().UpdateWorkspaceMonitor(gomock.Any(), database.UpdateWorkspaceMonitorParams{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - State: tt.expectState, - UpdatedAt: collectedAt, - DebouncedUntil: collectedAt, }) - if tt.shouldNotify { - mDB.EXPECT().GetWorkspaceByID(gomock.Any(), api.WorkspaceID).Return(database.Workspace{ - ID: api.WorkspaceID, - OwnerID: ownerID, - }, nil) - } - clock.Set(collectedAt) _, err := api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ Datapoints: datapoints, @@ -170,7 +168,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) if tt.shouldNotify { require.Len(t, sent, 1) - require.Equal(t, ownerID, sent[0].UserID) + require.Equal(t, user.ID, sent[0].UserID) } else { require.Len(t, sent, 0) } @@ -273,19 +271,11 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - notifyEnq := notificationstest.FakeEnqueuer{} - mDB := dbmock.NewMockStore(gomock.NewController(t)) - clock := quartz.NewMock(t) - api := &agentapi.WorkspaceMonitorAPI{ - WorkspaceID: uuid.New(), - Clock: clock, - Database: mDB, - NotificationsEnqueuer: ¬ifyEnq, - MinimumNOKs: tt.minimumNOKs, - ConsecutiveNOKs: tt.consecutiveNOKs, - VolumeUsageThresholds: map[string]int32{ - tt.volumePath: tt.thresholdPercent, - }, + api, user, clock, notifyEnq := workspaceMonitorAPI(t) + api.MinimumNOKs = tt.minimumNOKs + api.ConsecutiveNOKs = tt.consecutiveNOKs + api.VolumeUsageThresholds = map[string]int32{ + tt.volumePath: tt.thresholdPercent, } datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.volumeUsage)) @@ -307,35 +297,13 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { }) } - ownerID := uuid.New() - - mDB.EXPECT().GetWorkspaceMonitor(gomock.Any(), database.GetWorkspaceMonitorParams{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, - }).Return(database.WorkspaceMonitor{ + dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ WorkspaceID: api.WorkspaceID, MonitorType: database.WorkspaceMonitorTypeVolume, VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, State: tt.previousState, - }, nil) - - mDB.EXPECT().UpdateWorkspaceMonitor(gomock.Any(), database.UpdateWorkspaceMonitorParams{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, - State: tt.expectState, - UpdatedAt: collectedAt, - DebouncedUntil: collectedAt, }) - if tt.shouldNotify { - mDB.EXPECT().GetWorkspaceByID(gomock.Any(), api.WorkspaceID).Return(database.Workspace{ - ID: api.WorkspaceID, - OwnerID: ownerID, - }, nil) - } - clock.Set(collectedAt) _, err := api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ Datapoints: datapoints, @@ -345,7 +313,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) if tt.shouldNotify { require.Len(t, sent, 1) - require.Equal(t, ownerID, sent[0].UserID) + require.Equal(t, user.ID, sent[0].UserID) } else { require.Len(t, sent, 0) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 678363d1e11f8..b31f61b3b44c3 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1760,7 +1760,7 @@ CREATE TABLE workspace_monitors ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, debounced_until timestamp with time zone NOT NULL, - CONSTRAINT workspace_monitor_volume_path_exclusion CHECK (((volume_path = NULL::text) OR (monitor_type = 'volume'::workspace_monitor_type))) + CONSTRAINT workspace_monitor_volume_path_exclusion CHECK (((volume_path IS NULL) OR (monitor_type = 'volume'::workspace_monitor_type))) ); CREATE TABLE workspace_proxies ( diff --git a/coderd/database/migrations/000289_create_workspace_monitors.up.sql b/coderd/database/migrations/000289_create_workspace_monitors.up.sql index a60cd9232a41a..a67d3bc2b50e0 100644 --- a/coderd/database/migrations/000289_create_workspace_monitors.up.sql +++ b/coderd/database/migrations/000289_create_workspace_monitors.up.sql @@ -20,5 +20,5 @@ CREATE TABLE workspace_monitors ( ALTER TABLE workspace_monitors ADD CONSTRAINT workspace_monitor_volume_path_exclusion CHECK ( - volume_path = NULL OR monitor_type = 'volume' + volume_path IS NULL OR monitor_type = 'volume' ); From 9d9d7b474ea286d973d8b0b8a553bca8c7d73264 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 30 Jan 2025 18:26:43 +0000 Subject: [PATCH 09/37] chore: remove todo comment --- agent/agenttest/client.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 5839be09401d0..39b52184f978f 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -316,8 +316,6 @@ func (f *FakeAgentAPI) ScriptCompleted(_ context.Context, req *agentproto.Worksp } func (*FakeAgentAPI) UpdateWorkspaceMonitor(_ context.Context, _ *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { - // TODO: Figure out a good way of mocking the logic - return &agentproto.WorkspaceMonitorUpdateResponse{}, nil } From 944fdb51e5864e147451e41006e08118876a46e1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 4 Feb 2025 12:28:27 +0000 Subject: [PATCH 10/37] chore: rewrite ood notification --- ... 000289_oom_and_ood_notification.down.sql} | 0 ...=> 000289_oom_and_ood_notification.up.sql} | 11 ++++- coderd/notifications/notifications_test.go | 43 +++++++++++++++++-- .../TemplateWorkspaceOutOfDisk.html.golden | 14 +++--- .../TemplateWorkspaceOutOfDisk.json.golden | 19 +++++--- 5 files changed, 66 insertions(+), 21 deletions(-) rename coderd/database/migrations/{000288_oom_and_ood_notification.down.sql => 000289_oom_and_ood_notification.down.sql} (100%) rename coderd/database/migrations/{000288_oom_and_ood_notification.up.sql => 000289_oom_and_ood_notification.up.sql} (62%) diff --git a/coderd/database/migrations/000288_oom_and_ood_notification.down.sql b/coderd/database/migrations/000289_oom_and_ood_notification.down.sql similarity index 100% rename from coderd/database/migrations/000288_oom_and_ood_notification.down.sql rename to coderd/database/migrations/000289_oom_and_ood_notification.down.sql diff --git a/coderd/database/migrations/000288_oom_and_ood_notification.up.sql b/coderd/database/migrations/000289_oom_and_ood_notification.up.sql similarity index 62% rename from coderd/database/migrations/000288_oom_and_ood_notification.up.sql rename to coderd/database/migrations/000289_oom_and_ood_notification.up.sql index a8b7ce6b29987..f0489606bb5b9 100644 --- a/coderd/database/migrations/000288_oom_and_ood_notification.up.sql +++ b/coderd/database/migrations/000289_oom_and_ood_notification.up.sql @@ -20,9 +20,16 @@ INSERT INTO notification_templates VALUES ( 'f047f6a3-5713-40f7-85aa-0394cce9fa3a', 'Workspace Out Of Disk', - E'Your workspace "{{.Labels.workspace}}" is low on disk', + E'Your workspace "{{.Labels.workspace}}" is low on volume space', E'Hi {{.UserName}},\n\n'|| - E'Your workspace **{{.Labels.workspace}}** has reached the usage threshold set at **{{.Labels.threshold}}** for volume `{{.Labels.volume}}`.', + E'{{ if eq (len .Data.volumes) 1 }}{{ $volume := index .Data.volumes 0 }}'|| + E'Volume **`{{$volume.path}}`** is over {{$volume.threshold}} full in workspace **{{.Labels.workspace}}**.'|| + E'{{ else }}'|| + E'The following volumes are nearly full in workspace **{{.Labels.workspace}}**\n\n'|| + E'{{ range $volume := .Data.volumes }}'|| + E'- **`{{$volume.path}}`** is over {{$volume.threshold}} full\n'|| + E'{{ end }}'|| + E'{{ end }}', 'Workspace Events', '[ { diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 06118b54be492..5375543f28508 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1078,7 +1078,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, { - name: "TemplateWorkspaceOutOfDisk", + name: "TemplateWorkspaceOutOfDisk/SingleVolume", id: notifications.TemplateWorkspaceOutOfDisk, payload: types.MessagePayload{ UserName: "Bobby", @@ -1086,8 +1086,42 @@ func TestNotificationTemplates_Golden(t *testing.T) { UserUsername: "bobby", Labels: map[string]string{ "workspace": "bobby-workspace", - "threshold": "90%", - "volume": "/home/coder", + }, + Data: map[string]any{ + "volumes": []map[string]any{ + { + "path": "/home/coder", + "threshold": "90%", + }, + }, + }, + }, + }, + { + name: "TemplateWorkspaceOutOfDisk/MultipleVolumes", + id: notifications.TemplateWorkspaceOutOfDisk, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "workspace": "bobby-workspace", + }, + Data: map[string]any{ + "volumes": []map[string]any{ + { + "path": "/home/coder", + "threshold": "90%", + }, + { + "path": "/dev/coder", + "threshold": "80%", + }, + { + "path": "/etc/coder", + "threshold": "95%", + }, + }, }, }, }, @@ -1099,7 +1133,8 @@ func TestNotificationTemplates_Golden(t *testing.T) { for _, name := range allTemplates { var found bool for _, tc := range tests { - if tc.name == name { + tcName, _, _ := strings.Cut(tc.name, "/") + if tcName == name { found = true } } diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden index 542c1e4385b15..f217fc0f85c97 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk.html.golden @@ -1,6 +1,6 @@ From: system@coder.com To: bobby@coder.com -Subject: Your workspace "bobby-workspace" is low on disk +Subject: Your workspace "bobby-workspace" is low on volume space Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 Date: Fri, 11 Oct 2024 09:03:06 +0000 Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 @@ -12,8 +12,7 @@ Content-Type: text/plain; charset=UTF-8 Hi Bobby, -Your workspace bobby-workspace has reached the usage threshold set at 90% f= -or volume /home/coder. +Volume /home/coder is over 90% full in workspace bobby-workspace. View workspace: http://test.com/@bobby/bobby-workspace @@ -28,7 +27,7 @@ Content-Type: text/html; charset=UTF-8 - Codestin Search App + Codestin Search App

- Your workspace "bobby-workspace" is low on disk + Your workspace "bobby-workspace" is low on volume space

Hi Bobby,

-

Your workspace bobby-workspace has reached the usage th= -reshold set at 90% for volume /home/coder. +

Volume /home/coder is over 90% full in wor= +kspace bobby-workspace.

=20 diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden index 40dfbd0b75456..1bc671f52b6f9 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden @@ -16,14 +16,19 @@ } ], "labels": { - "threshold": "90%", - "volume": "/home/coder", "workspace": "bobby-workspace" }, - "data": null + "data": { + "volumes": [ + { + "path": "/home/coder", + "threshold": "90%" + } + ] + } }, - "title": "Your workspace \"bobby-workspace\" is low on disk", - "title_markdown": "Your workspace \"bobby-workspace\" is low on disk", - "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the usage threshold set at 90% for volume /home/coder.", - "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the usage threshold set at **90%** for volume `/home/coder`." + "title": "Your workspace \"bobby-workspace\" is low on volume space", + "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", + "body": "Hi Bobby,\n\nVolume /home/coder is over 90% full in workspace bobby-workspace.", + "body_markdown": "Hi Bobby,\n\nVolume **`/home/coder`** is over 90% full in workspace **bobby-workspace**." } \ No newline at end of file From 64441767896f8f2110a1587a33eee8909beb63a7 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 4 Feb 2025 13:22:50 +0000 Subject: [PATCH 11/37] chore: updaten golden file --- .../webhook/TemplateWorkspaceOutOfDisk.json.golden | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden index 1bc671f52b6f9..c876fb1754dd1 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden @@ -23,12 +23,20 @@ { "path": "/home/coder", "threshold": "90%" + }, + { + "path": "/dev/coder", + "threshold": "80%" + }, + { + "path": "/etc/coder", + "threshold": "95%" } ] } }, "title": "Your workspace \"bobby-workspace\" is low on volume space", "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", - "body": "Hi Bobby,\n\nVolume /home/coder is over 90% full in workspace bobby-workspace.", - "body_markdown": "Hi Bobby,\n\nVolume **`/home/coder`** is over 90% full in workspace **bobby-workspace**." + "body": "Hi Bobby,\n\nThe following volumes are nearly full in workspace bobby-workspace\n\n/home/coder is over 90% full\n/dev/coder is over 80% full\n/etc/coder is over 95% full", + "body_markdown": "Hi Bobby,\n\nThe following volumes are nearly full in workspace **bobby-workspace**\n\n- **`/home/coder`** is over 90% full\n- **`/dev/coder`** is over 80% full\n- **`/etc/coder`** is over 95% full\n" } \ No newline at end of file From bc87268e5eb8f2849a57b435b8f5ec576201a349 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 4 Feb 2025 13:27:50 +0000 Subject: [PATCH 12/37] chore: silly me --- coderd/notifications/notifications_test.go | 7 +- .../TemplateWorkspaceOutOfDisk#01.html.golden | 91 +++++++++++++++++++ .../TemplateWorkspaceOutOfDisk#01.json.golden | 42 +++++++++ .../TemplateWorkspaceOutOfDisk.json.golden | 12 +-- 4 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 5375543f28508..945beab1a507a 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1078,7 +1078,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, { - name: "TemplateWorkspaceOutOfDisk/SingleVolume", + name: "TemplateWorkspaceOutOfDisk", id: notifications.TemplateWorkspaceOutOfDisk, payload: types.MessagePayload{ UserName: "Bobby", @@ -1098,7 +1098,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, { - name: "TemplateWorkspaceOutOfDisk/MultipleVolumes", + name: "TemplateWorkspaceOutOfDisk", id: notifications.TemplateWorkspaceOutOfDisk, payload: types.MessagePayload{ UserName: "Bobby", @@ -1133,8 +1133,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { for _, name := range allTemplates { var found bool for _, tc := range tests { - tcName, _, _ := strings.Cut(tc.name, "/") - if tcName == name { + if tc.name == name { found = true } } diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden new file mode 100644 index 0000000000000..87e5dec07cdaf --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden @@ -0,0 +1,91 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Your workspace "bobby-workspace" is low on volume space +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +The following volumes are nearly full in workspace bobby-workspace + +/home/coder is over 90% full +/dev/coder is over 80% full +/etc/coder is over 95% full + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Codestin Search App + + +
+
+ 3D"Cod= +
+

+ Your workspace "bobby-workspace" is low on volume space +

+
+

Hi Bobby,

+ +

The following volumes are nearly full in workspace bobby-workspa= +ce

+ +
    +
  • /home/coder is over 90% full
    +
  • +
  • /dev/coder is over 80% full
    +
  • +
  • /etc/coder is over 95% full
    +
  • +
+
+
+ =20 + + View workspace + + =20 +
+ +
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden new file mode 100644 index 0000000000000..c876fb1754dd1 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden @@ -0,0 +1,42 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Out Of Disk", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "workspace": "bobby-workspace" + }, + "data": { + "volumes": [ + { + "path": "/home/coder", + "threshold": "90%" + }, + { + "path": "/dev/coder", + "threshold": "80%" + }, + { + "path": "/etc/coder", + "threshold": "95%" + } + ] + } + }, + "title": "Your workspace \"bobby-workspace\" is low on volume space", + "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", + "body": "Hi Bobby,\n\nThe following volumes are nearly full in workspace bobby-workspace\n\n/home/coder is over 90% full\n/dev/coder is over 80% full\n/etc/coder is over 95% full", + "body_markdown": "Hi Bobby,\n\nThe following volumes are nearly full in workspace **bobby-workspace**\n\n- **`/home/coder`** is over 90% full\n- **`/dev/coder`** is over 80% full\n- **`/etc/coder`** is over 95% full\n" +} \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden index c876fb1754dd1..1bc671f52b6f9 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk.json.golden @@ -23,20 +23,12 @@ { "path": "/home/coder", "threshold": "90%" - }, - { - "path": "/dev/coder", - "threshold": "80%" - }, - { - "path": "/etc/coder", - "threshold": "95%" } ] } }, "title": "Your workspace \"bobby-workspace\" is low on volume space", "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", - "body": "Hi Bobby,\n\nThe following volumes are nearly full in workspace bobby-workspace\n\n/home/coder is over 90% full\n/dev/coder is over 80% full\n/etc/coder is over 95% full", - "body_markdown": "Hi Bobby,\n\nThe following volumes are nearly full in workspace **bobby-workspace**\n\n- **`/home/coder`** is over 90% full\n- **`/dev/coder`** is over 80% full\n- **`/etc/coder`** is over 95% full\n" + "body": "Hi Bobby,\n\nVolume /home/coder is over 90% full in workspace bobby-workspace.", + "body_markdown": "Hi Bobby,\n\nVolume **`/home/coder`** is over 90% full in workspace **bobby-workspace**." } \ No newline at end of file From d2265f6625f5b38a20a973b9ff32ccd8ea46ddfe Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 4 Feb 2025 14:40:15 +0000 Subject: [PATCH 13/37] chore: rename test --- coderd/notifications/notifications_test.go | 2 +- ...spaceOutOfDisk_MultipleVolumes.html.golden | 91 +++++++++++++++++++ ...spaceOutOfDisk_MultipleVolumes.json.golden | 42 +++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk_MultipleVolumes.html.golden create mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk_MultipleVolumes.json.golden diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 945beab1a507a..895fafff8841b 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1098,7 +1098,7 @@ func TestNotificationTemplates_Golden(t *testing.T) { }, }, { - name: "TemplateWorkspaceOutOfDisk", + name: "TemplateWorkspaceOutOfDisk_MultipleVolumes", id: notifications.TemplateWorkspaceOutOfDisk, payload: types.MessagePayload{ UserName: "Bobby", diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk_MultipleVolumes.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk_MultipleVolumes.html.golden new file mode 100644 index 0000000000000..87e5dec07cdaf --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk_MultipleVolumes.html.golden @@ -0,0 +1,91 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Your workspace "bobby-workspace" is low on volume space +Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 +Date: Fri, 11 Oct 2024 09:03:06 +0000 +Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +The following volumes are nearly full in workspace bobby-workspace + +/home/coder is over 90% full +/dev/coder is over 80% full +/etc/coder is over 95% full + + +View workspace: http://test.com/@bobby/bobby-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Codestin Search App + + +
+
+ 3D"Cod= +
+

+ Your workspace "bobby-workspace" is low on volume space +

+
+

Hi Bobby,

+ +

The following volumes are nearly full in workspace bobby-workspa= +ce

+ +
    +
  • /home/coder is over 90% full
    +
  • +
  • /dev/coder is over 80% full
    +
  • +
  • /etc/coder is over 95% full
    +
  • +
+
+
+ =20 + + View workspace + + =20 +
+ +
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk_MultipleVolumes.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk_MultipleVolumes.json.golden new file mode 100644 index 0000000000000..c876fb1754dd1 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk_MultipleVolumes.json.golden @@ -0,0 +1,42 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.1", + "notification_name": "Workspace Out Of Disk", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View workspace", + "url": "http://test.com/@bobby/bobby-workspace" + } + ], + "labels": { + "workspace": "bobby-workspace" + }, + "data": { + "volumes": [ + { + "path": "/home/coder", + "threshold": "90%" + }, + { + "path": "/dev/coder", + "threshold": "80%" + }, + { + "path": "/etc/coder", + "threshold": "95%" + } + ] + } + }, + "title": "Your workspace \"bobby-workspace\" is low on volume space", + "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", + "body": "Hi Bobby,\n\nThe following volumes are nearly full in workspace bobby-workspace\n\n/home/coder is over 90% full\n/dev/coder is over 80% full\n/etc/coder is over 95% full", + "body_markdown": "Hi Bobby,\n\nThe following volumes are nearly full in workspace **bobby-workspace**\n\n- **`/home/coder`** is over 90% full\n- **`/dev/coder`** is over 80% full\n- **`/etc/coder`** is over 95% full\n" +} \ No newline at end of file From 62621d43e5cdc9b6dbe4ef7b6cdf220aa871c6bb Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 4 Feb 2025 16:34:32 +0000 Subject: [PATCH 14/37] chore: add more tests, fix broken sql query --- coderd/agentapi/workspacemonitor.go | 23 +- coderd/agentapi/workspacemonitor_test.go | 263 ++++++++++++++++++ coderd/database/dbgen/dbgen.go | 2 +- coderd/database/dbmock/dbmock.go | 24 +- ...000290_create_workspace_monitors.down.sql} | 0 ...> 000290_create_workspace_monitors.up.sql} | 0 coderd/database/queries.sql.go | 5 +- coderd/database/queries/workspacemonitors.sql | 5 +- 8 files changed, 298 insertions(+), 24 deletions(-) rename coderd/database/migrations/{000289_create_workspace_monitors.down.sql => 000290_create_workspace_monitors.down.sql} (100%) rename coderd/database/migrations/{000289_create_workspace_monitors.up.sql => 000290_create_workspace_monitors.up.sql} (100%) diff --git a/coderd/agentapi/workspacemonitor.go b/coderd/agentapi/workspacemonitor.go index cde9db3f10200..e947174eadc13 100644 --- a/coderd/agentapi/workspacemonitor.go +++ b/coderd/agentapi/workspacemonitor.go @@ -71,11 +71,14 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a oldState := memoryMonitor.State newState := m.nextState(oldState, memoryUsageStates) - shouldNotify := oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK - var debouncedUntil = m.Clock.Now() + shouldNotify := oldState == database.WorkspaceMonitorStateOK && + newState == database.WorkspaceMonitorStateNOK && + m.Clock.Now().After(memoryMonitor.DebouncedUntil) + + var debouncedUntil = memoryMonitor.DebouncedUntil if shouldNotify { - debouncedUntil = debouncedUntil.Add(m.Debounce) + debouncedUntil = m.Clock.Now().Add(m.Debounce) } err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ @@ -131,7 +134,7 @@ func (m *WorkspaceMonitorAPI) getOrInsertMemoryMonitor(ctx context.Context) (dat State: database.WorkspaceMonitorStateOK, CreatedAt: dbtime.Now(), UpdatedAt: dbtime.Now(), - DebouncedUntil: dbtime.Now(), + DebouncedUntil: time.Time{}, }, ) } @@ -172,11 +175,13 @@ func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, da oldState := volumeMonitor.State newState := m.nextState(oldState, volumeUsageStates) - shouldNotify := oldState == database.WorkspaceMonitorStateOK && newState == database.WorkspaceMonitorStateNOK + shouldNotify := oldState == database.WorkspaceMonitorStateOK && + newState == database.WorkspaceMonitorStateNOK && + m.Clock.Now().After(volumeMonitor.DebouncedUntil) - var debouncedUntil = m.Clock.Now() + var debouncedUntil = volumeMonitor.DebouncedUntil if shouldNotify { - debouncedUntil = debouncedUntil.Add(m.Debounce) + debouncedUntil = m.Clock.Now().Add(m.Debounce) } err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ @@ -234,7 +239,7 @@ func (m *WorkspaceMonitorAPI) getOrInsertVolumeMonitor(ctx context.Context, path State: database.WorkspaceMonitorStateOK, CreatedAt: dbtime.Now(), UpdatedAt: dbtime.Now(), - DebouncedUntil: dbtime.Now(), + DebouncedUntil: time.Time{}, }, ) } @@ -248,7 +253,7 @@ func (m *WorkspaceMonitorAPI) getOrInsertVolumeMonitor(ctx context.Context, path func (m *WorkspaceMonitorAPI) nextState(oldState database.WorkspaceMonitorState, states []database.WorkspaceMonitorState) database.WorkspaceMonitorState { // If we do not have an OK in the last `X` datapoints, then we are // in an alert state. - lastXStates := states[len(states)-m.ConsecutiveNOKs:] + lastXStates := states[max(len(states)-m.ConsecutiveNOKs, 0):] if !slices.Contains(lastXStates, database.WorkspaceMonitorStateOK) { return database.WorkspaceMonitorStateNOK } diff --git a/coderd/agentapi/workspacemonitor_test.go b/coderd/agentapi/workspacemonitor_test.go index b4ea597c8a6ea..4f9a665374898 100644 --- a/coderd/agentapi/workspacemonitor_test.go +++ b/coderd/agentapi/workspacemonitor_test.go @@ -46,6 +46,128 @@ func workspaceMonitorAPI(t *testing.T) (*agentapi.WorkspaceMonitorAPI, database. }, user, clock, notifyEnq } +func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { + t.Parallel() + + // This test is a bit of a long one. We're testing that + // when a monitor goes into an alert state, it doesn't + // allow another notification to occur until after the + // debounce period. + // + // 1. OK -> NOK |> sends a notification + // 2. NOK -> OK |> does nothing + // 3. OK -> NOK |> does nothing due to debounce period + // 4. NOK -> OK |> does nothing + // 5. OK -> NOK |> sends a notification as debounce period exceeded + + api, _, clock, notifyEnq := workspaceMonitorAPI(t) + api.MinimumNOKs = 10 + api.ConsecutiveNOKs = 4 + api.MemoryMonitorEnabled = true + api.MemoryUsageThreshold = 80 + api.Debounce = 1 * time.Minute + + // Given: A monitor in an OK state + dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeMemory, + State: database.WorkspaceMonitorStateOK, + }) + + // When: The monitor is given a state that will trigger NOK + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + + // Then: We expect there to be a notification sent + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 1) + notifyEnq.Clear() + + // When: The monitor moves to an OK state from NOK + clock.Advance(api.Debounce / 4) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + }, + }) + + // Then: We expect no new notifications + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to a NOK state before the debounced time. + clock.Advance(api.Debounce / 4) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + + // Then: We expect no new notifications (showing the debouncer working) + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to an OK state from NOK + clock.Advance(api.Debounce / 4) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + }, + }) + + // Then: We still expect no new notifications + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to a NOK state after the debounce period. + clock.Advance(api.Debounce/4 + 1*time.Second) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + + // Then: We expect a notification + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 1) +} + func TestWorkspaceMemoryMonitor(t *testing.T) { t.Parallel() @@ -176,6 +298,147 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { } } +func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { + t.Parallel() + + // This test is a bit of a long one. We're testing that + // when a monitor goes into an alert state, it doesn't + // allow another notification to occur until after the + // debounce period. + // + // 1. OK -> NOK |> sends a notification + // 2. NOK -> OK |> does nothing + // 3. OK -> NOK |> does nothing due to debounce period + // 4. NOK -> OK |> does nothing + // 5. OK -> NOK |> sends a notification as debounce period exceeded + + volumePath := "/home/coder" + + api, _, clock, notifyEnq := workspaceMonitorAPI(t) + api.MinimumNOKs = 10 + api.ConsecutiveNOKs = 4 + api.VolumeUsageThresholds = map[string]int32{ + volumePath: 80, + } + api.Debounce = 1 * time.Minute + + // Given: A monitor in an OK state + dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ + WorkspaceID: api.WorkspaceID, + MonitorType: database.WorkspaceMonitorTypeVolume, + VolumePath: sql.NullString{Valid: true, String: volumePath}, + State: database.WorkspaceMonitorStateOK, + }) + + // When: The monitor is given a state that will trigger NOK + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + { + Path: volumePath, + Used: 10, + Total: 10, + }, + }, + }, + }, + }) + + // Then: We expect there to be a notification sent + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + notifyEnq.Clear() + + // When: The monitor moves to an OK state from NOK + clock.Advance(api.Debounce / 4) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + { + Path: volumePath, + Used: 1, + Total: 10, + }, + }, + }, + }, + }) + + // Then: We expect no new notifications + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to a NOK state before the debounced time. + clock.Advance(api.Debounce / 4) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + { + Path: volumePath, + Used: 10, + Total: 10, + }, + }, + }, + }, + }) + + // Then: We expect no new notifications (showing the debouncer working) + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to an OK state from NOK + clock.Advance(api.Debounce / 4) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + { + Path: volumePath, + Used: 1, + Total: 10, + }, + }, + }, + }, + }) + + // Then: We still expect no new notifications + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 0) + notifyEnq.Clear() + + // When: The monitor moves back to a NOK state after the debounce period. + clock.Advance(api.Debounce/4 + 1*time.Second) + api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + { + Path: volumePath, + Used: 10, + Total: 10, + }, + }, + }, + }, + }) + + // Then: We expect a notification + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) +} + func TestWorkspaceVolumeMonitor(t *testing.T) { t.Parallel() diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index b1781cb199ddf..f307694c0b1ed 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -380,7 +380,7 @@ func WorkspaceMonitor(t testing.TB, db database.Store, orig database.WorkspaceMo State: takeFirst(orig.State, database.WorkspaceMonitorStateOK), CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), - DebouncedUntil: takeFirst(orig.DebouncedUntil, dbtime.Now()), + DebouncedUntil: takeFirst(orig.DebouncedUntil, time.Time{}), }) require.NoError(t, err, "insert monitor") diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 0b9bedf5e71dc..0d9c4a47ad118 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3457,18 +3457,18 @@ func (mr *MockStoreMockRecorder) GetWorkspaceModulesCreatedAfter(ctx, createdAt } // GetWorkspaceMonitor mocks base method. -func (m *MockStore) GetWorkspaceMonitor(arg0 context.Context, arg1 database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { +func (m *MockStore) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceMonitor", arg0, arg1) + ret := m.ctrl.Call(m, "GetWorkspaceMonitor", ctx, arg) ret0, _ := ret[0].(database.WorkspaceMonitor) ret1, _ := ret[1].(error) return ret0, ret1 } // GetWorkspaceMonitor indicates an expected call of GetWorkspaceMonitor. -func (mr *MockStoreMockRecorder) GetWorkspaceMonitor(arg0, arg1 any) *gomock.Call { +func (mr *MockStoreMockRecorder) GetWorkspaceMonitor(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).GetWorkspaceMonitor), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).GetWorkspaceMonitor), ctx, arg) } // GetWorkspaceProxies mocks base method. @@ -4433,18 +4433,18 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceModule(ctx, arg any) *gomock.Cal } // InsertWorkspaceMonitor mocks base method. -func (m *MockStore) InsertWorkspaceMonitor(arg0 context.Context, arg1 database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { +func (m *MockStore) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertWorkspaceMonitor", arg0, arg1) + ret := m.ctrl.Call(m, "InsertWorkspaceMonitor", ctx, arg) ret0, _ := ret[0].(database.WorkspaceMonitor) ret1, _ := ret[1].(error) return ret0, ret1 } // InsertWorkspaceMonitor indicates an expected call of InsertWorkspaceMonitor. -func (mr *MockStoreMockRecorder) InsertWorkspaceMonitor(arg0, arg1 any) *gomock.Call { +func (mr *MockStoreMockRecorder) InsertWorkspaceMonitor(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceMonitor), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceMonitor), ctx, arg) } // InsertWorkspaceProxy mocks base method. @@ -5564,17 +5564,17 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(ctx, arg any) *gomock } // UpdateWorkspaceMonitor mocks base method. -func (m *MockStore) UpdateWorkspaceMonitor(arg0 context.Context, arg1 database.UpdateWorkspaceMonitorParams) error { +func (m *MockStore) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateWorkspaceMonitor", arg0, arg1) + ret := m.ctrl.Call(m, "UpdateWorkspaceMonitor", ctx, arg) ret0, _ := ret[0].(error) return ret0 } // UpdateWorkspaceMonitor indicates an expected call of UpdateWorkspaceMonitor. -func (mr *MockStoreMockRecorder) UpdateWorkspaceMonitor(arg0, arg1 any) *gomock.Call { +func (mr *MockStoreMockRecorder) UpdateWorkspaceMonitor(ctx, arg any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceMonitor), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceMonitor), ctx, arg) } // UpdateWorkspaceNextStartAt mocks base method. diff --git a/coderd/database/migrations/000289_create_workspace_monitors.down.sql b/coderd/database/migrations/000290_create_workspace_monitors.down.sql similarity index 100% rename from coderd/database/migrations/000289_create_workspace_monitors.down.sql rename to coderd/database/migrations/000290_create_workspace_monitors.down.sql diff --git a/coderd/database/migrations/000289_create_workspace_monitors.up.sql b/coderd/database/migrations/000290_create_workspace_monitors.up.sql similarity index 100% rename from coderd/database/migrations/000289_create_workspace_monitors.up.sql rename to coderd/database/migrations/000290_create_workspace_monitors.up.sql diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 251afe4ec08d8..caaecb879affa 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15056,7 +15056,10 @@ SET state = $4, updated_at = $5, debounced_until = $6 -WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3 +WHERE + workspace_id = $1 AND + monitor_type = $2 AND + volume_path IS NOT DISTINCT FROM $3 ` type UpdateWorkspaceMonitorParams struct { diff --git a/coderd/database/queries/workspacemonitors.sql b/coderd/database/queries/workspacemonitors.sql index 05b994bf9eb59..eab61c5b5d387 100644 --- a/coderd/database/queries/workspacemonitors.sql +++ b/coderd/database/queries/workspacemonitors.sql @@ -31,4 +31,7 @@ SET state = $4, updated_at = $5, debounced_until = $6 -WHERE workspace_id = $1 AND monitor_type = $2 AND volume_path = $3; +WHERE + workspace_id = $1 AND + monitor_type = $2 AND + volume_path IS NOT DISTINCT FROM $3; From 7522b37293549757ea4116c5af336597557be5ae Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 4 Feb 2025 22:03:25 +0000 Subject: [PATCH 15/37] chore: update to match main --- coderd/agentapi/api.go | 6 + coderd/agentapi/workspacemonitor.go | 191 ++++++-------- coderd/agentapi/workspacemonitor_test.go | 124 +++++----- coderd/database/dbauthz/dbauthz.go | 38 ++- coderd/database/dbauthz/dbauthz_test.go | 169 +++++-------- coderd/database/dbgen/dbgen.go | 41 ++-- coderd/database/dbmem/dbmem.go | 127 +++++----- coderd/database/dbmetrics/querymetrics.go | 35 ++- coderd/database/dbmock/dbmock.go | 72 +++--- coderd/database/dump.sql | 36 +-- .../000289_oom_and_ood_notification.down.sql | 2 - .../000289_oom_and_ood_notification.up.sql | 40 --- .../000290_create_workspace_monitors.down.sql | 3 - .../000290_create_workspace_monitors.up.sql | 24 -- .../000291_create_workspace_monitors.down.sql | 11 + .../000291_create_workspace_monitors.up.sql | 14 ++ .../000289_create_workspace_monitors.up.sql | 15 -- coderd/database/models.go | 208 ++++++---------- coderd/database/querier.go | 5 +- coderd/database/queries.sql.go | 232 ++++++++---------- .../workspaceagentresourcemonitors.sql | 32 ++- coderd/database/queries/workspacemonitors.sql | 37 --- .../provisionerdserver/provisionerdserver.go | 24 +- 23 files changed, 598 insertions(+), 888 deletions(-) delete mode 100644 coderd/database/migrations/000289_oom_and_ood_notification.down.sql delete mode 100644 coderd/database/migrations/000289_oom_and_ood_notification.up.sql delete mode 100644 coderd/database/migrations/000290_create_workspace_monitors.down.sql delete mode 100644 coderd/database/migrations/000290_create_workspace_monitors.up.sql create mode 100644 coderd/database/migrations/000291_create_workspace_monitors.down.sql create mode 100644 coderd/database/migrations/000291_create_workspace_monitors.up.sql delete mode 100644 coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql delete mode 100644 coderd/database/queries/workspacemonitors.sql diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 8e0d480c3312f..0d81dc8955541 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -155,9 +155,15 @@ func New(opts Options) *API { } api.WorkspaceMonitorAPI = &WorkspaceMonitorAPI{ + AgentID: opts.AgentID, + WorkspaceID: opts.WorkspaceID, Clock: opts.Clock, Database: opts.Database, NotificationsEnqueuer: opts.NotificationsEnqueuer, + + // These values assume a window of 20 + MinimumNOKs: 4, + ConsecutiveNOKs: 10, } api.DRPCService = &tailnet.DRPCService{ diff --git a/coderd/agentapi/workspacemonitor.go b/coderd/agentapi/workspacemonitor.go index e947174eadc13..2b4323b5bac91 100644 --- a/coderd/agentapi/workspacemonitor.go +++ b/coderd/agentapi/workspacemonitor.go @@ -3,13 +3,15 @@ package agentapi import ( "context" "database/sql" + "errors" "fmt" "slices" "time" - "github.com/google/uuid" "golang.org/x/xerrors" + "github.com/google/uuid" + agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" @@ -19,16 +21,13 @@ import ( ) type WorkspaceMonitorAPI struct { + AgentID uuid.UUID WorkspaceID uuid.UUID Clock quartz.Clock Database database.Store NotificationsEnqueuer notifications.Enqueuer - MemoryMonitorEnabled bool - MemoryUsageThreshold int32 - VolumeUsageThresholds map[string]int32 - Debounce time.Duration // How many datapoints in a row are required to @@ -43,10 +42,8 @@ type WorkspaceMonitorAPI struct { func (m *WorkspaceMonitorAPI) UpdateWorkspaceMonitor(ctx context.Context, req *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { res := &agentproto.WorkspaceMonitorUpdateResponse{} - if m.MemoryMonitorEnabled { - if err := m.monitorMemory(ctx, req.Datapoints); err != nil { - return nil, xerrors.Errorf("monitor memory: %w", err) - } + if err := m.monitorMemory(ctx, req.Datapoints); err != nil { + return nil, xerrors.Errorf("monitor memory: %w", err) } if err := m.monitorVolumes(ctx, req.Datapoints); err != nil { @@ -57,34 +54,42 @@ func (m *WorkspaceMonitorAPI) UpdateWorkspaceMonitor(ctx context.Context, req *a } func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint) error { - memoryMonitor, err := m.getOrInsertMemoryMonitor(ctx) + monitor, err := m.Database.FetchMemoryResourceMonitorsByAgentID(ctx, m.AgentID) if err != nil { - return xerrors.Errorf("get or insert memory monitor: %w", err) + // It is valid for an agent to not have a memory monitor, so we + // do not want to treat it as an error. + if errors.Is(err, sql.ErrNoRows) { + return nil + } + + return xerrors.Errorf("fetch memory resource monitor: %w", err) } - memoryUsageDatapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage, 0, len(datapoints)) + if !monitor.Enabled { + return nil + } + + usageDatapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage, 0, len(datapoints)) for _, datapoint := range datapoints { - memoryUsageDatapoints = append(memoryUsageDatapoints, datapoint.Memory) + usageDatapoints = append(usageDatapoints, datapoint.Memory) } - memoryUsageStates := m.calculateMemoryUsageStates(memoryUsageDatapoints) + memoryUsageStates := calculateMemoryUsageStates(monitor, usageDatapoints) - oldState := memoryMonitor.State + oldState := monitor.State newState := m.nextState(oldState, memoryUsageStates) - shouldNotify := oldState == database.WorkspaceMonitorStateOK && - newState == database.WorkspaceMonitorStateNOK && - m.Clock.Now().After(memoryMonitor.DebouncedUntil) + shouldNotify := oldState == database.WorkspaceAgentMonitorStateOK && + newState == database.WorkspaceAgentMonitorStateNOK && + m.Clock.Now().After(monitor.DebouncedUntil) - var debouncedUntil = memoryMonitor.DebouncedUntil + var debouncedUntil = monitor.DebouncedUntil if shouldNotify { debouncedUntil = m.Clock.Now().Add(m.Debounce) } - err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - VolumePath: sql.NullString{Valid: false}, + err = m.Database.UpdateMemoryResourceMonitor(ctx, database.UpdateMemoryResourceMonitorParams{ + AgentID: m.AgentID, State: newState, UpdatedAt: dbtime.Time(m.Clock.Now()), DebouncedUntil: dbtime.Time(debouncedUntil), @@ -106,7 +111,7 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a notifications.TemplateWorkspaceOutOfMemory, map[string]string{ "workspace": workspace.Name, - "threshold": fmt.Sprintf("%d%%", m.MemoryUsageThreshold), + "threshold": fmt.Sprintf("%d%%", monitor.Threshold), }, "workspace-monitor-memory", ) @@ -118,34 +123,12 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a return nil } -func (m *WorkspaceMonitorAPI) getOrInsertMemoryMonitor(ctx context.Context) (database.WorkspaceMonitor, error) { - memoryMonitor, err := m.Database.GetWorkspaceMonitor(ctx, database.GetWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - }) +func (m *WorkspaceMonitorAPI) monitorVolumes(ctx context.Context, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint) error { + volumeMonitors, err := m.Database.FetchVolumesResourceMonitorsByAgentID(ctx, m.AgentID) if err != nil { - if xerrors.Is(err, sql.ErrNoRows) { - return m.Database.InsertWorkspaceMonitor( - ctx, - database.InsertWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - VolumePath: sql.NullString{Valid: false}, - State: database.WorkspaceMonitorStateOK, - CreatedAt: dbtime.Now(), - UpdatedAt: dbtime.Now(), - DebouncedUntil: time.Time{}, - }, - ) - } - - return database.WorkspaceMonitor{}, err + return xerrors.Errorf("get or insert volume monitor: %w", err) } - return memoryMonitor, nil -} - -func (m *WorkspaceMonitorAPI) monitorVolumes(ctx context.Context, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint) error { volumes := make(map[string][]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) for _, datapoint := range datapoints { @@ -156,8 +139,8 @@ func (m *WorkspaceMonitorAPI) monitorVolumes(ctx context.Context, datapoints []* } } - for path, volume := range volumes { - if err := m.monitorVolume(ctx, path, volume); err != nil { + for _, monitor := range volumeMonitors { + if err := m.monitorVolume(ctx, monitor, monitor.Path, volumes[monitor.Path]); err != nil { return xerrors.Errorf("monitor volume: %w", err) } } @@ -165,34 +148,37 @@ func (m *WorkspaceMonitorAPI) monitorVolumes(ctx context.Context, datapoints []* return nil } -func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) error { - volumeMonitor, err := m.getOrInsertVolumeMonitor(ctx, path) - if err != nil { - return xerrors.Errorf("get or insert volume monitor: %w", err) +func (m *WorkspaceMonitorAPI) monitorVolume( + ctx context.Context, + monitor database.WorkspaceAgentVolumeResourceMonitor, + path string, + datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage, +) error { + if !monitor.Enabled { + return nil } - volumeUsageStates := m.calculateVolumeUsageStates(path, datapoints) + volumeUsageStates := calculateVolumeUsageStates(monitor, datapoints) - oldState := volumeMonitor.State + oldState := monitor.State newState := m.nextState(oldState, volumeUsageStates) - shouldNotify := oldState == database.WorkspaceMonitorStateOK && - newState == database.WorkspaceMonitorStateNOK && - m.Clock.Now().After(volumeMonitor.DebouncedUntil) - var debouncedUntil = volumeMonitor.DebouncedUntil + shouldNotify := oldState == database.WorkspaceAgentMonitorStateOK && + newState == database.WorkspaceAgentMonitorStateNOK && + m.Clock.Now().After(monitor.DebouncedUntil) + + var debouncedUntil = monitor.DebouncedUntil if shouldNotify { debouncedUntil = m.Clock.Now().Add(m.Debounce) } - err = m.Database.UpdateWorkspaceMonitor(ctx, database.UpdateWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: path}, + if err := m.Database.UpdateVolumeResourceMonitor(ctx, database.UpdateVolumeResourceMonitorParams{ + AgentID: m.AgentID, + Path: path, State: newState, UpdatedAt: dbtime.Time(m.Clock.Now()), DebouncedUntil: dbtime.Time(debouncedUntil), - }) - if err != nil { + }); err != nil { return xerrors.Errorf("update workspace monitor: %w", err) } @@ -209,7 +195,7 @@ func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, da notifications.TemplateWorkspaceOutOfDisk, map[string]string{ "workspace": workspace.Name, - "threshold": fmt.Sprintf("%d%%", m.VolumeUsageThresholds[path]), + "threshold": fmt.Sprintf("%d%%", monitor.Threshold), "volume": path, }, "workspace-monitor-memory", @@ -222,72 +208,50 @@ func (m *WorkspaceMonitorAPI) monitorVolume(ctx context.Context, path string, da return nil } -func (m *WorkspaceMonitorAPI) getOrInsertVolumeMonitor(ctx context.Context, path string) (database.WorkspaceMonitor, error) { - memoryMonitor, err := m.Database.GetWorkspaceMonitor(ctx, database.GetWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: path}, - }) - if err != nil { - if xerrors.Is(err, sql.ErrNoRows) { - return m.Database.InsertWorkspaceMonitor( - ctx, - database.InsertWorkspaceMonitorParams{ - WorkspaceID: m.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: path}, - State: database.WorkspaceMonitorStateOK, - CreatedAt: dbtime.Now(), - UpdatedAt: dbtime.Now(), - DebouncedUntil: time.Time{}, - }, - ) - } - - return database.WorkspaceMonitor{}, err - } - - return memoryMonitor, nil -} - -func (m *WorkspaceMonitorAPI) nextState(oldState database.WorkspaceMonitorState, states []database.WorkspaceMonitorState) database.WorkspaceMonitorState { +func (m *WorkspaceMonitorAPI) nextState( + oldState database.WorkspaceAgentMonitorState, + states []database.WorkspaceAgentMonitorState, +) database.WorkspaceAgentMonitorState { // If we do not have an OK in the last `X` datapoints, then we are // in an alert state. lastXStates := states[max(len(states)-m.ConsecutiveNOKs, 0):] - if !slices.Contains(lastXStates, database.WorkspaceMonitorStateOK) { - return database.WorkspaceMonitorStateNOK + if !slices.Contains(lastXStates, database.WorkspaceAgentMonitorStateOK) { + return database.WorkspaceAgentMonitorStateNOK } nokCount := 0 for _, state := range states { - if state == database.WorkspaceMonitorStateNOK { + if state == database.WorkspaceAgentMonitorStateNOK { nokCount++ } } // If there are enough NOK datapoints, we should be in an alert state. if nokCount >= m.MinimumNOKs { - return database.WorkspaceMonitorStateNOK + return database.WorkspaceAgentMonitorStateNOK } // If there are no NOK datapoints, we should be in an OK state. if nokCount == 0 { - return database.WorkspaceMonitorStateOK + return database.WorkspaceAgentMonitorStateOK } // Otherwise we stay in the same state as last. return oldState } -func (m *WorkspaceMonitorAPI) calculateMemoryUsageStates(datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) []database.WorkspaceMonitorState { - states := make([]database.WorkspaceMonitorState, 0, len(datapoints)) +func calculateMemoryUsageStates( + monitor database.WorkspaceAgentMemoryResourceMonitor, + datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage, +) []database.WorkspaceAgentMonitorState { + states := make([]database.WorkspaceAgentMonitorState, 0, len(datapoints)) for _, datapoint := range datapoints { percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) - state := database.WorkspaceMonitorStateOK - if percent >= m.MemoryUsageThreshold { - state = database.WorkspaceMonitorStateNOK + state := database.WorkspaceAgentMonitorStateOK + if percent >= monitor.Threshold { + state = database.WorkspaceAgentMonitorStateNOK } states = append(states, state) @@ -296,15 +260,18 @@ func (m *WorkspaceMonitorAPI) calculateMemoryUsageStates(datapoints []*agentprot return states } -func (m *WorkspaceMonitorAPI) calculateVolumeUsageStates(path string, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) []database.WorkspaceMonitorState { - states := make([]database.WorkspaceMonitorState, 0, len(datapoints)) +func calculateVolumeUsageStates( + monitor database.WorkspaceAgentVolumeResourceMonitor, + datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage, +) []database.WorkspaceAgentMonitorState { + states := make([]database.WorkspaceAgentMonitorState, 0, len(datapoints)) for _, datapoint := range datapoints { percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) - state := database.WorkspaceMonitorStateOK - if percent >= m.VolumeUsageThresholds[path] { - state = database.WorkspaceMonitorStateNOK + state := database.WorkspaceAgentMonitorStateOK + if percent >= monitor.Threshold { + state = database.WorkspaceAgentMonitorStateNOK } states = append(states, state) diff --git a/coderd/agentapi/workspacemonitor_test.go b/coderd/agentapi/workspacemonitor_test.go index 4f9a665374898..64881da533369 100644 --- a/coderd/agentapi/workspacemonitor_test.go +++ b/coderd/agentapi/workspacemonitor_test.go @@ -2,10 +2,10 @@ package agentapi_test import ( "context" - "database/sql" "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" @@ -29,16 +29,36 @@ func workspaceMonitorAPI(t *testing.T) (*agentapi.WorkspaceMonitorAPI, database. OrganizationID: org.ID, CreatedBy: user.ID, }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ OrganizationID: org.ID, TemplateID: template.ID, OwnerID: user.ID, }) + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + JobID: job.ID, + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + }) + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + }) notifyEnq := ¬ificationstest.FakeEnqueuer{} clock := quartz.NewMock(t) return &agentapi.WorkspaceMonitorAPI{ + AgentID: agent.ID, WorkspaceID: workspace.ID, Clock: clock, Database: db, @@ -63,15 +83,13 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { api, _, clock, notifyEnq := workspaceMonitorAPI(t) api.MinimumNOKs = 10 api.ConsecutiveNOKs = 4 - api.MemoryMonitorEnabled = true - api.MemoryUsageThreshold = 80 api.Debounce = 1 * time.Minute // Given: A monitor in an OK state - dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - State: database.WorkspaceMonitorStateOK, + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, }) // When: The monitor is given a state that will trigger NOK @@ -178,8 +196,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { thresholdPercent int32 minimumNOKs int consecutiveNOKs int - previousState database.WorkspaceMonitorState - expectState database.WorkspaceMonitorState + previousState database.WorkspaceAgentMonitorState + expectState database.WorkspaceAgentMonitorState shouldNotify bool }{ { @@ -189,8 +207,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateOK, - expectState: database.WorkspaceMonitorStateOK, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, { @@ -200,8 +218,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: true, }, { @@ -211,8 +229,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { thresholdPercent: 80, minimumNOKs: 4, consecutiveNOKs: 10, - previousState: database.WorkspaceMonitorStateOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: true, }, { @@ -222,8 +240,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateNOK, - expectState: database.WorkspaceMonitorStateOK, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, { @@ -233,8 +251,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateNOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: false, }, { @@ -244,8 +262,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { thresholdPercent: 80, minimumNOKs: 4, consecutiveNOKs: 10, - previousState: database.WorkspaceMonitorStateNOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: false, }, } @@ -259,8 +277,6 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { api, user, clock, notifyEnq := workspaceMonitorAPI(t) api.MinimumNOKs = tt.minimumNOKs api.ConsecutiveNOKs = tt.consecutiveNOKs - api.MemoryMonitorEnabled = true - api.MemoryUsageThreshold = tt.thresholdPercent datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.memoryUsage)) collectedAt := clock.Now() @@ -275,10 +291,10 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }) } - dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeMemory, - State: tt.previousState, + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: tt.previousState, + Threshold: tt.thresholdPercent, }) clock.Set(collectedAt) @@ -317,17 +333,14 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { api, _, clock, notifyEnq := workspaceMonitorAPI(t) api.MinimumNOKs = 10 api.ConsecutiveNOKs = 4 - api.VolumeUsageThresholds = map[string]int32{ - volumePath: 80, - } api.Debounce = 1 * time.Minute // Given: A monitor in an OK state - dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: volumePath}, - State: database.WorkspaceMonitorStateOK, + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: volumePath, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, }) // When: The monitor is given a state that will trigger NOK @@ -448,8 +461,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { volumeUsage []int32 volumeTotal int32 thresholdPercent int32 - previousState database.WorkspaceMonitorState - expectState database.WorkspaceMonitorState + previousState database.WorkspaceAgentMonitorState + expectState database.WorkspaceAgentMonitorState shouldNotify bool minimumNOKs int consecutiveNOKs int @@ -462,8 +475,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateOK, - expectState: database.WorkspaceMonitorStateOK, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, { @@ -474,8 +487,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: true, }, { @@ -486,8 +499,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { thresholdPercent: 80, minimumNOKs: 4, consecutiveNOKs: 10, - previousState: database.WorkspaceMonitorStateOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: true, }, { @@ -498,8 +511,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateNOK, - expectState: database.WorkspaceMonitorStateOK, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, { @@ -510,8 +523,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { thresholdPercent: 80, consecutiveNOKs: 4, minimumNOKs: 10, - previousState: database.WorkspaceMonitorStateNOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: false, }, { @@ -522,8 +535,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { thresholdPercent: 80, minimumNOKs: 4, consecutiveNOKs: 10, - previousState: database.WorkspaceMonitorStateNOK, - expectState: database.WorkspaceMonitorStateNOK, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: false, }, } @@ -537,9 +550,6 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { api, user, clock, notifyEnq := workspaceMonitorAPI(t) api.MinimumNOKs = tt.minimumNOKs api.ConsecutiveNOKs = tt.consecutiveNOKs - api.VolumeUsageThresholds = map[string]int32{ - tt.volumePath: tt.thresholdPercent, - } datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.volumeUsage)) collectedAt := clock.Now() @@ -560,11 +570,11 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { }) } - dbgen.WorkspaceMonitor(t, api.Database, database.WorkspaceMonitor{ - WorkspaceID: api.WorkspaceID, - MonitorType: database.WorkspaceMonitorTypeVolume, - VolumePath: sql.NullString{Valid: true, String: tt.volumePath}, - State: tt.previousState, + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: tt.volumePath, + State: tt.previousState, + Threshold: tt.thresholdPercent, }) clock.Set(collectedAt) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 547e9bd7c4d57..5f9067379b3ad 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2770,14 +2770,6 @@ func (q *querier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt return q.db.GetWorkspaceModulesCreatedAfter(ctx, createdAt) } -func (q *querier) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { - return database.WorkspaceMonitor{}, err - } - - return q.db.GetWorkspaceMonitor(ctx, arg) -} - func (q *querier) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { return fetchWithPostFilter(q.auth, policy.ActionRead, func(ctx context.Context, _ interface{}) ([]database.WorkspaceProxy, error) { return q.db.GetWorkspaceProxies(ctx) @@ -3379,13 +3371,6 @@ func (q *querier) InsertWorkspaceModule(ctx context.Context, arg database.Insert return q.db.InsertWorkspaceModule(ctx, arg) } -func (q *querier) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { - return database.WorkspaceMonitor{}, err - } - return q.db.InsertWorkspaceMonitor(ctx, arg) -} - func (q *querier) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { return insert(q.log, q.auth, rbac.ResourceWorkspaceProxy, q.db.InsertWorkspaceProxy)(ctx, arg) } @@ -3633,6 +3618,14 @@ func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemb return q.db.UpdateMemberRoles(ctx, arg) } +func (q *querier) UpdateMemoryResourceMonitor(ctx context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return err + } + + return q.db.UpdateMemoryResourceMonitor(ctx, arg) +} + func (q *querier) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceNotificationTemplate); err != nil { return database.NotificationTemplate{}, err @@ -4029,6 +4022,14 @@ func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserS return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserStatus)(ctx, arg) } +func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { + return err + } + + return q.db.UpdateVolumeResourceMonitor(ctx, arg) +} + func (q *querier) UpdateWorkspace(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { fetch := func(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { w, err := q.db.GetWorkspaceByID(ctx, arg.ID) @@ -4205,13 +4206,6 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg) } -func (q *querier) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { - if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { - return err - } - return q.db.UpdateWorkspaceMonitor(ctx, arg) -} - func (q *querier) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { fetch := func(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) (database.Workspace, error) { return q.db.GetWorkspaceByID(ctx, arg.ID) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index bc44c09853e54..4685e120a3521 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4563,112 +4563,79 @@ func (s *MethodTestSuite) TestOAuth2ProviderAppTokens() { })) } -func (s *MethodTestSuite) TestWorkspaceMonitor() { - s.Run("GetWorkspaceMonitor", s.Subtest(func(db database.Store, check *expects) { - user := dbgen.User(s.T(), db, database.User{}) - org := dbgen.Organization(s.T(), db, database.Organization{}) - template := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OwnerID: user.ID, - OrganizationID: org.ID, - TemplateID: template.ID, - }) - monitor := dbgen.WorkspaceMonitor(s.T(), db, database.WorkspaceMonitor{ - WorkspaceID: workspace.ID, - MonitorType: database.WorkspaceMonitorTypeMemory, - VolumePath: sql.NullString{}, - }) - - check.Args(database.GetWorkspaceMonitorParams{ - WorkspaceID: monitor.WorkspaceID, - MonitorType: monitor.MonitorType, - VolumePath: monitor.VolumePath, - }).Asserts(rbac.ResourceSystem, policy.ActionRead) - })) - s.Run("InsertWorkspaceMonitor", s.Subtest(func(db database.Store, check *expects) { - user := dbgen.User(s.T(), db, database.User{}) - org := dbgen.Organization(s.T(), db, database.Organization{}) - template := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OwnerID: user.ID, - OrganizationID: org.ID, - TemplateID: template.ID, - }) - - check.Args(database.InsertWorkspaceMonitorParams{ - WorkspaceID: workspace.ID, - MonitorType: database.WorkspaceMonitorTypeMemory, - State: database.WorkspaceMonitorStateOK, - }).Asserts(rbac.ResourceSystem, policy.ActionCreate) - })) - s.Run("UpdateWorkspaceMonitor", s.Subtest(func(db database.Store, check *expects) { - user := dbgen.User(s.T(), db, database.User{}) - org := dbgen.Organization(s.T(), db, database.Organization{}) - template := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OwnerID: user.ID, - OrganizationID: org.ID, - TemplateID: template.ID, - }) - monitor := dbgen.WorkspaceMonitor(s.T(), db, database.WorkspaceMonitor{ - WorkspaceID: workspace.ID, - }) - - check.Args(database.UpdateWorkspaceMonitorParams{ - WorkspaceID: monitor.WorkspaceID, - MonitorType: monitor.MonitorType, - State: database.WorkspaceMonitorStateNOK, - }).Asserts(rbac.ResourceSystem, policy.ActionUpdate) - })) -} - func (s *MethodTestSuite) TestResourcesMonitor() { - s.Run("InsertMemoryResourceMonitor", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertMemoryResourceMonitorParams{}).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionCreate) - })) - - s.Run("InsertVolumeResourceMonitor", s.Subtest(func(db database.Store, check *expects) { - dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) - check.Args(database.InsertVolumeResourceMonitorParams{}).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionCreate) - })) + createAgent := func(t *testing.T, db database.Store) database.WorkspaceAgent { + t.Helper() - s.Run("FetchMemoryResourceMonitorsByAgentID", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - o := dbgen.Organization(s.T(), db, database.Organization{}) - tpl := dbgen.Template(s.T(), db, database.Template{ + u := dbgen.User(t, db, database.User{}) + o := dbgen.Organization(t, db, database.Organization{}) + tpl := dbgen.Template(t, db, database.Template{ OrganizationID: o.ID, CreatedBy: u.ID, }) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{ TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, OrganizationID: o.ID, CreatedBy: u.ID, }) - w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + w := dbgen.Workspace(t, db, database.WorkspaceTable{ TemplateID: tpl.ID, OrganizationID: o.ID, OwnerID: u.ID, }) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + j := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, }) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + b := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ JobID: j.ID, WorkspaceID: w.ID, TemplateVersionID: tv.ID, }) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: b.JobID}) - agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) + res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: b.JobID}) + agt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID}) + + return agt + } + + s.Run("InsertMemoryResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt := createAgent(s.T(), db) + + check.Args(database.InsertMemoryResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionCreate) + })) + + s.Run("InsertVolumeResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt := createAgent(s.T(), db) + + check.Args(database.InsertVolumeResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionCreate) + })) + + s.Run("UpdateMemoryResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt := createAgent(s.T(), db) + + check.Args(database.UpdateMemoryResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionUpdate) + })) + + s.Run("UpdateVolumeResourceMonitor", s.Subtest(func(db database.Store, check *expects) { + agt := createAgent(s.T(), db) + + check.Args(database.UpdateVolumeResourceMonitorParams{ + AgentID: agt.ID, + State: database.WorkspaceAgentMonitorStateOK, + }).Asserts(rbac.ResourceWorkspaceAgentResourceMonitor, policy.ActionUpdate) + })) + + s.Run("FetchMemoryResourceMonitorsByAgentID", s.Subtest(func(db database.Store, check *expects) { + agt := createAgent(s.T(), db) + dbgen.WorkspaceAgentMemoryResourceMonitor(s.T(), db, database.WorkspaceAgentMemoryResourceMonitor{ AgentID: agt.ID, Enabled: true, @@ -4683,32 +4650,8 @@ func (s *MethodTestSuite) TestResourcesMonitor() { })) s.Run("FetchVolumesResourceMonitorsByAgentID", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - o := dbgen.Organization(s.T(), db, database.Organization{}) - tpl := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - OrganizationID: o.ID, - CreatedBy: u.ID, - }) - w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - TemplateID: tpl.ID, - OrganizationID: o.ID, - OwnerID: u.ID, - }) - j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeWorkspaceBuild, - }) - b := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ - JobID: j.ID, - WorkspaceID: w.ID, - TemplateVersionID: tv.ID, - }) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{JobID: b.JobID}) - agt := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ResourceID: res.ID}) + agt := createAgent(s.T(), db) + dbgen.WorkspaceAgentVolumeResourceMonitor(s.T(), db, database.WorkspaceAgentVolumeResourceMonitor{ AgentID: agt.ID, Path: "/var/lib", diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index a5483e2aea5b9..a82a93f5959a4 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -370,23 +370,6 @@ func WorkspaceBuildParameters(t testing.TB, db database.Store, orig []database.W return params } -func WorkspaceMonitor(t testing.TB, db database.Store, orig database.WorkspaceMonitor) database.WorkspaceMonitor { - t.Helper() - - monitor, err := db.InsertWorkspaceMonitor(genCtx, database.InsertWorkspaceMonitorParams{ - WorkspaceID: takeFirst(orig.WorkspaceID, uuid.New()), - MonitorType: takeFirst(orig.MonitorType, database.WorkspaceMonitorTypeMemory), - VolumePath: takeFirst(orig.VolumePath, sql.NullString{}), - State: takeFirst(orig.State, database.WorkspaceMonitorStateOK), - CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), - UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()), - DebouncedUntil: takeFirst(orig.DebouncedUntil, time.Time{}), - }) - require.NoError(t, err, "insert monitor") - - return monitor -} - func User(t testing.TB, db database.Store, orig database.User) database.User { user, err := db.InsertUser(genCtx, database.InsertUserParams{ ID: takeFirst(orig.ID, uuid.New()), @@ -1051,10 +1034,13 @@ func OAuth2ProviderAppToken(t testing.TB, db database.Store, seed database.OAuth func WorkspaceAgentMemoryResourceMonitor(t testing.TB, db database.Store, seed database.WorkspaceAgentMemoryResourceMonitor) database.WorkspaceAgentMemoryResourceMonitor { monitor, err := db.InsertMemoryResourceMonitor(genCtx, database.InsertMemoryResourceMonitorParams{ - AgentID: takeFirst(seed.AgentID, uuid.New()), - Enabled: takeFirst(seed.Enabled, true), - Threshold: takeFirst(seed.Threshold, 100), - CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), + AgentID: takeFirst(seed.AgentID, uuid.New()), + Enabled: takeFirst(seed.Enabled, true), + State: takeFirst(seed.State, database.WorkspaceAgentMonitorStateOK), + Threshold: takeFirst(seed.Threshold, 100), + CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), + UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()), + DebouncedUntil: takeFirst(seed.DebouncedUntil, time.Time{}), }) require.NoError(t, err, "insert workspace agent memory resource monitor") return monitor @@ -1062,11 +1048,14 @@ func WorkspaceAgentMemoryResourceMonitor(t testing.TB, db database.Store, seed d func WorkspaceAgentVolumeResourceMonitor(t testing.TB, db database.Store, seed database.WorkspaceAgentVolumeResourceMonitor) database.WorkspaceAgentVolumeResourceMonitor { monitor, err := db.InsertVolumeResourceMonitor(genCtx, database.InsertVolumeResourceMonitorParams{ - AgentID: takeFirst(seed.AgentID, uuid.New()), - Path: takeFirst(seed.Path, "/"), - Enabled: takeFirst(seed.Enabled, true), - Threshold: takeFirst(seed.Threshold, 100), - CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), + AgentID: takeFirst(seed.AgentID, uuid.New()), + Path: takeFirst(seed.Path, "/"), + Enabled: takeFirst(seed.Enabled, true), + State: takeFirst(seed.State, database.WorkspaceAgentMonitorStateOK), + Threshold: takeFirst(seed.Threshold, 100), + CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()), + UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()), + DebouncedUntil: takeFirst(seed.DebouncedUntil, time.Time{}), }) require.NoError(t, err, "insert workspace agent volume resource monitor") return monitor diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index c4e9449ec21df..7428a4b4d28ba 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -238,7 +238,6 @@ type data struct { workspaceResourceMetadata []database.WorkspaceResourceMetadatum workspaceResources []database.WorkspaceResource workspaceModules []database.WorkspaceModule - workspaceMonitors []database.WorkspaceMonitor workspaces []database.WorkspaceTable workspaceProxies []database.WorkspaceProxy customRoles []database.CustomRole @@ -7190,26 +7189,6 @@ func (q *FakeQuerier) GetWorkspaceModulesCreatedAfter(_ context.Context, created return modules, nil } -func (q *FakeQuerier) GetWorkspaceMonitor(_ context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - err := validateDatabaseType(arg) - if err != nil { - return database.WorkspaceMonitor{}, err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, monitor := range q.workspaceMonitors { - if monitor.WorkspaceID == arg.WorkspaceID && - monitor.MonitorType == arg.MonitorType && - monitor.VolumePath == arg.VolumePath { - return monitor, nil - } - } - - return database.WorkspaceMonitor{}, sql.ErrNoRows -} - func (q *FakeQuerier) GetWorkspaceProxies(_ context.Context) ([]database.WorkspaceProxy, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -7849,7 +7828,16 @@ func (q *FakeQuerier) InsertMemoryResourceMonitor(_ context.Context, arg databas q.mutex.Lock() defer q.mutex.Unlock() - monitor := database.WorkspaceAgentMemoryResourceMonitor(arg) + //nolint:unconvert // The structs field-order differs so this is needed. + monitor := database.WorkspaceAgentMemoryResourceMonitor(database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: arg.AgentID, + Enabled: arg.Enabled, + State: arg.State, + Threshold: arg.Threshold, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + DebouncedUntil: arg.DebouncedUntil, + }) q.workspaceAgentMemoryResourceMonitors = append(q.workspaceAgentMemoryResourceMonitors, monitor) return monitor, nil @@ -8492,11 +8480,14 @@ func (q *FakeQuerier) InsertVolumeResourceMonitor(_ context.Context, arg databas defer q.mutex.Unlock() monitor := database.WorkspaceAgentVolumeResourceMonitor{ - AgentID: arg.AgentID, - Path: arg.Path, - Enabled: arg.Enabled, - Threshold: arg.Threshold, - CreatedAt: arg.CreatedAt, + AgentID: arg.AgentID, + Path: arg.Path, + Enabled: arg.Enabled, + State: arg.State, + Threshold: arg.Threshold, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + DebouncedUntil: arg.DebouncedUntil, } q.workspaceAgentVolumeResourceMonitors = append(q.workspaceAgentVolumeResourceMonitors, monitor) @@ -8879,20 +8870,6 @@ func (q *FakeQuerier) InsertWorkspaceModule(_ context.Context, arg database.Inse return workspaceModule, nil } -func (q *FakeQuerier) InsertWorkspaceMonitor(_ context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - err := validateDatabaseType(arg) - if err != nil { - return database.WorkspaceMonitor{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - workspaceMonitor := database.WorkspaceMonitor(arg) - q.workspaceMonitors = append(q.workspaceMonitors, workspaceMonitor) - return workspaceMonitor, nil -} - func (q *FakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { q.mutex.Lock() defer q.mutex.Unlock() @@ -9521,6 +9498,27 @@ func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMe return database.OrganizationMember{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateMemoryResourceMonitor(_ context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + for i, monitor := range q.workspaceAgentMemoryResourceMonitors { + if monitor.AgentID != arg.AgentID { + continue + } + + monitor.State = arg.State + monitor.UpdatedAt = arg.UpdatedAt + monitor.DebouncedUntil = arg.DebouncedUntil + q.workspaceAgentMemoryResourceMonitors[i] = monitor + return nil + } + + return nil +} + func (*FakeQuerier) UpdateNotificationTemplateMethodByID(_ context.Context, _ database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { // Not implementing this function because it relies on state in the database which is created with migrations. // We could consider using code-generation to align the database state and dbmem, but it's not worth it right now. @@ -10299,6 +10297,27 @@ func (q *FakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse return database.User{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateVolumeResourceMonitor(_ context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + err := validateDatabaseType(arg) + if err != nil { + return err + } + + for i, monitor := range q.workspaceAgentVolumeResourceMonitors { + if monitor.AgentID != arg.AgentID || monitor.Path != arg.Path { + continue + } + + monitor.State = arg.State + monitor.UpdatedAt = arg.UpdatedAt + monitor.DebouncedUntil = arg.DebouncedUntil + q.workspaceAgentVolumeResourceMonitors[i] = monitor + return nil + } + + return nil +} + func (q *FakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { if err := validateDatabaseType(arg); err != nil { return database.WorkspaceTable{}, err @@ -10651,32 +10670,6 @@ func (q *FakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database. return sql.ErrNoRows } -func (q *FakeQuerier) UpdateWorkspaceMonitor(_ context.Context, arg database.UpdateWorkspaceMonitorParams) error { - err := validateDatabaseType(arg) - if err != nil { - return err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for index, monitor := range q.workspaceMonitors { - if monitor.WorkspaceID != arg.WorkspaceID || - monitor.MonitorType != arg.MonitorType || - monitor.VolumePath != arg.VolumePath { - continue - } - - monitor.DebouncedUntil = arg.DebouncedUntil - monitor.UpdatedAt = arg.UpdatedAt - monitor.State = arg.State - q.workspaceMonitors[index] = monitor - return nil - } - - return sql.ErrNoRows -} - func (q *FakeQuerier) UpdateWorkspaceNextStartAt(_ context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 63476c952aef6..932b1b27a5de3 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1652,13 +1652,6 @@ func (m queryMetricsStore) GetWorkspaceModulesCreatedAfter(ctx context.Context, return r0, r1 } -func (m queryMetricsStore) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - start := time.Now() - r0, r1 := m.s.GetWorkspaceMonitor(ctx, arg) - m.queryLatencies.WithLabelValues("GetWorkspaceMonitor").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { start := time.Now() proxies, err := m.s.GetWorkspaceProxies(ctx) @@ -2121,13 +2114,6 @@ func (m queryMetricsStore) InsertWorkspaceModule(ctx context.Context, arg databa return r0, r1 } -func (m queryMetricsStore) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - start := time.Now() - r0, r1 := m.s.InsertWorkspaceMonitor(ctx, arg) - m.queryLatencies.WithLabelValues("InsertWorkspaceMonitor").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { start := time.Now() proxy, err := m.s.InsertWorkspaceProxy(ctx, arg) @@ -2310,6 +2296,13 @@ func (m queryMetricsStore) UpdateMemberRoles(ctx context.Context, arg database.U return member, err } +func (m queryMetricsStore) UpdateMemoryResourceMonitor(ctx context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + start := time.Now() + r0 := m.s.UpdateMemoryResourceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateMemoryResourceMonitor").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { start := time.Now() r0, r1 := m.s.UpdateNotificationTemplateMethodByID(ctx, arg) @@ -2548,6 +2541,13 @@ func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.Up return user, err } +func (m queryMetricsStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + start := time.Now() + r0 := m.s.UpdateVolumeResourceMonitor(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateVolumeResourceMonitor").Observe(time.Since(start).Seconds()) + return r0 +} + func (m queryMetricsStore) UpdateWorkspace(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { start := time.Now() workspace, err := m.s.UpdateWorkspace(ctx, arg) @@ -2653,13 +2653,6 @@ func (m queryMetricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg da return err } -func (m queryMetricsStore) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { - start := time.Now() - r0 := m.s.UpdateWorkspaceMonitor(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateWorkspaceMonitor").Observe(time.Since(start).Seconds()) - return r0 -} - func (m queryMetricsStore) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { start := time.Now() r0 := m.s.UpdateWorkspaceNextStartAt(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index c92e4f24230b8..3d31f3f399bd8 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3486,21 +3486,6 @@ func (mr *MockStoreMockRecorder) GetWorkspaceModulesCreatedAfter(ctx, createdAt return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceModulesCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceModulesCreatedAfter), ctx, createdAt) } -// GetWorkspaceMonitor mocks base method. -func (m *MockStore) GetWorkspaceMonitor(ctx context.Context, arg database.GetWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetWorkspaceMonitor", ctx, arg) - ret0, _ := ret[0].(database.WorkspaceMonitor) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetWorkspaceMonitor indicates an expected call of GetWorkspaceMonitor. -func (mr *MockStoreMockRecorder) GetWorkspaceMonitor(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).GetWorkspaceMonitor), ctx, arg) -} - // GetWorkspaceProxies mocks base method. func (m *MockStore) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) { m.ctrl.T.Helper() @@ -4492,21 +4477,6 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceModule(ctx, arg any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceModule", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceModule), ctx, arg) } -// InsertWorkspaceMonitor mocks base method. -func (m *MockStore) InsertWorkspaceMonitor(ctx context.Context, arg database.InsertWorkspaceMonitorParams) (database.WorkspaceMonitor, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertWorkspaceMonitor", ctx, arg) - ret0, _ := ret[0].(database.WorkspaceMonitor) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InsertWorkspaceMonitor indicates an expected call of InsertWorkspaceMonitor. -func (mr *MockStoreMockRecorder) InsertWorkspaceMonitor(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceMonitor), ctx, arg) -} - // InsertWorkspaceProxy mocks base method. func (m *MockStore) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) { m.ctrl.T.Helper() @@ -4920,6 +4890,20 @@ func (mr *MockStoreMockRecorder) UpdateMemberRoles(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemberRoles", reflect.TypeOf((*MockStore)(nil).UpdateMemberRoles), ctx, arg) } +// UpdateMemoryResourceMonitor mocks base method. +func (m *MockStore) UpdateMemoryResourceMonitor(ctx context.Context, arg database.UpdateMemoryResourceMonitorParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateMemoryResourceMonitor", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateMemoryResourceMonitor indicates an expected call of UpdateMemoryResourceMonitor. +func (mr *MockStoreMockRecorder) UpdateMemoryResourceMonitor(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMemoryResourceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateMemoryResourceMonitor), ctx, arg) +} + // UpdateNotificationTemplateMethodByID mocks base method. func (m *MockStore) UpdateNotificationTemplateMethodByID(ctx context.Context, arg database.UpdateNotificationTemplateMethodByIDParams) (database.NotificationTemplate, error) { m.ctrl.T.Helper() @@ -5411,6 +5395,20 @@ func (mr *MockStoreMockRecorder) UpdateUserStatus(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockStore)(nil).UpdateUserStatus), ctx, arg) } +// UpdateVolumeResourceMonitor mocks base method. +func (m *MockStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateVolumeResourceMonitor", ctx, arg) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateVolumeResourceMonitor indicates an expected call of UpdateVolumeResourceMonitor. +func (mr *MockStoreMockRecorder) UpdateVolumeResourceMonitor(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateVolumeResourceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateVolumeResourceMonitor), ctx, arg) +} + // UpdateWorkspace mocks base method. func (m *MockStore) UpdateWorkspace(ctx context.Context, arg database.UpdateWorkspaceParams) (database.WorkspaceTable, error) { m.ctrl.T.Helper() @@ -5623,20 +5621,6 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(ctx, arg any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), ctx, arg) } -// UpdateWorkspaceMonitor mocks base method. -func (m *MockStore) UpdateWorkspaceMonitor(ctx context.Context, arg database.UpdateWorkspaceMonitorParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateWorkspaceMonitor", ctx, arg) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateWorkspaceMonitor indicates an expected call of UpdateWorkspaceMonitor. -func (mr *MockStoreMockRecorder) UpdateWorkspaceMonitor(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceMonitor", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceMonitor), ctx, arg) -} - // UpdateWorkspaceNextStartAt mocks base method. func (m *MockStore) UpdateWorkspaceNextStartAt(ctx context.Context, arg database.UpdateWorkspaceNextStartAtParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index bbfb4b53087b3..b5cb3a280a210 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -238,6 +238,11 @@ CREATE TYPE workspace_agent_lifecycle_state AS ENUM ( 'off' ); +CREATE TYPE workspace_agent_monitor_state AS ENUM ( + 'OK', + 'NOK' +); + CREATE TYPE workspace_agent_script_timing_stage AS ENUM ( 'start', 'stop', @@ -275,16 +280,6 @@ CREATE TYPE workspace_app_open_in AS ENUM ( 'slim-window' ); -CREATE TYPE workspace_monitor_state AS ENUM ( - 'OK', - 'NOK' -); - -CREATE TYPE workspace_monitor_type AS ENUM ( - 'memory', - 'volume' -); - CREATE TYPE workspace_transition AS ENUM ( 'start', 'stop', @@ -1500,7 +1495,10 @@ CREATE TABLE workspace_agent_memory_resource_monitors ( agent_id uuid NOT NULL, enabled boolean NOT NULL, threshold integer NOT NULL, - created_at timestamp with time zone NOT NULL + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + state workspace_agent_monitor_state DEFAULT 'OK'::workspace_agent_monitor_state NOT NULL, + debounced_until timestamp with time zone NOT NULL ); CREATE UNLOGGED TABLE workspace_agent_metadata ( @@ -1585,7 +1583,10 @@ CREATE TABLE workspace_agent_volume_resource_monitors ( enabled boolean NOT NULL, threshold integer NOT NULL, path text NOT NULL, - created_at timestamp with time zone NOT NULL + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + state workspace_agent_monitor_state DEFAULT 'OK'::workspace_agent_monitor_state NOT NULL, + debounced_until timestamp with time zone NOT NULL ); CREATE TABLE workspace_agents ( @@ -1774,17 +1775,6 @@ CREATE TABLE workspace_modules ( created_at timestamp with time zone NOT NULL ); -CREATE TABLE workspace_monitors ( - workspace_id uuid NOT NULL, - monitor_type workspace_monitor_type NOT NULL, - volume_path text, - state workspace_monitor_state NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - debounced_until timestamp with time zone NOT NULL, - CONSTRAINT workspace_monitor_volume_path_exclusion CHECK (((volume_path IS NULL) OR (monitor_type = 'volume'::workspace_monitor_type))) -); - CREATE TABLE workspace_proxies ( id uuid NOT NULL, name text NOT NULL, diff --git a/coderd/database/migrations/000289_oom_and_ood_notification.down.sql b/coderd/database/migrations/000289_oom_and_ood_notification.down.sql deleted file mode 100644 index a7d54ccf6ec7a..0000000000000 --- a/coderd/database/migrations/000289_oom_and_ood_notification.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DELETE FROM notification_templates WHERE id = 'f047f6a3-5713-40f7-85aa-0394cce9fa3a'; -DELETE FROM notification_templates WHERE id = 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a'; diff --git a/coderd/database/migrations/000289_oom_and_ood_notification.up.sql b/coderd/database/migrations/000289_oom_and_ood_notification.up.sql deleted file mode 100644 index f0489606bb5b9..0000000000000 --- a/coderd/database/migrations/000289_oom_and_ood_notification.up.sql +++ /dev/null @@ -1,40 +0,0 @@ -INSERT INTO notification_templates - (id, name, title_template, body_template, "group", actions) -VALUES ( - 'a9d027b4-ac49-4fb1-9f6d-45af15f64e7a', - 'Workspace Out Of Memory', - E'Your workspace "{{.Labels.workspace}}" is low on memory', - E'Hi {{.UserName}},\n\n'|| - E'Your workspace **{{.Labels.workspace}}** has reached the memory usage threshold set at **{{.Labels.threshold}}**.', - 'Workspace Events', - '[ - { - "label": "View workspace", - "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" - } - ]'::jsonb -); - -INSERT INTO notification_templates - (id, name, title_template, body_template, "group", actions) -VALUES ( - 'f047f6a3-5713-40f7-85aa-0394cce9fa3a', - 'Workspace Out Of Disk', - E'Your workspace "{{.Labels.workspace}}" is low on volume space', - E'Hi {{.UserName}},\n\n'|| - E'{{ if eq (len .Data.volumes) 1 }}{{ $volume := index .Data.volumes 0 }}'|| - E'Volume **`{{$volume.path}}`** is over {{$volume.threshold}} full in workspace **{{.Labels.workspace}}**.'|| - E'{{ else }}'|| - E'The following volumes are nearly full in workspace **{{.Labels.workspace}}**\n\n'|| - E'{{ range $volume := .Data.volumes }}'|| - E'- **`{{$volume.path}}`** is over {{$volume.threshold}} full\n'|| - E'{{ end }}'|| - E'{{ end }}', - 'Workspace Events', - '[ - { - "label": "View workspace", - "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" - } - ]'::jsonb -); diff --git a/coderd/database/migrations/000290_create_workspace_monitors.down.sql b/coderd/database/migrations/000290_create_workspace_monitors.down.sql deleted file mode 100644 index 5aab6243dd407..0000000000000 --- a/coderd/database/migrations/000290_create_workspace_monitors.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP TABLE workspace_monitors; -DROP TYPE workspace_monitor_state; -DROP TYPE workspace_monitor_type; diff --git a/coderd/database/migrations/000290_create_workspace_monitors.up.sql b/coderd/database/migrations/000290_create_workspace_monitors.up.sql deleted file mode 100644 index a67d3bc2b50e0..0000000000000 --- a/coderd/database/migrations/000290_create_workspace_monitors.up.sql +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TYPE workspace_monitor_state AS ENUM ( - 'OK', - 'NOK' -); - -CREATE TYPE workspace_monitor_type AS ENUM ( - 'memory', - 'volume' -); - -CREATE TABLE workspace_monitors ( - workspace_id uuid NOT NULL, - monitor_type workspace_monitor_type NOT NULL, - volume_path text, - state workspace_monitor_state NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - debounced_until timestamp with time zone NOT NULL -); - -ALTER TABLE workspace_monitors -ADD CONSTRAINT workspace_monitor_volume_path_exclusion CHECK ( - volume_path IS NULL OR monitor_type = 'volume' -); diff --git a/coderd/database/migrations/000291_create_workspace_monitors.down.sql b/coderd/database/migrations/000291_create_workspace_monitors.down.sql new file mode 100644 index 0000000000000..c3c6ce7c614ac --- /dev/null +++ b/coderd/database/migrations/000291_create_workspace_monitors.down.sql @@ -0,0 +1,11 @@ +ALTER TABLE workspace_agent_volume_resource_monitors + DROP COLUMN updated_at, + DROP COLUMN state, + DROP COLUMN debounced_until; + +ALTER TABLE workspace_agent_memory_resource_monitors + DROP COLUMN updated_at, + DROP COLUMN state, + DROP COLUMN debounced_until; + +DROP TYPE workspace_agent_monitor_state; diff --git a/coderd/database/migrations/000291_create_workspace_monitors.up.sql b/coderd/database/migrations/000291_create_workspace_monitors.up.sql new file mode 100644 index 0000000000000..a6b1f7609d7da --- /dev/null +++ b/coderd/database/migrations/000291_create_workspace_monitors.up.sql @@ -0,0 +1,14 @@ +CREATE TYPE workspace_agent_monitor_state AS ENUM ( + 'OK', + 'NOK' +); + +ALTER TABLE workspace_agent_memory_resource_monitors + ADD COLUMN updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + ADD COLUMN state workspace_agent_monitor_state NOT NULL DEFAULT 'OK', + ADD COLUMN debounced_until timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00'::timestamptz; + +ALTER TABLE workspace_agent_volume_resource_monitors + ADD COLUMN updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + ADD COLUMN state workspace_agent_monitor_state NOT NULL DEFAULT 'OK', + ADD COLUMN debounced_until timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00'::timestamptz; diff --git a/coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql b/coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql deleted file mode 100644 index 05ff05e2b0343..0000000000000 --- a/coderd/database/migrations/testdata/fixtures/000289_create_workspace_monitors.up.sql +++ /dev/null @@ -1,15 +0,0 @@ -INSERT INTO workspace_monitors ( - workspace_id, - monitor_type, - state, - created_at, - updated_at, - debounced_until -) VALUES ( - (SELECT id FROM workspaces WHERE deleted = FALSE LIMIT 1), - 'memory', - 'OK', - NOW(), - NOW(), - NOW() -); diff --git a/coderd/database/models.go b/coderd/database/models.go index 2d3f5c899dea3..725b7a283128a 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1958,6 +1958,64 @@ func AllWorkspaceAgentLifecycleStateValues() []WorkspaceAgentLifecycleState { } } +type WorkspaceAgentMonitorState string + +const ( + WorkspaceAgentMonitorStateOK WorkspaceAgentMonitorState = "OK" + WorkspaceAgentMonitorStateNOK WorkspaceAgentMonitorState = "NOK" +) + +func (e *WorkspaceAgentMonitorState) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = WorkspaceAgentMonitorState(s) + case string: + *e = WorkspaceAgentMonitorState(s) + default: + return fmt.Errorf("unsupported scan type for WorkspaceAgentMonitorState: %T", src) + } + return nil +} + +type NullWorkspaceAgentMonitorState struct { + WorkspaceAgentMonitorState WorkspaceAgentMonitorState `json:"workspace_agent_monitor_state"` + Valid bool `json:"valid"` // Valid is true if WorkspaceAgentMonitorState is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullWorkspaceAgentMonitorState) Scan(value interface{}) error { + if value == nil { + ns.WorkspaceAgentMonitorState, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.WorkspaceAgentMonitorState.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullWorkspaceAgentMonitorState) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.WorkspaceAgentMonitorState), nil +} + +func (e WorkspaceAgentMonitorState) Valid() bool { + switch e { + case WorkspaceAgentMonitorStateOK, + WorkspaceAgentMonitorStateNOK: + return true + } + return false +} + +func AllWorkspaceAgentMonitorStateValues() []WorkspaceAgentMonitorState { + return []WorkspaceAgentMonitorState{ + WorkspaceAgentMonitorStateOK, + WorkspaceAgentMonitorStateNOK, + } +} + // What stage the script was ran in. type WorkspaceAgentScriptTimingStage string @@ -2274,122 +2332,6 @@ func AllWorkspaceAppOpenInValues() []WorkspaceAppOpenIn { } } -type WorkspaceMonitorState string - -const ( - WorkspaceMonitorStateOK WorkspaceMonitorState = "OK" - WorkspaceMonitorStateNOK WorkspaceMonitorState = "NOK" -) - -func (e *WorkspaceMonitorState) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = WorkspaceMonitorState(s) - case string: - *e = WorkspaceMonitorState(s) - default: - return fmt.Errorf("unsupported scan type for WorkspaceMonitorState: %T", src) - } - return nil -} - -type NullWorkspaceMonitorState struct { - WorkspaceMonitorState WorkspaceMonitorState `json:"workspace_monitor_state"` - Valid bool `json:"valid"` // Valid is true if WorkspaceMonitorState is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullWorkspaceMonitorState) Scan(value interface{}) error { - if value == nil { - ns.WorkspaceMonitorState, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.WorkspaceMonitorState.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullWorkspaceMonitorState) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.WorkspaceMonitorState), nil -} - -func (e WorkspaceMonitorState) Valid() bool { - switch e { - case WorkspaceMonitorStateOK, - WorkspaceMonitorStateNOK: - return true - } - return false -} - -func AllWorkspaceMonitorStateValues() []WorkspaceMonitorState { - return []WorkspaceMonitorState{ - WorkspaceMonitorStateOK, - WorkspaceMonitorStateNOK, - } -} - -type WorkspaceMonitorType string - -const ( - WorkspaceMonitorTypeMemory WorkspaceMonitorType = "memory" - WorkspaceMonitorTypeVolume WorkspaceMonitorType = "volume" -) - -func (e *WorkspaceMonitorType) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = WorkspaceMonitorType(s) - case string: - *e = WorkspaceMonitorType(s) - default: - return fmt.Errorf("unsupported scan type for WorkspaceMonitorType: %T", src) - } - return nil -} - -type NullWorkspaceMonitorType struct { - WorkspaceMonitorType WorkspaceMonitorType `json:"workspace_monitor_type"` - Valid bool `json:"valid"` // Valid is true if WorkspaceMonitorType is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullWorkspaceMonitorType) Scan(value interface{}) error { - if value == nil { - ns.WorkspaceMonitorType, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.WorkspaceMonitorType.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullWorkspaceMonitorType) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.WorkspaceMonitorType), nil -} - -func (e WorkspaceMonitorType) Valid() bool { - switch e { - case WorkspaceMonitorTypeMemory, - WorkspaceMonitorTypeVolume: - return true - } - return false -} - -func AllWorkspaceMonitorTypeValues() []WorkspaceMonitorType { - return []WorkspaceMonitorType{ - WorkspaceMonitorTypeMemory, - WorkspaceMonitorTypeVolume, - } -} - type WorkspaceTransition string const ( @@ -3269,10 +3211,13 @@ type WorkspaceAgentLogSource struct { } type WorkspaceAgentMemoryResourceMonitor struct { - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Enabled bool `db:"enabled" json:"enabled"` - Threshold int32 `db:"threshold" json:"threshold"` - CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Enabled bool `db:"enabled" json:"enabled"` + Threshold int32 `db:"threshold" json:"threshold"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` } type WorkspaceAgentMetadatum struct { @@ -3343,11 +3288,14 @@ type WorkspaceAgentStat struct { } type WorkspaceAgentVolumeResourceMonitor struct { - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Enabled bool `db:"enabled" json:"enabled"` - Threshold int32 `db:"threshold" json:"threshold"` - Path string `db:"path" json:"path"` - CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Enabled bool `db:"enabled" json:"enabled"` + Threshold int32 `db:"threshold" json:"threshold"` + Path string `db:"path" json:"path"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` } type WorkspaceApp struct { @@ -3452,16 +3400,6 @@ type WorkspaceModule struct { CreatedAt time.Time `db:"created_at" json:"created_at"` } -type WorkspaceMonitor struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` - VolumePath sql.NullString `db:"volume_path" json:"volume_path"` - State WorkspaceMonitorState `db:"state" json:"state"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` -} - type WorkspaceProxy struct { ID uuid.UUID `db:"id" json:"id"` Name string `db:"name" json:"name"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 66d6bf2178107..1c78833ed53d9 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -350,7 +350,6 @@ type sqlcQuerier interface { GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceModule, error) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceModule, error) - GetWorkspaceMonitor(ctx context.Context, arg GetWorkspaceMonitorParams) (WorkspaceMonitor, error) GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) // Finds a workspace proxy that has an access URL or app hostname that matches // the provided hostname. This is to check if a hostname matches any workspace @@ -437,7 +436,6 @@ type sqlcQuerier interface { InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error) - InsertWorkspaceMonitor(ctx context.Context, arg InsertWorkspaceMonitorParams) (WorkspaceMonitor, error) InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error) @@ -475,6 +473,7 @@ type sqlcQuerier interface { UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) + UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error) UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) @@ -509,6 +508,7 @@ type sqlcQuerier interface { UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) + UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error UpdateWorkspaceAgentLifecycleStateByID(ctx context.Context, arg UpdateWorkspaceAgentLifecycleStateByIDParams) error @@ -524,7 +524,6 @@ type sqlcQuerier interface { UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg UpdateWorkspaceDormantDeletingAtParams) (WorkspaceTable, error) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error - UpdateWorkspaceMonitor(ctx context.Context, arg UpdateWorkspaceMonitorParams) error UpdateWorkspaceNextStartAt(ctx context.Context, arg UpdateWorkspaceNextStartAtParams) error // This allows editing the properties of a workspace proxy. UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b88cde9e7ce71..de94723bfbbbc 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11767,7 +11767,7 @@ func (q *sqlQuerier) UpsertWorkspaceAgentPortShare(ctx context.Context, arg Upse const fetchMemoryResourceMonitorsByAgentID = `-- name: FetchMemoryResourceMonitorsByAgentID :one SELECT - agent_id, enabled, threshold, created_at + agent_id, enabled, threshold, created_at, updated_at, state, debounced_until FROM workspace_agent_memory_resource_monitors WHERE @@ -11782,13 +11782,16 @@ func (q *sqlQuerier) FetchMemoryResourceMonitorsByAgentID(ctx context.Context, a &i.Enabled, &i.Threshold, &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, ) return i, err } const fetchVolumesResourceMonitorsByAgentID = `-- name: FetchVolumesResourceMonitorsByAgentID :many SELECT - agent_id, enabled, threshold, path, created_at + agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until FROM workspace_agent_volume_resource_monitors WHERE @@ -11810,6 +11813,9 @@ func (q *sqlQuerier) FetchVolumesResourceMonitorsByAgentID(ctx context.Context, &i.Threshold, &i.Path, &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, ); err != nil { return nil, err } @@ -11829,26 +11835,35 @@ INSERT INTO workspace_agent_memory_resource_monitors ( agent_id, enabled, + state, threshold, - created_at + created_at, + updated_at, + debounced_until ) VALUES - ($1, $2, $3, $4) RETURNING agent_id, enabled, threshold, created_at + ($1, $2, $3, $4, $5, $6, $7) RETURNING agent_id, enabled, threshold, created_at, updated_at, state, debounced_until ` type InsertMemoryResourceMonitorParams struct { - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Enabled bool `db:"enabled" json:"enabled"` - Threshold int32 `db:"threshold" json:"threshold"` - CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Enabled bool `db:"enabled" json:"enabled"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + Threshold int32 `db:"threshold" json:"threshold"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` } func (q *sqlQuerier) InsertMemoryResourceMonitor(ctx context.Context, arg InsertMemoryResourceMonitorParams) (WorkspaceAgentMemoryResourceMonitor, error) { row := q.db.QueryRowContext(ctx, insertMemoryResourceMonitor, arg.AgentID, arg.Enabled, + arg.State, arg.Threshold, arg.CreatedAt, + arg.UpdatedAt, + arg.DebouncedUntil, ) var i WorkspaceAgentMemoryResourceMonitor err := row.Scan( @@ -11856,6 +11871,9 @@ func (q *sqlQuerier) InsertMemoryResourceMonitor(ctx context.Context, arg Insert &i.Enabled, &i.Threshold, &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, ) return i, err } @@ -11866,19 +11884,25 @@ INSERT INTO agent_id, path, enabled, + state, threshold, - created_at + created_at, + updated_at, + debounced_until ) VALUES - ($1, $2, $3, $4, $5) RETURNING agent_id, enabled, threshold, path, created_at + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING agent_id, enabled, threshold, path, created_at, updated_at, state, debounced_until ` type InsertVolumeResourceMonitorParams struct { - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - Path string `db:"path" json:"path"` - Enabled bool `db:"enabled" json:"enabled"` - Threshold int32 `db:"threshold" json:"threshold"` - CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Path string `db:"path" json:"path"` + Enabled bool `db:"enabled" json:"enabled"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + Threshold int32 `db:"threshold" json:"threshold"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` } func (q *sqlQuerier) InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error) { @@ -11886,8 +11910,11 @@ func (q *sqlQuerier) InsertVolumeResourceMonitor(ctx context.Context, arg Insert arg.AgentID, arg.Path, arg.Enabled, + arg.State, arg.Threshold, arg.CreatedAt, + arg.UpdatedAt, + arg.DebouncedUntil, ) var i WorkspaceAgentVolumeResourceMonitor err := row.Scan( @@ -11896,10 +11923,69 @@ func (q *sqlQuerier) InsertVolumeResourceMonitor(ctx context.Context, arg Insert &i.Threshold, &i.Path, &i.CreatedAt, + &i.UpdatedAt, + &i.State, + &i.DebouncedUntil, ) return i, err } +const updateMemoryResourceMonitor = `-- name: UpdateMemoryResourceMonitor :exec +UPDATE workspace_agent_memory_resource_monitors +SET + updated_at = $2, + state = $3, + debounced_until = $4 +WHERE + agent_id = $1 +` + +type UpdateMemoryResourceMonitorParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error { + _, err := q.db.ExecContext(ctx, updateMemoryResourceMonitor, + arg.AgentID, + arg.UpdatedAt, + arg.State, + arg.DebouncedUntil, + ) + return err +} + +const updateVolumeResourceMonitor = `-- name: UpdateVolumeResourceMonitor :exec +UPDATE workspace_agent_volume_resource_monitors +SET + updated_at = $3, + state = $4, + debounced_until = $5 +WHERE + agent_id = $1 AND path = $2 +` + +type UpdateVolumeResourceMonitorParams struct { + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + Path string `db:"path" json:"path"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + State WorkspaceAgentMonitorState `db:"state" json:"state"` + DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` +} + +func (q *sqlQuerier) UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error { + _, err := q.db.ExecContext(ctx, updateVolumeResourceMonitor, + arg.AgentID, + arg.Path, + arg.UpdatedAt, + arg.State, + arg.DebouncedUntil, + ) + return err +} + const deleteOldWorkspaceAgentLogs = `-- name: DeleteOldWorkspaceAgentLogs :exec WITH latest_builds AS ( @@ -15102,122 +15188,6 @@ func (q *sqlQuerier) InsertWorkspaceModule(ctx context.Context, arg InsertWorksp return i, err } -const getWorkspaceMonitor = `-- name: GetWorkspaceMonitor :one -SELECT workspace_id, monitor_type, volume_path, state, created_at, updated_at, debounced_until -FROM workspace_monitors -WHERE - workspace_id = $1 AND - monitor_type = $2 AND - volume_path IS NOT DISTINCT FROM $3 -` - -type GetWorkspaceMonitorParams struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` - VolumePath sql.NullString `db:"volume_path" json:"volume_path"` -} - -func (q *sqlQuerier) GetWorkspaceMonitor(ctx context.Context, arg GetWorkspaceMonitorParams) (WorkspaceMonitor, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceMonitor, arg.WorkspaceID, arg.MonitorType, arg.VolumePath) - var i WorkspaceMonitor - err := row.Scan( - &i.WorkspaceID, - &i.MonitorType, - &i.VolumePath, - &i.State, - &i.CreatedAt, - &i.UpdatedAt, - &i.DebouncedUntil, - ) - return i, err -} - -const insertWorkspaceMonitor = `-- name: InsertWorkspaceMonitor :one -INSERT INTO workspace_monitors ( - workspace_id, - monitor_type, - volume_path, - state, - created_at, - updated_at, - debounced_until -) VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7 -) RETURNING workspace_id, monitor_type, volume_path, state, created_at, updated_at, debounced_until -` - -type InsertWorkspaceMonitorParams struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` - VolumePath sql.NullString `db:"volume_path" json:"volume_path"` - State WorkspaceMonitorState `db:"state" json:"state"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` -} - -func (q *sqlQuerier) InsertWorkspaceMonitor(ctx context.Context, arg InsertWorkspaceMonitorParams) (WorkspaceMonitor, error) { - row := q.db.QueryRowContext(ctx, insertWorkspaceMonitor, - arg.WorkspaceID, - arg.MonitorType, - arg.VolumePath, - arg.State, - arg.CreatedAt, - arg.UpdatedAt, - arg.DebouncedUntil, - ) - var i WorkspaceMonitor - err := row.Scan( - &i.WorkspaceID, - &i.MonitorType, - &i.VolumePath, - &i.State, - &i.CreatedAt, - &i.UpdatedAt, - &i.DebouncedUntil, - ) - return i, err -} - -const updateWorkspaceMonitor = `-- name: UpdateWorkspaceMonitor :exec -UPDATE workspace_monitors -SET - state = $4, - updated_at = $5, - debounced_until = $6 -WHERE - workspace_id = $1 AND - monitor_type = $2 AND - volume_path IS NOT DISTINCT FROM $3 -` - -type UpdateWorkspaceMonitorParams struct { - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - MonitorType WorkspaceMonitorType `db:"monitor_type" json:"monitor_type"` - VolumePath sql.NullString `db:"volume_path" json:"volume_path"` - State WorkspaceMonitorState `db:"state" json:"state"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - DebouncedUntil time.Time `db:"debounced_until" json:"debounced_until"` -} - -func (q *sqlQuerier) UpdateWorkspaceMonitor(ctx context.Context, arg UpdateWorkspaceMonitorParams) error { - _, err := q.db.ExecContext(ctx, updateWorkspaceMonitor, - arg.WorkspaceID, - arg.MonitorType, - arg.VolumePath, - arg.State, - arg.UpdatedAt, - arg.DebouncedUntil, - ) - return err -} - const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path diff --git a/coderd/database/queries/workspaceagentresourcemonitors.sql b/coderd/database/queries/workspaceagentresourcemonitors.sql index e70ef85f3cbd5..84ee5c67b37ef 100644 --- a/coderd/database/queries/workspaceagentresourcemonitors.sql +++ b/coderd/database/queries/workspaceagentresourcemonitors.sql @@ -19,11 +19,14 @@ INSERT INTO workspace_agent_memory_resource_monitors ( agent_id, enabled, + state, threshold, - created_at + created_at, + updated_at, + debounced_until ) VALUES - ($1, $2, $3, $4) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: InsertVolumeResourceMonitor :one INSERT INTO @@ -31,8 +34,29 @@ INSERT INTO agent_id, path, enabled, + state, threshold, - created_at + created_at, + updated_at, + debounced_until ) VALUES - ($1, $2, $3, $4, $5) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; + +-- name: UpdateMemoryResourceMonitor :exec +UPDATE workspace_agent_memory_resource_monitors +SET + updated_at = $2, + state = $3, + debounced_until = $4 +WHERE + agent_id = $1; + +-- name: UpdateVolumeResourceMonitor :exec +UPDATE workspace_agent_volume_resource_monitors +SET + updated_at = $3, + state = $4, + debounced_until = $5 +WHERE + agent_id = $1 AND path = $2; diff --git a/coderd/database/queries/workspacemonitors.sql b/coderd/database/queries/workspacemonitors.sql deleted file mode 100644 index eab61c5b5d387..0000000000000 --- a/coderd/database/queries/workspacemonitors.sql +++ /dev/null @@ -1,37 +0,0 @@ --- name: GetWorkspaceMonitor :one -SELECT * -FROM workspace_monitors -WHERE - workspace_id = $1 AND - monitor_type = $2 AND - volume_path IS NOT DISTINCT FROM $3; - --- name: InsertWorkspaceMonitor :one -INSERT INTO workspace_monitors ( - workspace_id, - monitor_type, - volume_path, - state, - created_at, - updated_at, - debounced_until -) VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7 -) RETURNING *; - --- name: UpdateWorkspaceMonitor :exec -UPDATE workspace_monitors -SET - state = $4, - updated_at = $5, - debounced_until = $6 -WHERE - workspace_id = $1 AND - monitor_type = $2 AND - volume_path IS NOT DISTINCT FROM $3; diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index ee00c06e530cd..d9fef0cacb512 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1930,10 +1930,13 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. if prAgent.ResourcesMonitoring != nil { if prAgent.ResourcesMonitoring.Memory != nil { _, err = db.InsertMemoryResourceMonitor(ctx, database.InsertMemoryResourceMonitorParams{ - AgentID: agentID, - Enabled: prAgent.ResourcesMonitoring.Memory.Enabled, - Threshold: prAgent.ResourcesMonitoring.Memory.Threshold, - CreatedAt: dbtime.Now(), + AgentID: agentID, + Enabled: prAgent.ResourcesMonitoring.Memory.Enabled, + Threshold: prAgent.ResourcesMonitoring.Memory.Threshold, + State: database.WorkspaceAgentMonitorStateOK, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + DebouncedUntil: time.Time{}, }) if err != nil { return xerrors.Errorf("failed to insert agent memory resource monitor into db: %w", err) @@ -1941,11 +1944,14 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } for _, volume := range prAgent.ResourcesMonitoring.Volumes { _, err = db.InsertVolumeResourceMonitor(ctx, database.InsertVolumeResourceMonitorParams{ - AgentID: agentID, - Path: volume.Path, - Enabled: volume.Enabled, - Threshold: volume.Threshold, - CreatedAt: dbtime.Now(), + AgentID: agentID, + Path: volume.Path, + Enabled: volume.Enabled, + Threshold: volume.Threshold, + State: database.WorkspaceAgentMonitorStateOK, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + DebouncedUntil: time.Time{}, }) if err != nil { return xerrors.Errorf("failed to insert agent volume resource monitor into db: %w", err) From 69c4f424dd2605a61f777f1bcd3b8a7002086c80 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 4 Feb 2025 23:05:55 +0000 Subject: [PATCH 16/37] chore: run 'make gen' --- coderd/database/dump.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index b5cb3a280a210..6dba1df5050bb 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1496,9 +1496,9 @@ CREATE TABLE workspace_agent_memory_resource_monitors ( enabled boolean NOT NULL, threshold integer NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, state workspace_agent_monitor_state DEFAULT 'OK'::workspace_agent_monitor_state NOT NULL, - debounced_until timestamp with time zone NOT NULL + debounced_until timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL ); CREATE UNLOGGED TABLE workspace_agent_metadata ( @@ -1584,9 +1584,9 @@ CREATE TABLE workspace_agent_volume_resource_monitors ( threshold integer NOT NULL, path text NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, state workspace_agent_monitor_state DEFAULT 'OK'::workspace_agent_monitor_state NOT NULL, - debounced_until timestamp with time zone NOT NULL + debounced_until timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL ); CREATE TABLE workspace_agents ( From 44ebf659f1bc2d1800b8b9491362388957ae39ec Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 5 Feb 2025 09:11:08 +0000 Subject: [PATCH 17/37] chore: run 'make fmt' --- coderd/agentapi/workspacemonitor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/agentapi/workspacemonitor.go b/coderd/agentapi/workspacemonitor.go index 2b4323b5bac91..ec24966d56edc 100644 --- a/coderd/agentapi/workspacemonitor.go +++ b/coderd/agentapi/workspacemonitor.go @@ -83,7 +83,7 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a newState == database.WorkspaceAgentMonitorStateNOK && m.Clock.Now().After(monitor.DebouncedUntil) - var debouncedUntil = monitor.DebouncedUntil + debouncedUntil := monitor.DebouncedUntil if shouldNotify { debouncedUntil = m.Clock.Now().Add(m.Debounce) } @@ -167,7 +167,7 @@ func (m *WorkspaceMonitorAPI) monitorVolume( newState == database.WorkspaceAgentMonitorStateNOK && m.Clock.Now().After(monitor.DebouncedUntil) - var debouncedUntil = monitor.DebouncedUntil + debouncedUntil := monitor.DebouncedUntil if shouldNotify { debouncedUntil = m.Clock.Now().Add(m.Debounce) } From 714e7431f31ebcd23f07c31cea43bbddabe5fe5c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 5 Feb 2025 09:16:07 +0000 Subject: [PATCH 18/37] chore: remove cruft --- .../TemplateWorkspaceOutOfDisk#01.html.golden | 91 ------------------- ...kspaceReachedResourceThreshold.html.golden | 79 ---------------- .../TemplateWorkspaceOutOfDisk#01.json.golden | 42 --------- ...kspaceReachedResourceThreshold.json.golden | 29 ------ 4 files changed, 241 deletions(-) delete mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden delete mode 100644 coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden delete mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden delete mode 100644 coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden deleted file mode 100644 index 87e5dec07cdaf..0000000000000 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceOutOfDisk#01.html.golden +++ /dev/null @@ -1,91 +0,0 @@ -From: system@coder.com -To: bobby@coder.com -Subject: Your workspace "bobby-workspace" is low on volume space -Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 -Date: Fri, 11 Oct 2024 09:03:06 +0000 -Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 -MIME-Version: 1.0 - ---bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 -Content-Transfer-Encoding: quoted-printable -Content-Type: text/plain; charset=UTF-8 - -Hi Bobby, - -The following volumes are nearly full in workspace bobby-workspace - -/home/coder is over 90% full -/dev/coder is over 80% full -/etc/coder is over 95% full - - -View workspace: http://test.com/@bobby/bobby-workspace - ---bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 -Content-Transfer-Encoding: quoted-printable -Content-Type: text/html; charset=UTF-8 - - - - - - - Codestin Search App - - -
-
- 3D"Cod= -
-

- Your workspace "bobby-workspace" is low on volume space -

-
-

Hi Bobby,

- -

The following volumes are nearly full in workspace bobby-workspa= -ce

- -
    -
  • /home/coder is over 90% full
    -
  • -
  • /dev/coder is over 80% full
    -
  • -
  • /etc/coder is over 95% full
    -
  • -
-
-
- =20 - - View workspace - - =20 -
- -
- - - ---bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden deleted file mode 100644 index 8e42cf6729b7e..0000000000000 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceReachedResourceThreshold.html.golden +++ /dev/null @@ -1,79 +0,0 @@ -From: system@coder.com -To: bobby@coder.com -Subject: Workspace "bobby-workspace" reached resource threshold -Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48 -Date: Fri, 11 Oct 2024 09:03:06 +0000 -Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 -MIME-Version: 1.0 - ---bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 -Content-Transfer-Encoding: quoted-printable -Content-Type: text/plain; charset=UTF-8 - -Hi Bobby, - -Your workspace bobby-workspace has reached the memory usage threshold set a= -t 90%. - - -View workspace: http://test.com/@bobby/bobby-workspace - ---bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 -Content-Transfer-Encoding: quoted-printable -Content-Type: text/html; charset=UTF-8 - - - - - - - Codestin Search App - - -
-
- 3D"Cod= -
-

- Workspace "bobby-workspace" reached resource threshold -

-
-

Hi Bobby,

- -

Your workspace bobby-workspace has reached the memory u= -sage threshold set at 90%.

-
-
- =20 - - View workspace - - =20 -
- -
- - - ---bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden deleted file mode 100644 index c876fb1754dd1..0000000000000 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceOutOfDisk#01.json.golden +++ /dev/null @@ -1,42 +0,0 @@ -{ - "_version": "1.1", - "msg_id": "00000000-0000-0000-0000-000000000000", - "payload": { - "_version": "1.1", - "notification_name": "Workspace Out Of Disk", - "notification_template_id": "00000000-0000-0000-0000-000000000000", - "user_id": "00000000-0000-0000-0000-000000000000", - "user_email": "bobby@coder.com", - "user_name": "Bobby", - "user_username": "bobby", - "actions": [ - { - "label": "View workspace", - "url": "http://test.com/@bobby/bobby-workspace" - } - ], - "labels": { - "workspace": "bobby-workspace" - }, - "data": { - "volumes": [ - { - "path": "/home/coder", - "threshold": "90%" - }, - { - "path": "/dev/coder", - "threshold": "80%" - }, - { - "path": "/etc/coder", - "threshold": "95%" - } - ] - } - }, - "title": "Your workspace \"bobby-workspace\" is low on volume space", - "title_markdown": "Your workspace \"bobby-workspace\" is low on volume space", - "body": "Hi Bobby,\n\nThe following volumes are nearly full in workspace bobby-workspace\n\n/home/coder is over 90% full\n/dev/coder is over 80% full\n/etc/coder is over 95% full", - "body_markdown": "Hi Bobby,\n\nThe following volumes are nearly full in workspace **bobby-workspace**\n\n- **`/home/coder`** is over 90% full\n- **`/dev/coder`** is over 80% full\n- **`/etc/coder`** is over 95% full\n" -} \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden deleted file mode 100644 index 4c5c540343ba0..0000000000000 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceReachedResourceThreshold.json.golden +++ /dev/null @@ -1,29 +0,0 @@ -{ - "_version": "1.1", - "msg_id": "00000000-0000-0000-0000-000000000000", - "payload": { - "_version": "1.1", - "notification_name": "Workspace Reached Resource Threshold", - "notification_template_id": "00000000-0000-0000-0000-000000000000", - "user_id": "00000000-0000-0000-0000-000000000000", - "user_email": "bobby@coder.com", - "user_name": "Bobby", - "user_username": "bobby", - "actions": [ - { - "label": "View workspace", - "url": "http://test.com/@bobby/bobby-workspace" - } - ], - "labels": { - "threshold": "90%", - "threshold_type": "memory usage", - "workspace": "bobby-workspace" - }, - "data": null - }, - "title": "Workspace \"bobby-workspace\" reached resource threshold", - "title_markdown": "Workspace \"bobby-workspace\" reached resource threshold", - "body": "Hi Bobby,\n\nYour workspace bobby-workspace has reached the memory usage threshold set at 90%.", - "body_markdown": "Hi Bobby,\n\nYour workspace **bobby-workspace** has reached the memory usage threshold set at **90%**." -} \ No newline at end of file From 7cf5212082af96754547aa32387cd2de740f4d87 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 5 Feb 2025 22:32:36 +0000 Subject: [PATCH 19/37] chore: align interface --- agent/agenttest/client.go | 4 +- agent/proto/agent.pb.go | 387 +++++++++--------- agent/proto/agent.proto | 14 +- agent/proto/agent_drpc.pb.go | 28 +- coderd/agentapi/api.go | 4 +- ...pacemonitor.go => resources_monitoring.go} | 110 +++-- ...r_test.go => resources_monitoring_test.go} | 158 +++---- 7 files changed, 356 insertions(+), 349 deletions(-) rename coderd/agentapi/{workspacemonitor.go => resources_monitoring.go} (66%) rename coderd/agentapi/{workspacemonitor_test.go => resources_monitoring_test.go} (73%) diff --git a/agent/agenttest/client.go b/agent/agenttest/client.go index 39b52184f978f..6c80f48d4f77e 100644 --- a/agent/agenttest/client.go +++ b/agent/agenttest/client.go @@ -315,8 +315,8 @@ func (f *FakeAgentAPI) ScriptCompleted(_ context.Context, req *agentproto.Worksp return &agentproto.WorkspaceAgentScriptCompletedResponse{}, nil } -func (*FakeAgentAPI) UpdateWorkspaceMonitor(_ context.Context, _ *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { - return &agentproto.WorkspaceMonitorUpdateResponse{}, nil +func (*FakeAgentAPI) PushResourcesMonitoringUsage(_ context.Context, _ *agentproto.PushResourcesMonitoringUsageRequest) (*agentproto.PushResourcesMonitoringUsageResponse, error) { + return &agentproto.PushResourcesMonitoringUsageResponse{}, nil } func NewFakeAgentAPI(t testing.TB, logger slog.Logger, manifest *agentproto.Manifest, statsCh chan *agentproto.Stats) *FakeAgentAPI { diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 6fef3423ec4c2..7cc46f89ff7c5 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -2304,16 +2304,16 @@ func (x *Timing) GetStatus() Timing_Status { return Timing_OK } -type WorkspaceMonitorUpdateRequest struct { +type PushResourcesMonitoringUsageRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Datapoints []*WorkspaceMonitorUpdateRequest_Datapoint `protobuf:"bytes,1,rep,name=datapoints,proto3" json:"datapoints,omitempty"` + Datapoints []*PushResourcesMonitoringUsageRequest_Datapoint `protobuf:"bytes,1,rep,name=datapoints,proto3" json:"datapoints,omitempty"` } -func (x *WorkspaceMonitorUpdateRequest) Reset() { - *x = WorkspaceMonitorUpdateRequest{} +func (x *PushResourcesMonitoringUsageRequest) Reset() { + *x = PushResourcesMonitoringUsageRequest{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2321,13 +2321,13 @@ func (x *WorkspaceMonitorUpdateRequest) Reset() { } } -func (x *WorkspaceMonitorUpdateRequest) String() string { +func (x *PushResourcesMonitoringUsageRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceMonitorUpdateRequest) ProtoMessage() {} +func (*PushResourcesMonitoringUsageRequest) ProtoMessage() {} -func (x *WorkspaceMonitorUpdateRequest) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageRequest) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2339,26 +2339,26 @@ func (x *WorkspaceMonitorUpdateRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WorkspaceMonitorUpdateRequest.ProtoReflect.Descriptor instead. -func (*WorkspaceMonitorUpdateRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use PushResourcesMonitoringUsageRequest.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{28} } -func (x *WorkspaceMonitorUpdateRequest) GetDatapoints() []*WorkspaceMonitorUpdateRequest_Datapoint { +func (x *PushResourcesMonitoringUsageRequest) GetDatapoints() []*PushResourcesMonitoringUsageRequest_Datapoint { if x != nil { return x.Datapoints } return nil } -type WorkspaceMonitorUpdateResponse struct { +type PushResourcesMonitoringUsageResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } -func (x *WorkspaceMonitorUpdateResponse) Reset() { - *x = WorkspaceMonitorUpdateResponse{} +func (x *PushResourcesMonitoringUsageResponse) Reset() { + *x = PushResourcesMonitoringUsageResponse{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2366,13 +2366,13 @@ func (x *WorkspaceMonitorUpdateResponse) Reset() { } } -func (x *WorkspaceMonitorUpdateResponse) String() string { +func (x *PushResourcesMonitoringUsageResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceMonitorUpdateResponse) ProtoMessage() {} +func (*PushResourcesMonitoringUsageResponse) ProtoMessage() {} -func (x *WorkspaceMonitorUpdateResponse) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageResponse) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2384,8 +2384,8 @@ func (x *WorkspaceMonitorUpdateResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use WorkspaceMonitorUpdateResponse.ProtoReflect.Descriptor instead. -func (*WorkspaceMonitorUpdateResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use PushResourcesMonitoringUsageResponse.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageResponse) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{29} } @@ -2783,18 +2783,18 @@ func (x *BatchUpdateAppHealthRequest_HealthUpdate) GetHealth() AppHealth { return AppHealth_APP_HEALTH_UNSPECIFIED } -type WorkspaceMonitorUpdateRequest_Datapoint struct { +type PushResourcesMonitoringUsageRequest_Datapoint struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - CollectedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=collected_at,json=collectedAt,proto3" json:"collected_at,omitempty"` - Memory *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` - Volume []*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage `protobuf:"bytes,3,rep,name=volume,proto3" json:"volume,omitempty"` + CollectedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=collected_at,json=collectedAt,proto3" json:"collected_at,omitempty"` + Memory *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` + Volume []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage `protobuf:"bytes,3,rep,name=volume,proto3" json:"volume,omitempty"` } -func (x *WorkspaceMonitorUpdateRequest_Datapoint) Reset() { - *x = WorkspaceMonitorUpdateRequest_Datapoint{} +func (x *PushResourcesMonitoringUsageRequest_Datapoint) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2802,13 +2802,13 @@ func (x *WorkspaceMonitorUpdateRequest_Datapoint) Reset() { } } -func (x *WorkspaceMonitorUpdateRequest_Datapoint) String() string { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceMonitorUpdateRequest_Datapoint) ProtoMessage() {} +func (*PushResourcesMonitoringUsageRequest_Datapoint) ProtoMessage() {} -func (x *WorkspaceMonitorUpdateRequest_Datapoint) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2820,44 +2820,44 @@ func (x *WorkspaceMonitorUpdateRequest_Datapoint) ProtoReflect() protoreflect.Me return mi.MessageOf(x) } -// Deprecated: Use WorkspaceMonitorUpdateRequest_Datapoint.ProtoReflect.Descriptor instead. -func (*WorkspaceMonitorUpdateRequest_Datapoint) Descriptor() ([]byte, []int) { +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0} } -func (x *WorkspaceMonitorUpdateRequest_Datapoint) GetCollectedAt() *timestamppb.Timestamp { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetCollectedAt() *timestamppb.Timestamp { if x != nil { return x.CollectedAt } return nil } -func (x *WorkspaceMonitorUpdateRequest_Datapoint) GetMemory() *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetMemory() *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage { if x != nil { return x.Memory } return nil } -func (x *WorkspaceMonitorUpdateRequest_Datapoint) GetVolume() []*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetVolume() []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage { if x != nil { return x.Volume } return nil } -type WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage struct { +type PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - Used int32 `protobuf:"varint,2,opt,name=used,proto3" json:"used,omitempty"` - Total int32 `protobuf:"varint,3,opt,name=total,proto3" json:"total,omitempty"` + Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` + SpaceUsed int64 `protobuf:"varint,2,opt,name=space_used,json=spaceUsed,proto3" json:"space_used,omitempty"` + SpaceTotal int64 `protobuf:"varint,3,opt,name=space_total,json=spaceTotal,proto3" json:"space_total,omitempty"` } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) Reset() { - *x = WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{} +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2865,13 +2865,13 @@ func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) Reset() { } } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) String() string { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) ProtoMessage() {} +func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoMessage() {} -func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2883,43 +2883,43 @@ func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) ProtoReflect() pro return mi.MessageOf(x) } -// Deprecated: Use WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage.ProtoReflect.Descriptor instead. -func (*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) Descriptor() ([]byte, []int) { +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0, 0} } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) GetPath() string { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetPath() string { if x != nil { return x.Path } return "" } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) GetUsed() int32 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetSpaceUsed() int64 { if x != nil { - return x.Used + return x.SpaceUsed } return 0 } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) GetTotal() int32 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetSpaceTotal() int64 { if x != nil { - return x.Total + return x.SpaceTotal } return 0 } -type WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage struct { +type PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Used int32 `protobuf:"varint,1,opt,name=used,proto3" json:"used,omitempty"` - Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + Used int64 `protobuf:"varint,1,opt,name=used,proto3" json:"used,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) Reset() { - *x = WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{} +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2927,13 +2927,13 @@ func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) Reset() { } } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) String() string { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) String() string { return protoimpl.X.MessageStringOf(x) } -func (*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) ProtoMessage() {} +func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoMessage() {} -func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2945,19 +2945,19 @@ func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) ProtoReflect() pro return mi.MessageOf(x) } -// Deprecated: Use WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage.ProtoReflect.Descriptor instead. -func (*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) Descriptor() ([]byte, []int) { +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0, 1} } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) GetUsed() int32 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetUsed() int64 { if x != nil { return x.Used } return 0 } -func (x *WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage) GetTotal() int32 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetTotal() int64 { if x != nil { return x.Total } @@ -3358,121 +3358,126 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, - 0x4e, 0x10, 0x03, 0x22, 0x85, 0x04, 0x0a, 0x1d, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x57, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x8a, - 0x03, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x5b, 0x0a, 0x06, 0x6d, - 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, - 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x5b, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, - 0x6d, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x06, 0x76, - 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x1a, 0x4b, 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, - 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, - 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, - 0x61, 0x6c, 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x20, 0x0a, 0x1e, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x63, 0x0a, - 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x50, - 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, - 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, 0x42, 0x4c, - 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, 0x4c, 0x49, - 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, - 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, - 0x10, 0x04, 0x32, 0xe8, 0x08, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, 0x0a, 0x0b, - 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, - 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, 0x65, 0x74, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x27, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, - 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, - 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, - 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, - 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x4e, 0x10, 0x03, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, + 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0a, 0x64, + 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x3d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, + 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, + 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0xac, 0x03, 0x0a, 0x09, 0x44, + 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x61, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, + 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, + 0x67, 0x65, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x61, 0x0a, 0x06, 0x76, 0x6f, + 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, + 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, + 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x1a, 0x61, 0x0a, + 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x70, 0x61, 0x63, 0x65, 0x55, 0x73, 0x65, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, + 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, 0x73, + 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, + 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, + 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, + 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, + 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, + 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xfb, 0x08, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, + 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, + 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, + 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, + 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, + 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, + 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, + 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, + 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, + 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, - 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x47, - 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, - 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, - 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, - 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x57, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x12, 0x2d, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, + 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, + 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, + 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, - 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, + 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, + 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3527,8 +3532,8 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ (*WorkspaceAgentScriptCompletedRequest)(nil), // 34: coder.agent.v2.WorkspaceAgentScriptCompletedRequest (*WorkspaceAgentScriptCompletedResponse)(nil), // 35: coder.agent.v2.WorkspaceAgentScriptCompletedResponse (*Timing)(nil), // 36: coder.agent.v2.Timing - (*WorkspaceMonitorUpdateRequest)(nil), // 37: coder.agent.v2.WorkspaceMonitorUpdateRequest - (*WorkspaceMonitorUpdateResponse)(nil), // 38: coder.agent.v2.WorkspaceMonitorUpdateResponse + (*PushResourcesMonitoringUsageRequest)(nil), // 37: coder.agent.v2.PushResourcesMonitoringUsageRequest + (*PushResourcesMonitoringUsageResponse)(nil), // 38: coder.agent.v2.PushResourcesMonitoringUsageResponse (*WorkspaceApp_Healthcheck)(nil), // 39: coder.agent.v2.WorkspaceApp.Healthcheck (*WorkspaceAgentMetadata_Result)(nil), // 40: coder.agent.v2.WorkspaceAgentMetadata.Result (*WorkspaceAgentMetadata_Description)(nil), // 41: coder.agent.v2.WorkspaceAgentMetadata.Description @@ -3536,13 +3541,13 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ nil, // 43: coder.agent.v2.Stats.ConnectionsByProtoEntry (*Stats_Metric)(nil), // 44: coder.agent.v2.Stats.Metric (*Stats_Metric_Label)(nil), // 45: coder.agent.v2.Stats.Metric.Label - (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 46: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate - (*WorkspaceMonitorUpdateRequest_Datapoint)(nil), // 47: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint - (*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage)(nil), // 48: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.VolumeUsage - (*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage)(nil), // 49: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.MemoryUsage - (*durationpb.Duration)(nil), // 50: google.protobuf.Duration - (*proto.DERPMap)(nil), // 51: coder.tailnet.v2.DERPMap - (*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp + (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 46: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate + (*PushResourcesMonitoringUsageRequest_Datapoint)(nil), // 47: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint + (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 48: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 49: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + (*durationpb.Duration)(nil), // 50: google.protobuf.Duration + (*proto.DERPMap)(nil), // 51: coder.tailnet.v2.DERPMap + (*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp } var file_agent_proto_agent_proto_depIdxs = []int32{ 1, // 0: coder.agent.v2.WorkspaceApp.sharing_level:type_name -> coder.agent.v2.WorkspaceApp.SharingLevel @@ -3577,7 +3582,7 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 52, // 29: coder.agent.v2.Timing.end:type_name -> google.protobuf.Timestamp 7, // 30: coder.agent.v2.Timing.stage:type_name -> coder.agent.v2.Timing.Stage 8, // 31: coder.agent.v2.Timing.status:type_name -> coder.agent.v2.Timing.Status - 47, // 32: coder.agent.v2.WorkspaceMonitorUpdateRequest.datapoints:type_name -> coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint + 47, // 32: coder.agent.v2.PushResourcesMonitoringUsageRequest.datapoints:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint 50, // 33: coder.agent.v2.WorkspaceApp.Healthcheck.interval:type_name -> google.protobuf.Duration 52, // 34: coder.agent.v2.WorkspaceAgentMetadata.Result.collected_at:type_name -> google.protobuf.Timestamp 50, // 35: coder.agent.v2.WorkspaceAgentMetadata.Description.interval:type_name -> google.protobuf.Duration @@ -3585,9 +3590,9 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 3, // 37: coder.agent.v2.Stats.Metric.type:type_name -> coder.agent.v2.Stats.Metric.Type 45, // 38: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label 0, // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth - 52, // 40: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp - 49, // 41: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.memory:type_name -> coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.MemoryUsage - 48, // 42: coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.volume:type_name -> coder.agent.v2.WorkspaceMonitorUpdateRequest.Datapoint.VolumeUsage + 52, // 40: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp + 49, // 41: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + 48, // 42: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volume:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage 13, // 43: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest 15, // 44: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest 17, // 45: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest @@ -3598,7 +3603,7 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 29, // 50: coder.agent.v2.Agent.BatchCreateLogs:input_type -> coder.agent.v2.BatchCreateLogsRequest 31, // 51: coder.agent.v2.Agent.GetAnnouncementBanners:input_type -> coder.agent.v2.GetAnnouncementBannersRequest 34, // 52: coder.agent.v2.Agent.ScriptCompleted:input_type -> coder.agent.v2.WorkspaceAgentScriptCompletedRequest - 37, // 53: coder.agent.v2.Agent.UpdateWorkspaceMonitor:input_type -> coder.agent.v2.WorkspaceMonitorUpdateRequest + 37, // 53: coder.agent.v2.Agent.PushResourcesMonitoringUsage:input_type -> coder.agent.v2.PushResourcesMonitoringUsageRequest 12, // 54: coder.agent.v2.Agent.GetManifest:output_type -> coder.agent.v2.Manifest 14, // 55: coder.agent.v2.Agent.GetServiceBanner:output_type -> coder.agent.v2.ServiceBanner 18, // 56: coder.agent.v2.Agent.UpdateStats:output_type -> coder.agent.v2.UpdateStatsResponse @@ -3609,7 +3614,7 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 30, // 61: coder.agent.v2.Agent.BatchCreateLogs:output_type -> coder.agent.v2.BatchCreateLogsResponse 32, // 62: coder.agent.v2.Agent.GetAnnouncementBanners:output_type -> coder.agent.v2.GetAnnouncementBannersResponse 35, // 63: coder.agent.v2.Agent.ScriptCompleted:output_type -> coder.agent.v2.WorkspaceAgentScriptCompletedResponse - 38, // 64: coder.agent.v2.Agent.UpdateWorkspaceMonitor:output_type -> coder.agent.v2.WorkspaceMonitorUpdateResponse + 38, // 64: coder.agent.v2.Agent.PushResourcesMonitoringUsage:output_type -> coder.agent.v2.PushResourcesMonitoringUsageResponse 54, // [54:65] is the sub-list for method output_type 43, // [43:54] is the sub-list for method input_type 43, // [43:43] is the sub-list for extension type_name @@ -3960,7 +3965,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceMonitorUpdateRequest); i { + switch v := v.(*PushResourcesMonitoringUsageRequest); i { case 0: return &v.state case 1: @@ -3972,7 +3977,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceMonitorUpdateResponse); i { + switch v := v.(*PushResourcesMonitoringUsageResponse); i { case 0: return &v.state case 1: @@ -4056,7 +4061,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceMonitorUpdateRequest_Datapoint); i { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint); i { case 0: return &v.state case 1: @@ -4068,7 +4073,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage); i { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage); i { case 0: return &v.state case 1: @@ -4080,7 +4085,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage); i { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage); i { case 0: return &v.state case 1: diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index c187e289f8131..8fc83c88956e7 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -295,17 +295,17 @@ message Timing { Status status = 6; } -message WorkspaceMonitorUpdateRequest { +message PushResourcesMonitoringUsageRequest { message Datapoint { message VolumeUsage { string path = 1; - int32 used = 2; - int32 total = 3; + int64 space_used = 2; + int64 space_total = 3; } message MemoryUsage { - int32 used = 1; - int32 total = 2; + int64 used = 1; + int64 total = 2; } google.protobuf.Timestamp collected_at = 1; @@ -316,7 +316,7 @@ message WorkspaceMonitorUpdateRequest { repeated Datapoint datapoints = 1; } -message WorkspaceMonitorUpdateResponse { +message PushResourcesMonitoringUsageResponse { } @@ -331,5 +331,5 @@ service Agent { rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse); rpc GetAnnouncementBanners(GetAnnouncementBannersRequest) returns (GetAnnouncementBannersResponse); rpc ScriptCompleted(WorkspaceAgentScriptCompletedRequest) returns (WorkspaceAgentScriptCompletedResponse); - rpc UpdateWorkspaceMonitor(WorkspaceMonitorUpdateRequest) returns (WorkspaceMonitorUpdateResponse); + rpc PushResourcesMonitoringUsage(PushResourcesMonitoringUsageRequest) returns (PushResourcesMonitoringUsageResponse); } diff --git a/agent/proto/agent_drpc.pb.go b/agent/proto/agent_drpc.pb.go index c0f58cd11f3cc..071d0b65dae57 100644 --- a/agent/proto/agent_drpc.pb.go +++ b/agent/proto/agent_drpc.pb.go @@ -48,7 +48,7 @@ type DRPCAgentClient interface { BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(ctx context.Context, in *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) ScriptCompleted(ctx context.Context, in *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) - UpdateWorkspaceMonitor(ctx context.Context, in *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) + PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) } type drpcAgentClient struct { @@ -151,9 +151,9 @@ func (c *drpcAgentClient) ScriptCompleted(ctx context.Context, in *WorkspaceAgen return out, nil } -func (c *drpcAgentClient) UpdateWorkspaceMonitor(ctx context.Context, in *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) { - out := new(WorkspaceMonitorUpdateResponse) - err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/UpdateWorkspaceMonitor", drpcEncoding_File_agent_proto_agent_proto{}, in, out) +func (c *drpcAgentClient) PushResourcesMonitoringUsage(ctx context.Context, in *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) { + out := new(PushResourcesMonitoringUsageResponse) + err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/PushResourcesMonitoringUsage", drpcEncoding_File_agent_proto_agent_proto{}, in, out) if err != nil { return nil, err } @@ -171,7 +171,7 @@ type DRPCAgentServer interface { BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) GetAnnouncementBanners(context.Context, *GetAnnouncementBannersRequest) (*GetAnnouncementBannersResponse, error) ScriptCompleted(context.Context, *WorkspaceAgentScriptCompletedRequest) (*WorkspaceAgentScriptCompletedResponse, error) - UpdateWorkspaceMonitor(context.Context, *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) + PushResourcesMonitoringUsage(context.Context, *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) } type DRPCAgentUnimplementedServer struct{} @@ -216,7 +216,7 @@ func (s *DRPCAgentUnimplementedServer) ScriptCompleted(context.Context, *Workspa return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } -func (s *DRPCAgentUnimplementedServer) UpdateWorkspaceMonitor(context.Context, *WorkspaceMonitorUpdateRequest) (*WorkspaceMonitorUpdateResponse, error) { +func (s *DRPCAgentUnimplementedServer) PushResourcesMonitoringUsage(context.Context, *PushResourcesMonitoringUsageRequest) (*PushResourcesMonitoringUsageResponse, error) { return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented) } @@ -317,14 +317,14 @@ func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, ) }, DRPCAgentServer.ScriptCompleted, true case 10: - return "/coder.agent.v2.Agent/UpdateWorkspaceMonitor", drpcEncoding_File_agent_proto_agent_proto{}, + return "/coder.agent.v2.Agent/PushResourcesMonitoringUsage", drpcEncoding_File_agent_proto_agent_proto{}, func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) { return srv.(DRPCAgentServer). - UpdateWorkspaceMonitor( + PushResourcesMonitoringUsage( ctx, - in1.(*WorkspaceMonitorUpdateRequest), + in1.(*PushResourcesMonitoringUsageRequest), ) - }, DRPCAgentServer.UpdateWorkspaceMonitor, true + }, DRPCAgentServer.PushResourcesMonitoringUsage, true default: return "", nil, nil, nil, false } @@ -494,16 +494,16 @@ func (x *drpcAgent_ScriptCompletedStream) SendAndClose(m *WorkspaceAgentScriptCo return x.CloseSend() } -type DRPCAgent_UpdateWorkspaceMonitorStream interface { +type DRPCAgent_PushResourcesMonitoringUsageStream interface { drpc.Stream - SendAndClose(*WorkspaceMonitorUpdateResponse) error + SendAndClose(*PushResourcesMonitoringUsageResponse) error } -type drpcAgent_UpdateWorkspaceMonitorStream struct { +type drpcAgent_PushResourcesMonitoringUsageStream struct { drpc.Stream } -func (x *drpcAgent_UpdateWorkspaceMonitorStream) SendAndClose(m *WorkspaceMonitorUpdateResponse) error { +func (x *drpcAgent_PushResourcesMonitoringUsageStream) SendAndClose(m *PushResourcesMonitoringUsageResponse) error { if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil { return err } diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 0d81dc8955541..3747106c7fdb3 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -46,7 +46,7 @@ type API struct { *MetadataAPI *LogsAPI *ScriptsAPI - *WorkspaceMonitorAPI + *ResourcesMonitoringAPI *tailnet.DRPCService mu sync.Mutex @@ -154,7 +154,7 @@ func New(opts Options) *API { Database: opts.Database, } - api.WorkspaceMonitorAPI = &WorkspaceMonitorAPI{ + api.ResourcesMonitoringAPI = &ResourcesMonitoringAPI{ AgentID: opts.AgentID, WorkspaceID: opts.WorkspaceID, Clock: opts.Clock, diff --git a/coderd/agentapi/workspacemonitor.go b/coderd/agentapi/resources_monitoring.go similarity index 66% rename from coderd/agentapi/workspacemonitor.go rename to coderd/agentapi/resources_monitoring.go index ec24966d56edc..64a89bcce1439 100644 --- a/coderd/agentapi/workspacemonitor.go +++ b/coderd/agentapi/resources_monitoring.go @@ -20,7 +20,7 @@ import ( "github.com/coder/quartz" ) -type WorkspaceMonitorAPI struct { +type ResourcesMonitoringAPI struct { AgentID uuid.UUID WorkspaceID uuid.UUID @@ -39,9 +39,7 @@ type WorkspaceMonitorAPI struct { MinimumNOKs int } -func (m *WorkspaceMonitorAPI) UpdateWorkspaceMonitor(ctx context.Context, req *agentproto.WorkspaceMonitorUpdateRequest) (*agentproto.WorkspaceMonitorUpdateResponse, error) { - res := &agentproto.WorkspaceMonitorUpdateResponse{} - +func (m *ResourcesMonitoringAPI) PushResourcesMonitoringUsage(ctx context.Context, req *agentproto.PushResourcesMonitoringUsageRequest) (*agentproto.PushResourcesMonitoringUsageResponse, error) { if err := m.monitorMemory(ctx, req.Datapoints); err != nil { return nil, xerrors.Errorf("monitor memory: %w", err) } @@ -50,10 +48,10 @@ func (m *WorkspaceMonitorAPI) UpdateWorkspaceMonitor(ctx context.Context, req *a return nil, xerrors.Errorf("monitor volumes: %w", err) } - return res, nil + return &agentproto.PushResourcesMonitoringUsageResponse{}, nil } -func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint) error { +func (m *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint) error { monitor, err := m.Database.FetchMemoryResourceMonitorsByAgentID(ctx, m.AgentID) if err != nil { // It is valid for an agent to not have a memory monitor, so we @@ -69,19 +67,19 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a return nil } - usageDatapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage, 0, len(datapoints)) + usageDatapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage, 0, len(datapoints)) for _, datapoint := range datapoints { usageDatapoints = append(usageDatapoints, datapoint.Memory) } - memoryUsageStates := calculateMemoryUsageStates(monitor, usageDatapoints) + usageStates := calculateMemoryUsageStates(monitor, usageDatapoints) oldState := monitor.State - newState := m.nextState(oldState, memoryUsageStates) + newState := m.nextState(oldState, usageStates) - shouldNotify := oldState == database.WorkspaceAgentMonitorStateOK && - newState == database.WorkspaceAgentMonitorStateNOK && - m.Clock.Now().After(monitor.DebouncedUntil) + shouldNotify := m.Clock.Now().After(monitor.DebouncedUntil) && + oldState == database.WorkspaceAgentMonitorStateOK && + newState == database.WorkspaceAgentMonitorStateNOK debouncedUntil := monitor.DebouncedUntil if shouldNotify { @@ -123,13 +121,13 @@ func (m *WorkspaceMonitorAPI) monitorMemory(ctx context.Context, datapoints []*a return nil } -func (m *WorkspaceMonitorAPI) monitorVolumes(ctx context.Context, datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint) error { +func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint) error { volumeMonitors, err := m.Database.FetchVolumesResourceMonitorsByAgentID(ctx, m.AgentID) if err != nil { return xerrors.Errorf("get or insert volume monitor: %w", err) } - volumes := make(map[string][]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage) + volumes := make(map[string][]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) for _, datapoint := range datapoints { for _, volume := range datapoint.Volume { @@ -139,76 +137,70 @@ func (m *WorkspaceMonitorAPI) monitorVolumes(ctx context.Context, datapoints []* } } + outOfDiskVolumes := make([]map[string]any, 0) + for _, monitor := range volumeMonitors { - if err := m.monitorVolume(ctx, monitor, monitor.Path, volumes[monitor.Path]); err != nil { - return xerrors.Errorf("monitor volume: %w", err) + if !monitor.Enabled { + continue } - } - - return nil -} -func (m *WorkspaceMonitorAPI) monitorVolume( - ctx context.Context, - monitor database.WorkspaceAgentVolumeResourceMonitor, - path string, - datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage, -) error { - if !monitor.Enabled { - return nil - } + usageStates := calculateVolumeUsageStates(monitor, volumes[monitor.Path]) - volumeUsageStates := calculateVolumeUsageStates(monitor, datapoints) + oldState := monitor.State + newState := m.nextState(oldState, usageStates) - oldState := monitor.State - newState := m.nextState(oldState, volumeUsageStates) + shouldNotify := m.Clock.Now().After(monitor.DebouncedUntil) && + oldState == database.WorkspaceAgentMonitorStateOK && + newState == database.WorkspaceAgentMonitorStateNOK - shouldNotify := oldState == database.WorkspaceAgentMonitorStateOK && - newState == database.WorkspaceAgentMonitorStateNOK && - m.Clock.Now().After(monitor.DebouncedUntil) + debouncedUntil := monitor.DebouncedUntil + if shouldNotify { + debouncedUntil = m.Clock.Now().Add(m.Debounce) - debouncedUntil := monitor.DebouncedUntil - if shouldNotify { - debouncedUntil = m.Clock.Now().Add(m.Debounce) - } + outOfDiskVolumes = append(outOfDiskVolumes, map[string]any{ + "path": monitor.Path, + "threshold": fmt.Sprintf("%d%%", monitor.Threshold), + }) + } - if err := m.Database.UpdateVolumeResourceMonitor(ctx, database.UpdateVolumeResourceMonitorParams{ - AgentID: m.AgentID, - Path: path, - State: newState, - UpdatedAt: dbtime.Time(m.Clock.Now()), - DebouncedUntil: dbtime.Time(debouncedUntil), - }); err != nil { - return xerrors.Errorf("update workspace monitor: %w", err) + if err := m.Database.UpdateVolumeResourceMonitor(ctx, database.UpdateVolumeResourceMonitorParams{ + AgentID: m.AgentID, + Path: monitor.Path, + State: newState, + UpdatedAt: dbtime.Time(m.Clock.Now()), + DebouncedUntil: dbtime.Time(debouncedUntil), + }); err != nil { + return xerrors.Errorf("update workspace monitor: %w", err) + } } - if shouldNotify { + if len(outOfDiskVolumes) != 0 { workspace, err := m.Database.GetWorkspaceByID(ctx, m.WorkspaceID) if err != nil { return xerrors.Errorf("get workspace by id: %w", err) } - _, err = m.NotificationsEnqueuer.Enqueue( + if _, err := m.NotificationsEnqueuer.EnqueueWithData( // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, notifications.TemplateWorkspaceOutOfDisk, map[string]string{ "workspace": workspace.Name, - "threshold": fmt.Sprintf("%d%%", monitor.Threshold), - "volume": path, }, - "workspace-monitor-memory", - ) - if err != nil { - return xerrors.Errorf("notify workspace OOM: %w", err) + map[string]any{ + "volumes": outOfDiskVolumes, + }, + "workspace-monitor-volumes", + ); err != nil { + return xerrors.Errorf("notify workspace OOD: %w", err) } } return nil } -func (m *WorkspaceMonitorAPI) nextState( +func (m *ResourcesMonitoringAPI) nextState( oldState database.WorkspaceAgentMonitorState, states []database.WorkspaceAgentMonitorState, ) database.WorkspaceAgentMonitorState { @@ -242,7 +234,7 @@ func (m *WorkspaceMonitorAPI) nextState( func calculateMemoryUsageStates( monitor database.WorkspaceAgentMemoryResourceMonitor, - datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage, + datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage, ) []database.WorkspaceAgentMonitorState { states := make([]database.WorkspaceAgentMonitorState, 0, len(datapoints)) @@ -262,12 +254,12 @@ func calculateMemoryUsageStates( func calculateVolumeUsageStates( monitor database.WorkspaceAgentVolumeResourceMonitor, - datapoints []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage, + datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage, ) []database.WorkspaceAgentMonitorState { states := make([]database.WorkspaceAgentMonitorState, 0, len(datapoints)) for _, datapoint := range datapoints { - percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) + percent := int32(float64(datapoint.SpaceUsed) / float64(datapoint.SpaceTotal) * 100) state := database.WorkspaceAgentMonitorStateOK if percent >= monitor.Threshold { diff --git a/coderd/agentapi/workspacemonitor_test.go b/coderd/agentapi/resources_monitoring_test.go similarity index 73% rename from coderd/agentapi/workspacemonitor_test.go rename to coderd/agentapi/resources_monitoring_test.go index 64881da533369..8995b5915c017 100644 --- a/coderd/agentapi/workspacemonitor_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -19,7 +19,7 @@ import ( "github.com/coder/quartz" ) -func workspaceMonitorAPI(t *testing.T) (*agentapi.WorkspaceMonitorAPI, database.User, *quartz.Mock, *notificationstest.FakeEnqueuer) { +func workspaceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, database.User, *quartz.Mock, *notificationstest.FakeEnqueuer) { t.Helper() db, _ := dbtestutil.NewDB(t) @@ -57,7 +57,7 @@ func workspaceMonitorAPI(t *testing.T) (*agentapi.WorkspaceMonitorAPI, database. notifyEnq := ¬ificationstest.FakeEnqueuer{} clock := quartz.NewMock(t) - return &agentapi.WorkspaceMonitorAPI{ + return &agentapi.ResourcesMonitoringAPI{ AgentID: agent.ID, WorkspaceID: workspace.ID, Clock: clock, @@ -93,17 +93,18 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { }) // When: The monitor is given a state that will trigger NOK - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ Used: 10, Total: 10, }, }, }, }) + require.NoError(t, err) // Then: We expect there to be a notification sent sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) @@ -112,17 +113,18 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { // When: The monitor moves to an OK state from NOK clock.Advance(api.Debounce / 4) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ Used: 1, Total: 10, }, }, }, }) + require.NoError(t, err) // Then: We expect no new notifications sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) @@ -131,17 +133,18 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { // When: The monitor moves back to a NOK state before the debounced time. clock.Advance(api.Debounce / 4) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ Used: 10, Total: 10, }, }, }, }) + require.NoError(t, err) // Then: We expect no new notifications (showing the debouncer working) sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) @@ -150,17 +153,18 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { // When: The monitor moves back to an OK state from NOK clock.Advance(api.Debounce / 4) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ Used: 1, Total: 10, }, }, }, }) + require.NoError(t, err) // Then: We still expect no new notifications sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) @@ -169,17 +173,18 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { // When: The monitor moves back to a NOK state after the debounce period. clock.Advance(api.Debounce/4 + 1*time.Second) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ Used: 10, Total: 10, }, }, }, }) + require.NoError(t, err) // Then: We expect a notification sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) @@ -191,8 +196,8 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { tests := []struct { name string - memoryUsage []int32 - memoryTotal int32 + memoryUsage []int64 + memoryTotal int64 thresholdPercent int32 minimumNOKs int consecutiveNOKs int @@ -202,7 +207,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }{ { name: "WhenOK/NeverExceedsThreshold", - memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, memoryTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -213,7 +218,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }, { name: "WhenOK/ConsecutiveExceedsThreshold", - memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, memoryTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -224,7 +229,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }, { name: "WhenOK/MinimumExceedsThreshold", - memoryUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, memoryTotal: 10, thresholdPercent: 80, minimumNOKs: 4, @@ -235,7 +240,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }, { name: "WhenNOK/NeverExceedsThreshold", - memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, memoryTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -246,7 +251,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }, { name: "WhenNOK/ConsecutiveExceedsThreshold", - memoryUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, memoryTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -257,7 +262,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }, { name: "WhenNOK/MinimumExceedsThreshold", - memoryUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, memoryTotal: 10, thresholdPercent: 80, minimumNOKs: 4, @@ -278,13 +283,13 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { api.MinimumNOKs = tt.minimumNOKs api.ConsecutiveNOKs = tt.consecutiveNOKs - datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.memoryUsage)) + datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.memoryUsage)) collectedAt := clock.Now() for _, usage := range tt.memoryUsage { collectedAt = collectedAt.Add(15 * time.Second) - datapoints = append(datapoints, &agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + datapoints = append(datapoints, &agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ CollectedAt: timestamppb.New(collectedAt), - Memory: &agentproto.WorkspaceMonitorUpdateRequest_Datapoint_MemoryUsage{ + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ Used: usage, Total: tt.memoryTotal, }, @@ -298,7 +303,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { }) clock.Set(collectedAt) - _, err := api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: datapoints, }) require.NoError(t, err) @@ -344,20 +349,21 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { }) // When: The monitor is given a state that will trigger NOK - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: volumePath, - Used: 10, - Total: 10, + Path: volumePath, + SpaceUsed: 10, + SpaceTotal: 10, }, }, }, }, }) + require.NoError(t, err) // Then: We expect there to be a notification sent sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) @@ -366,20 +372,21 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { // When: The monitor moves to an OK state from NOK clock.Advance(api.Debounce / 4) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: volumePath, - Used: 1, - Total: 10, + Path: volumePath, + SpaceUsed: 1, + SpaceTotal: 10, }, }, }, }, }) + require.NoError(t, err) // Then: We expect no new notifications sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) @@ -388,20 +395,21 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { // When: The monitor moves back to a NOK state before the debounced time. clock.Advance(api.Debounce / 4) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: volumePath, - Used: 10, - Total: 10, + Path: volumePath, + SpaceUsed: 10, + SpaceTotal: 10, }, }, }, }, }) + require.NoError(t, err) // Then: We expect no new notifications (showing the debouncer working) sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) @@ -410,20 +418,21 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { // When: The monitor moves back to an OK state from NOK clock.Advance(api.Debounce / 4) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: volumePath, - Used: 1, - Total: 10, + Path: volumePath, + SpaceUsed: 1, + SpaceTotal: 10, }, }, }, }, }) + require.NoError(t, err) // Then: We still expect no new notifications sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) @@ -432,20 +441,21 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { // When: The monitor moves back to a NOK state after the debounce period. clock.Advance(api.Debounce/4 + 1*time.Second) - api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ - Datapoints: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: volumePath, - Used: 10, - Total: 10, + Path: volumePath, + SpaceUsed: 10, + SpaceTotal: 10, }, }, }, }, }) + require.NoError(t, err) // Then: We expect a notification sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) @@ -458,8 +468,8 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { tests := []struct { name string volumePath string - volumeUsage []int32 - volumeTotal int32 + volumeUsage []int64 + volumeTotal int64 thresholdPercent int32 previousState database.WorkspaceAgentMonitorState expectState database.WorkspaceAgentMonitorState @@ -470,7 +480,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { { name: "WhenOK/NeverExceedsThreshold", volumePath: "/home/coder", - volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, volumeTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -482,7 +492,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { { name: "WhenOK/ConsecutiveExceedsThreshold", volumePath: "/home/coder", - volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, volumeTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -494,7 +504,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { { name: "WhenOK/MinimumExceedsThreshold", volumePath: "/home/coder", - volumeUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + volumeUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, volumeTotal: 10, thresholdPercent: 80, minimumNOKs: 4, @@ -506,7 +516,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { { name: "WhenNOK/NeverExceedsThreshold", volumePath: "/home/coder", - volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, volumeTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -518,7 +528,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { { name: "WhenNOK/ConsecutiveExceedsThreshold", volumePath: "/home/coder", - volumeUsage: []int32{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, volumeTotal: 10, thresholdPercent: 80, consecutiveNOKs: 4, @@ -530,7 +540,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { { name: "WhenNOK/MinimumExceedsThreshold", volumePath: "/home/coder", - volumeUsage: []int32{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + volumeUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, volumeTotal: 10, thresholdPercent: 80, minimumNOKs: 4, @@ -551,20 +561,20 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { api.MinimumNOKs = tt.minimumNOKs api.ConsecutiveNOKs = tt.consecutiveNOKs - datapoints := make([]*agentproto.WorkspaceMonitorUpdateRequest_Datapoint, 0, len(tt.volumeUsage)) + datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.volumeUsage)) collectedAt := clock.Now() for _, volumeUsage := range tt.volumeUsage { collectedAt = collectedAt.Add(15 * time.Second) - volumeDatapoints := []*agentproto.WorkspaceMonitorUpdateRequest_Datapoint_VolumeUsage{ + volumeDatapoints := []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: tt.volumePath, - Used: volumeUsage, - Total: tt.volumeTotal, + Path: tt.volumePath, + SpaceUsed: volumeUsage, + SpaceTotal: tt.volumeTotal, }, } - datapoints = append(datapoints, &agentproto.WorkspaceMonitorUpdateRequest_Datapoint{ + datapoints = append(datapoints, &agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ CollectedAt: timestamppb.New(collectedAt), Volume: volumeDatapoints, }) @@ -578,7 +588,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { }) clock.Set(collectedAt) - _, err := api.UpdateWorkspaceMonitor(context.Background(), &agentproto.WorkspaceMonitorUpdateRequest{ + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: datapoints, }) require.NoError(t, err) From d08e71350c6bf7f19902672caa94b6f63c51da91 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 6 Feb 2025 11:26:59 +0000 Subject: [PATCH 20/37] chore: add another test --- coderd/agentapi/resources_monitoring_test.go | 85 ++++++++++++++++---- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 8995b5915c017..ce38d3374a8e9 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -19,7 +19,7 @@ import ( "github.com/coder/quartz" ) -func workspaceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, database.User, *quartz.Mock, *notificationstest.FakeEnqueuer) { +func resourceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, database.User, *quartz.Mock, *notificationstest.FakeEnqueuer) { t.Helper() db, _ := dbtestutil.NewDB(t) @@ -63,10 +63,13 @@ func workspaceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, databa Clock: clock, Database: db, NotificationsEnqueuer: notifyEnq, + MinimumNOKs: 4, + ConsecutiveNOKs: 10, + Debounce: 1 * time.Minute, }, user, clock, notifyEnq } -func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { +func TestMemoryResourceMonitorDebounce(t *testing.T) { t.Parallel() // This test is a bit of a long one. We're testing that @@ -80,10 +83,7 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { // 4. NOK -> OK |> does nothing // 5. OK -> NOK |> sends a notification as debounce period exceeded - api, _, clock, notifyEnq := workspaceMonitorAPI(t) - api.MinimumNOKs = 10 - api.ConsecutiveNOKs = 4 - api.Debounce = 1 * time.Minute + api, user, clock, notifyEnq := resourceMonitorAPI(t) // Given: A monitor in an OK state dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -109,6 +109,7 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { // Then: We expect there to be a notification sent sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) notifyEnq.Clear() // When: The monitor moves to an OK state from NOK @@ -189,9 +190,10 @@ func TestWorkspaceMemoryMonitorDebounce(t *testing.T) { // Then: We expect a notification sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) require.Len(t, sent, 1) + require.Equal(t, user.ID, sent[0].UserID) } -func TestWorkspaceMemoryMonitor(t *testing.T) { +func TestMemoryResourceMonitor(t *testing.T) { t.Parallel() tests := []struct { @@ -279,7 +281,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, user, clock, notifyEnq := workspaceMonitorAPI(t) + api, user, clock, notifyEnq := resourceMonitorAPI(t) api.MinimumNOKs = tt.minimumNOKs api.ConsecutiveNOKs = tt.consecutiveNOKs @@ -319,7 +321,7 @@ func TestWorkspaceMemoryMonitor(t *testing.T) { } } -func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { +func TestVolumeResourceMonitorDebounce(t *testing.T) { t.Parallel() // This test is a bit of a long one. We're testing that @@ -335,10 +337,7 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { volumePath := "/home/coder" - api, _, clock, notifyEnq := workspaceMonitorAPI(t) - api.MinimumNOKs = 10 - api.ConsecutiveNOKs = 4 - api.Debounce = 1 * time.Minute + api, _, clock, notifyEnq := resourceMonitorAPI(t) // Given: A monitor in an OK state dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ @@ -462,7 +461,7 @@ func TestWorkspaceVolumeMonitorDebounce(t *testing.T) { require.Len(t, sent, 1) } -func TestWorkspaceVolumeMonitor(t *testing.T) { +func TestVolumeResourceMonitor(t *testing.T) { t.Parallel() tests := []struct { @@ -557,7 +556,7 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - api, user, clock, notifyEnq := workspaceMonitorAPI(t) + api, user, clock, notifyEnq := resourceMonitorAPI(t) api.MinimumNOKs = tt.minimumNOKs api.ConsecutiveNOKs = tt.consecutiveNOKs @@ -603,3 +602,59 @@ func TestWorkspaceVolumeMonitor(t *testing.T) { }) } } + +func TestVolumeResourceMonitorMultiple(t *testing.T) { + t.Parallel() + + api, _, clock, notifyEnq := resourceMonitorAPI(t) + + // Given: two different volume resource monitors + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: "/home/coder", + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: "/dev/coder", + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + // When: only one of them is in an NOK state. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Path: "/home/coder", + SpaceUsed: 1, + SpaceTotal: 10, + }, + { + Path: "/dev/coder", + SpaceUsed: 10, + SpaceTotal: 10, + }, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect a notification that contains only the alerting volume. + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + + volumesData := sent[0].Data["volumes"] + require.IsType(t, []map[string]any{}, volumesData) + + volumes := volumesData.([]map[string]any) + require.Len(t, volumes, 1) + + volume := volumes[0] + require.Equal(t, "/dev/coder", volume["path"]) +} From ed42eae70355354be9c43436841ad4000e296817 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 6 Feb 2025 15:35:33 +0000 Subject: [PATCH 21/37] chore: improve volume monitor test --- coderd/agentapi/resources_monitoring_test.go | 175 +++++++++++++------ 1 file changed, 118 insertions(+), 57 deletions(-) diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index ce38d3374a8e9..ad9213e86a482 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -324,141 +324,197 @@ func TestMemoryResourceMonitor(t *testing.T) { func TestVolumeResourceMonitorDebounce(t *testing.T) { t.Parallel() - // This test is a bit of a long one. We're testing that - // when a monitor goes into an alert state, it doesn't - // allow another notification to occur until after the - // debounce period. + // This test is an even longer one. We're testing + // that the debounce logic is independent per + // volume monitor. We interleave the triggering + // of each monitor to ensure the debounce logic + // is monitor independent. + // + // First Monitor: + // 1. OK -> NOK |> sends a notification + // 2. NOK -> OK |> does nothing + // 3. OK -> NOK |> does nothing due to debounce period + // 4. NOK -> OK |> does nothing + // 5. OK -> NOK |> sends a notification as debounce period exceeded + // 6. NOK -> OK |> does nothing + // + // Second Monitor: + // 1. OK -> OK |> does nothing + // 2. OK -> NOK |> sends a notification + // 3. NOK -> OK |> does nothing + // 4. OK -> NOK |> does nothing due to debounce period + // 5. NOK -> OK |> does nothing + // 6. OK -> NOK |> sends a notification as debounce period exceeded // - // 1. OK -> NOK |> sends a notification - // 2. NOK -> OK |> does nothing - // 3. OK -> NOK |> does nothing due to debounce period - // 4. NOK -> OK |> does nothing - // 5. OK -> NOK |> sends a notification as debounce period exceeded - volumePath := "/home/coder" + firstVolumePath := "/home/coder" + secondVolumePath := "/dev/coder" api, _, clock, notifyEnq := resourceMonitorAPI(t) - // Given: A monitor in an OK state + // Given: + // - First monitor in an OK state + // - Second monitor in an OK state dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ AgentID: api.AgentID, - Path: volumePath, + Path: firstVolumePath, State: database.WorkspaceAgentMonitorStateOK, Threshold: 80, }) + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: secondVolumePath, + State: database.WorkspaceAgentMonitorStateNOK, + Threshold: 80, + }) - // When: The monitor is given a state that will trigger NOK + // When: + // - First monitor is in a NOK state + // - Second monitor is in an OK state _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - { - Path: volumePath, - SpaceUsed: 10, - SpaceTotal: 10, - }, + {Path: firstVolumePath, SpaceUsed: 10, SpaceTotal: 10}, + {Path: secondVolumePath, SpaceUsed: 1, SpaceTotal: 10}, }, }, }, }) require.NoError(t, err) - // Then: We expect there to be a notification sent + // Then: + // - We expect a notification from only the first monitor sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) require.Len(t, sent, 1) + volumes := requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, firstVolumePath, volumes[0]["path"]) notifyEnq.Clear() - // When: The monitor moves to an OK state from NOK + // When: + // - First monitor moves back to OK + // - Second monitor moves to NOK clock.Advance(api.Debounce / 4) _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - { - Path: volumePath, - SpaceUsed: 1, - SpaceTotal: 10, - }, + {Path: firstVolumePath, SpaceUsed: 1, SpaceTotal: 10}, + {Path: secondVolumePath, SpaceUsed: 10, SpaceTotal: 10}, }, }, }, }) require.NoError(t, err) - // Then: We expect no new notifications + // Then: + // - We expect a notification from only the second monitor sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) - require.Len(t, sent, 0) + require.Len(t, sent, 1) + volumes = requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, secondVolumePath, volumes[0]["path"]) notifyEnq.Clear() - // When: The monitor moves back to a NOK state before the debounced time. + // When: + // - First monitor moves back to NOK before debounce period has ended + // - Second monitor moves back to OK clock.Advance(api.Debounce / 4) _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - { - Path: volumePath, - SpaceUsed: 10, - SpaceTotal: 10, - }, + {Path: firstVolumePath, SpaceUsed: 10, SpaceTotal: 10}, + {Path: secondVolumePath, SpaceUsed: 1, SpaceTotal: 10}, }, }, }, }) require.NoError(t, err) - // Then: We expect no new notifications (showing the debouncer working) + // Then: + // - We expect no new notifications sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) require.Len(t, sent, 0) notifyEnq.Clear() - // When: The monitor moves back to an OK state from NOK + // When: + // - First monitor moves back to OK + // - Second monitor moves back to NOK clock.Advance(api.Debounce / 4) _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - { - Path: volumePath, - SpaceUsed: 1, - SpaceTotal: 10, - }, + {Path: firstVolumePath, SpaceUsed: 1, SpaceTotal: 10}, + {Path: secondVolumePath, SpaceUsed: 10, SpaceTotal: 10}, }, }, }, }) require.NoError(t, err) - // Then: We still expect no new notifications + // Then: + // - We expect no new notifications. sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) require.Len(t, sent, 0) notifyEnq.Clear() - // When: The monitor moves back to a NOK state after the debounce period. + // When: + // - First monitor moves back to a NOK state after the debounce period + // - Second monitor moves back to OK clock.Advance(api.Debounce/4 + 1*time.Second) _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - { - Path: volumePath, - SpaceUsed: 10, - SpaceTotal: 10, - }, + {Path: firstVolumePath, SpaceUsed: 10, SpaceTotal: 10}, + {Path: secondVolumePath, SpaceUsed: 1, SpaceTotal: 10}, }, }, }, }) require.NoError(t, err) - // Then: We expect a notification + // Then: + // - We expect a notification from only the first monitor + sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 1) + volumes = requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, firstVolumePath, volumes[0]["path"]) + notifyEnq.Clear() + + // When: + // - First montior moves back to OK + // - Second monitor moves back to NOK after the debounce period + clock.Advance(api.Debounce/4 + 1*time.Second) + _, err = api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + {Path: firstVolumePath, SpaceUsed: 1, SpaceTotal: 10}, + {Path: secondVolumePath, SpaceUsed: 10, SpaceTotal: 10}, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: + // - We expect a notification from only the second monitor sent = notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) require.Len(t, sent, 1) + volumes = requireVolumeData(t, sent[0]) + require.Len(t, volumes, 1) + require.Equal(t, secondVolumePath, volumes[0]["path"]) } func TestVolumeResourceMonitor(t *testing.T) { @@ -623,7 +679,7 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { Threshold: 80, }) - // When: only one of them is in an NOK state. + // When: both of them move to a NOK state _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { @@ -631,7 +687,7 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { Path: "/home/coder", - SpaceUsed: 1, + SpaceUsed: 10, SpaceTotal: 10, }, { @@ -645,16 +701,21 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { }) require.NoError(t, err) - // Then: We expect a notification that contains only the alerting volume. + // Then: We expect a notification to alert with information about both sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) require.Len(t, sent, 1) - volumesData := sent[0].Data["volumes"] - require.IsType(t, []map[string]any{}, volumesData) + volumes := requireVolumeData(t, sent[0]) + require.Len(t, volumes, 2) + require.Equal(t, "/home/coder", volumes[0]["path"]) + require.Equal(t, "/dev/coder", volumes[1]["path"]) +} - volumes := volumesData.([]map[string]any) - require.Len(t, volumes, 1) +func requireVolumeData(t *testing.T, notif *notificationstest.FakeNotification) []map[string]any { + t.Helper() + + volumesData := notif.Data["volumes"] + require.IsType(t, []map[string]any{}, volumesData) - volume := volumes[0] - require.Equal(t, "/dev/coder", volume["path"]) + return volumesData.([]map[string]any) } From 1b0d0d207cf6b4e59abe8e0cba3505a83c24a261 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 11 Feb 2025 14:51:16 +0000 Subject: [PATCH 22/37] chore: rename fields --- coderd/agentapi/api.go | 4 ++-- coderd/agentapi/resources_monitoring.go | 15 ++++++------ coderd/agentapi/resources_monitoring_test.go | 24 ++++++++++---------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 3747106c7fdb3..c1e654ef3269e 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -162,8 +162,8 @@ func New(opts Options) *API { NotificationsEnqueuer: opts.NotificationsEnqueuer, // These values assume a window of 20 - MinimumNOKs: 4, - ConsecutiveNOKs: 10, + MinimumNOKsToAlert: 4, + ConsecutiveNOKsToAlert: 10, } api.DRPCService = &tailnet.DRPCService{ diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 64a89bcce1439..44740772db8cd 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -32,11 +32,11 @@ type ResourcesMonitoringAPI struct { // How many datapoints in a row are required to // put the monitor in an alert state. - ConsecutiveNOKs int + ConsecutiveNOKsToAlert int // How many datapoints in total are required to // put the monitor in an alert state. - MinimumNOKs int + MinimumNOKsToAlert int } func (m *ResourcesMonitoringAPI) PushResourcesMonitoringUsage(ctx context.Context, req *agentproto.PushResourcesMonitoringUsageRequest) (*agentproto.PushResourcesMonitoringUsageResponse, error) { @@ -75,7 +75,7 @@ func (m *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ usageStates := calculateMemoryUsageStates(monitor, usageDatapoints) oldState := monitor.State - newState := m.nextState(oldState, usageStates) + newState := m.calculateNextState(oldState, usageStates) shouldNotify := m.Clock.Now().After(monitor.DebouncedUntil) && oldState == database.WorkspaceAgentMonitorStateOK && @@ -128,7 +128,6 @@ func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints } volumes := make(map[string][]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) - for _, datapoint := range datapoints { for _, volume := range datapoint.Volume { volumeDatapoints := volumes[volume.Path] @@ -147,7 +146,7 @@ func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints usageStates := calculateVolumeUsageStates(monitor, volumes[monitor.Path]) oldState := monitor.State - newState := m.nextState(oldState, usageStates) + newState := m.calculateNextState(oldState, usageStates) shouldNotify := m.Clock.Now().After(monitor.DebouncedUntil) && oldState == database.WorkspaceAgentMonitorStateOK && @@ -200,13 +199,13 @@ func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints return nil } -func (m *ResourcesMonitoringAPI) nextState( +func (m *ResourcesMonitoringAPI) calculateNextState( oldState database.WorkspaceAgentMonitorState, states []database.WorkspaceAgentMonitorState, ) database.WorkspaceAgentMonitorState { // If we do not have an OK in the last `X` datapoints, then we are // in an alert state. - lastXStates := states[max(len(states)-m.ConsecutiveNOKs, 0):] + lastXStates := states[max(len(states)-m.ConsecutiveNOKsToAlert, 0):] if !slices.Contains(lastXStates, database.WorkspaceAgentMonitorStateOK) { return database.WorkspaceAgentMonitorStateNOK } @@ -219,7 +218,7 @@ func (m *ResourcesMonitoringAPI) nextState( } // If there are enough NOK datapoints, we should be in an alert state. - if nokCount >= m.MinimumNOKs { + if nokCount >= m.MinimumNOKsToAlert { return database.WorkspaceAgentMonitorStateNOK } diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index ad9213e86a482..027916d6a2b99 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -58,14 +58,14 @@ func resourceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, databas clock := quartz.NewMock(t) return &agentapi.ResourcesMonitoringAPI{ - AgentID: agent.ID, - WorkspaceID: workspace.ID, - Clock: clock, - Database: db, - NotificationsEnqueuer: notifyEnq, - MinimumNOKs: 4, - ConsecutiveNOKs: 10, - Debounce: 1 * time.Minute, + AgentID: agent.ID, + WorkspaceID: workspace.ID, + Clock: clock, + Database: db, + NotificationsEnqueuer: notifyEnq, + MinimumNOKsToAlert: 4, + ConsecutiveNOKsToAlert: 10, + Debounce: 1 * time.Minute, }, user, clock, notifyEnq } @@ -282,8 +282,8 @@ func TestMemoryResourceMonitor(t *testing.T) { t.Parallel() api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.MinimumNOKs = tt.minimumNOKs - api.ConsecutiveNOKs = tt.consecutiveNOKs + api.MinimumNOKsToAlert = tt.minimumNOKs + api.ConsecutiveNOKsToAlert = tt.consecutiveNOKs datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.memoryUsage)) collectedAt := clock.Now() @@ -613,8 +613,8 @@ func TestVolumeResourceMonitor(t *testing.T) { t.Parallel() api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.MinimumNOKs = tt.minimumNOKs - api.ConsecutiveNOKs = tt.consecutiveNOKs + api.MinimumNOKsToAlert = tt.minimumNOKs + api.ConsecutiveNOKsToAlert = tt.consecutiveNOKs datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.volumeUsage)) collectedAt := clock.Now() From 4e43bab3427b9cc4015fe1662919eae3b1e90e6f Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 11 Feb 2025 15:27:01 +0000 Subject: [PATCH 23/37] chore: align with other branch --- agent/proto/agent.pb.go | 269 +++++++++---------- agent/proto/agent.proto | 13 +- coderd/agentapi/resources_monitoring.go | 6 +- coderd/agentapi/resources_monitoring_test.go | 42 +-- 4 files changed, 165 insertions(+), 165 deletions(-) diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 7cc46f89ff7c5..56dfe6190f42f 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -2846,18 +2846,17 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetVolume() []*PushResou return nil } -type PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage struct { +type PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Path string `protobuf:"bytes,1,opt,name=path,proto3" json:"path,omitempty"` - SpaceUsed int64 `protobuf:"varint,2,opt,name=space_used,json=spaceUsed,proto3" json:"space_used,omitempty"` - SpaceTotal int64 `protobuf:"varint,3,opt,name=space_total,json=spaceTotal,proto3" json:"space_total,omitempty"` + Used int64 `protobuf:"varint,1,opt,name=used,proto3" json:"used,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Reset() { - *x = PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{} +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2865,13 +2864,13 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Reset() { } } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) String() string { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoMessage() {} +func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoMessage() {} -func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2883,43 +2882,37 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoReflect return mi.MessageOf(x) } -// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage.ProtoReflect.Descriptor instead. -func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Descriptor() ([]byte, []int) { +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0, 0} } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetPath() string { - if x != nil { - return x.Path - } - return "" -} - -func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetSpaceUsed() int64 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetUsed() int64 { if x != nil { - return x.SpaceUsed + return x.Used } return 0 } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetSpaceTotal() int64 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetTotal() int64 { if x != nil { - return x.SpaceTotal + return x.Total } return 0 } -type PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage struct { +type PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Used int64 `protobuf:"varint,1,opt,name=used,proto3" json:"used,omitempty"` - Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + Volume string `protobuf:"bytes,1,opt,name=volume,proto3" json:"volume,omitempty"` + Used int64 `protobuf:"varint,2,opt,name=used,proto3" json:"used,omitempty"` + Total int64 `protobuf:"varint,3,opt,name=total,proto3" json:"total,omitempty"` } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Reset() { - *x = PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{} +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Reset() { + *x = PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{} if protoimpl.UnsafeEnabled { mi := &file_agent_proto_agent_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2927,13 +2920,13 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Reset() { } } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) String() string { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoMessage() {} +func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoMessage() {} -func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoReflect() protoreflect.Message { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_agent_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2945,19 +2938,26 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) ProtoReflect return mi.MessageOf(x) } -// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage.ProtoReflect.Descriptor instead. -func (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) Descriptor() ([]byte, []int) { +// Deprecated: Use PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage.ProtoReflect.Descriptor instead. +func (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) Descriptor() ([]byte, []int) { return file_agent_proto_agent_proto_rawDescGZIP(), []int{28, 0, 1} } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetUsed() int64 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetVolume() string { + if x != nil { + return x.Volume + } + return "" +} + +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetUsed() int64 { if x != nil { return x.Used } return 0 } -func (x *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage) GetTotal() int64 { +func (x *PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) GetTotal() int64 { if x != nil { return x.Total } @@ -3358,7 +3358,7 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, - 0x4e, 0x10, 0x03, 0x22, 0xb3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, + 0x4e, 0x10, 0x03, 0x22, 0xa1, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, @@ -3366,7 +3366,7 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, - 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0xac, 0x03, 0x0a, 0x09, 0x44, + 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x9a, 0x03, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, @@ -3383,101 +3383,100 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x1a, 0x61, 0x0a, - 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x73, 0x70, 0x61, 0x63, 0x65, 0x55, 0x73, 0x65, 0x64, 0x12, - 0x1f, 0x0a, 0x0b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, - 0x1a, 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, - 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, 0x73, - 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, - 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, - 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, - 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, - 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, - 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, - 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xfb, 0x08, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, - 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, - 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, - 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, - 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, - 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, - 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, - 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, - 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, - 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, - 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, - 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, - 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, - 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, - 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, - 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x1a, 0x37, 0x0a, + 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, + 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, - 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, + 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, + 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, + 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, + 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, + 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, + 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, + 0x48, 0x59, 0x10, 0x04, 0x32, 0xfb, 0x08, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, + 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, + 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, + 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, + 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, + 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, + 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, + 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, - 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, - 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, + 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, + 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, + 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, + 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, 0x68, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, + 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, + 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, + 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, + 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -3543,8 +3542,8 @@ var file_agent_proto_agent_proto_goTypes = []interface{}{ (*Stats_Metric_Label)(nil), // 45: coder.agent.v2.Stats.Metric.Label (*BatchUpdateAppHealthRequest_HealthUpdate)(nil), // 46: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate (*PushResourcesMonitoringUsageRequest_Datapoint)(nil), // 47: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint - (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 48: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage - (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 49: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + (*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage)(nil), // 48: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + (*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage)(nil), // 49: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage (*durationpb.Duration)(nil), // 50: google.protobuf.Duration (*proto.DERPMap)(nil), // 51: coder.tailnet.v2.DERPMap (*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp @@ -3591,8 +3590,8 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 45, // 38: coder.agent.v2.Stats.Metric.labels:type_name -> coder.agent.v2.Stats.Metric.Label 0, // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth 52, // 40: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp - 49, // 41: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage - 48, // 42: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volume:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + 48, // 41: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage + 49, // 42: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volume:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage 13, // 43: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest 15, // 44: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest 17, // 45: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest @@ -4073,7 +4072,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage); i { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage); i { case 0: return &v.state case 1: @@ -4085,7 +4084,7 @@ func file_agent_proto_agent_proto_init() { } } file_agent_proto_agent_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage); i { + switch v := v.(*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage); i { case 0: return &v.state case 1: diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index 8fc83c88956e7..db2cb4dc05d0b 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -297,17 +297,18 @@ message Timing { message PushResourcesMonitoringUsageRequest { message Datapoint { - message VolumeUsage { - string path = 1; - int64 space_used = 2; - int64 space_total = 3; - } - message MemoryUsage { int64 used = 1; int64 total = 2; } + message VolumeUsage { + string volume = 1; + int64 used = 2; + int64 total = 3; + } + + google.protobuf.Timestamp collected_at = 1; MemoryUsage memory = 2; repeated VolumeUsage volume = 3; diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 44740772db8cd..91545a02b3d30 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -130,9 +130,9 @@ func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints volumes := make(map[string][]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) for _, datapoint := range datapoints { for _, volume := range datapoint.Volume { - volumeDatapoints := volumes[volume.Path] + volumeDatapoints := volumes[volume.Volume] volumeDatapoints = append(volumeDatapoints, volume) - volumes[volume.Path] = volumeDatapoints + volumes[volume.Volume] = volumeDatapoints } } @@ -258,7 +258,7 @@ func calculateVolumeUsageStates( states := make([]database.WorkspaceAgentMonitorState, 0, len(datapoints)) for _, datapoint := range datapoints { - percent := int32(float64(datapoint.SpaceUsed) / float64(datapoint.SpaceTotal) * 100) + percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) state := database.WorkspaceAgentMonitorStateOK if percent >= monitor.Threshold { diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 027916d6a2b99..44d5a031f348a 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -376,8 +376,8 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - {Path: firstVolumePath, SpaceUsed: 10, SpaceTotal: 10}, - {Path: secondVolumePath, SpaceUsed: 1, SpaceTotal: 10}, + {Volume: firstVolumePath, Used: 10, Total: 10}, + {Volume: secondVolumePath, Used: 1, Total: 10}, }, }, }, @@ -402,8 +402,8 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - {Path: firstVolumePath, SpaceUsed: 1, SpaceTotal: 10}, - {Path: secondVolumePath, SpaceUsed: 10, SpaceTotal: 10}, + {Volume: firstVolumePath, Used: 1, Total: 10}, + {Volume: secondVolumePath, Used: 10, Total: 10}, }, }, }, @@ -428,8 +428,8 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - {Path: firstVolumePath, SpaceUsed: 10, SpaceTotal: 10}, - {Path: secondVolumePath, SpaceUsed: 1, SpaceTotal: 10}, + {Volume: firstVolumePath, Used: 10, Total: 10}, + {Volume: secondVolumePath, Used: 1, Total: 10}, }, }, }, @@ -451,8 +451,8 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - {Path: firstVolumePath, SpaceUsed: 1, SpaceTotal: 10}, - {Path: secondVolumePath, SpaceUsed: 10, SpaceTotal: 10}, + {Volume: firstVolumePath, Used: 1, Total: 10}, + {Volume: secondVolumePath, Used: 10, Total: 10}, }, }, }, @@ -474,8 +474,8 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - {Path: firstVolumePath, SpaceUsed: 10, SpaceTotal: 10}, - {Path: secondVolumePath, SpaceUsed: 1, SpaceTotal: 10}, + {Volume: firstVolumePath, Used: 10, Total: 10}, + {Volume: secondVolumePath, Used: 1, Total: 10}, }, }, }, @@ -500,8 +500,8 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ - {Path: firstVolumePath, SpaceUsed: 1, SpaceTotal: 10}, - {Path: secondVolumePath, SpaceUsed: 10, SpaceTotal: 10}, + {Volume: firstVolumePath, Used: 1, Total: 10}, + {Volume: secondVolumePath, Used: 10, Total: 10}, }, }, }, @@ -623,9 +623,9 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeDatapoints := []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: tt.volumePath, - SpaceUsed: volumeUsage, - SpaceTotal: tt.volumeTotal, + Volume: tt.volumePath, + Used: volumeUsage, + Total: tt.volumeTotal, }, } @@ -686,14 +686,14 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { CollectedAt: timestamppb.New(clock.Now()), Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { - Path: "/home/coder", - SpaceUsed: 10, - SpaceTotal: 10, + Volume: "/home/coder", + Used: 10, + Total: 10, }, { - Path: "/dev/coder", - SpaceUsed: 10, - SpaceTotal: 10, + Volume: "/dev/coder", + Used: 10, + Total: 10, }, }, }, From da25ecccd508abfa7ae55fdb89148a6c4fbf5282 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 12 Feb 2025 11:34:50 +0000 Subject: [PATCH 24/37] chore: bump migration number --- ...monitors.down.sql => 000292_workspace_monitors_state.down.sql} | 0 ...ace_monitors.up.sql => 000292_workspace_monitors_state.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000291_create_workspace_monitors.down.sql => 000292_workspace_monitors_state.down.sql} (100%) rename coderd/database/migrations/{000291_create_workspace_monitors.up.sql => 000292_workspace_monitors_state.up.sql} (100%) diff --git a/coderd/database/migrations/000291_create_workspace_monitors.down.sql b/coderd/database/migrations/000292_workspace_monitors_state.down.sql similarity index 100% rename from coderd/database/migrations/000291_create_workspace_monitors.down.sql rename to coderd/database/migrations/000292_workspace_monitors_state.down.sql diff --git a/coderd/database/migrations/000291_create_workspace_monitors.up.sql b/coderd/database/migrations/000292_workspace_monitors_state.up.sql similarity index 100% rename from coderd/database/migrations/000291_create_workspace_monitors.up.sql rename to coderd/database/migrations/000292_workspace_monitors_state.up.sql From fe1e8051a6bff78ca429cebb01a8de227312fa34 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 12 Feb 2025 12:01:27 +0000 Subject: [PATCH 25/37] chore: add test and align better --- agent/proto/agent.pb.go | 208 +++++++++---------- agent/proto/agent.proto | 4 +- coderd/agentapi/resources_monitoring.go | 65 +++--- coderd/agentapi/resources_monitoring_test.go | 62 +++++- 4 files changed, 199 insertions(+), 140 deletions(-) diff --git a/agent/proto/agent.pb.go b/agent/proto/agent.pb.go index 56dfe6190f42f..f16e67640df9c 100644 --- a/agent/proto/agent.pb.go +++ b/agent/proto/agent.pb.go @@ -2790,7 +2790,7 @@ type PushResourcesMonitoringUsageRequest_Datapoint struct { CollectedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=collected_at,json=collectedAt,proto3" json:"collected_at,omitempty"` Memory *PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage `protobuf:"bytes,2,opt,name=memory,proto3" json:"memory,omitempty"` - Volume []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage `protobuf:"bytes,3,rep,name=volume,proto3" json:"volume,omitempty"` + Volumes []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage `protobuf:"bytes,3,rep,name=volumes,proto3" json:"volumes,omitempty"` } func (x *PushResourcesMonitoringUsageRequest_Datapoint) Reset() { @@ -2839,9 +2839,9 @@ func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetMemory() *PushResourc return nil } -func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetVolume() []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage { +func (x *PushResourcesMonitoringUsageRequest_Datapoint) GetVolumes() []*PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage { if x != nil { - return x.Volume + return x.Volumes } return nil } @@ -3358,7 +3358,7 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x0c, 0x45, 0x58, 0x49, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x49, 0x4d, 0x45, 0x44, 0x5f, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x49, 0x50, 0x45, 0x53, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x5f, 0x4f, 0x50, 0x45, - 0x4e, 0x10, 0x03, 0x22, 0xa1, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, + 0x4e, 0x10, 0x03, 0x22, 0xa3, 0x04, 0x0a, 0x23, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, @@ -3366,7 +3366,7 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0a, - 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x9a, 0x03, 0x0a, 0x09, 0x44, + 0x64, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x9c, 0x03, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, @@ -3377,106 +3377,106 @@ var file_agent_proto_agent_proto_rawDesc = []byte{ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, - 0x67, 0x65, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x61, 0x0a, 0x06, 0x76, 0x6f, - 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, - 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, - 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x1a, 0x37, 0x0a, - 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, 0x65, - 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, 0x73, 0x68, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, - 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, - 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x16, - 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, 0x53, 0x41, - 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, 0x49, 0x41, - 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, 0x41, 0x4c, - 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, 0x4c, 0x54, - 0x48, 0x59, 0x10, 0x04, 0x32, 0xfb, 0x08, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x4b, - 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x22, 0x2e, - 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, - 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x47, - 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, - 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, 0x63, - 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, 0x79, - 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x66, 0x65, - 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, 0x12, 0x2b, + 0x67, 0x65, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x63, 0x0a, 0x07, 0x76, 0x6f, + 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, + 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x2e, 0x44, 0x61, 0x74, 0x61, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x1a, + 0x37, 0x0a, 0x0b, 0x4d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, 0x73, + 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x4f, 0x0a, 0x0b, 0x56, 0x6f, 0x6c, 0x75, + 0x6d, 0x65, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0x26, 0x0a, 0x24, 0x50, 0x75, 0x73, + 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x2a, 0x63, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x1a, + 0x0a, 0x16, 0x41, 0x50, 0x50, 0x5f, 0x48, 0x45, 0x41, 0x4c, 0x54, 0x48, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x44, 0x49, + 0x53, 0x41, 0x42, 0x4c, 0x45, 0x44, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x4e, 0x49, 0x54, + 0x49, 0x41, 0x4c, 0x49, 0x5a, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x45, + 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x48, 0x45, 0x41, + 0x4c, 0x54, 0x48, 0x59, 0x10, 0x04, 0x32, 0xfb, 0x08, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, + 0x12, 0x4b, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, + 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, + 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, + 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, + 0x72, 0x12, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, + 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x6f, 0x64, + 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x56, 0x0a, 0x0b, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x54, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x63, + 0x79, 0x63, 0x6c, 0x65, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x66, 0x65, + 0x63, 0x79, 0x63, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, + 0x66, 0x65, 0x63, 0x79, 0x63, 0x6c, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x42, 0x61, 0x74, 0x63, 0x68, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x73, + 0x12, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, + 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, + 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, + 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, + 0x6c, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, + 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, - 0x61, 0x6c, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, - 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x48, 0x65, 0x61, 0x6c, 0x74, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0d, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x64, - 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x17, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x12, 0x6e, 0x0a, 0x13, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, - 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, 0x61, 0x74, - 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, - 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x77, 0x0a, - 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, - 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, - 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x64, 0x65, - 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, - 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, - 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, 0x68, 0x52, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x42, + 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x26, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x77, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, + 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x6e, 0x6e, + 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7e, 0x0a, 0x0f, 0x53, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x34, 0x2e, 0x63, 0x6f, + 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, + 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x76, 0x32, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x89, 0x01, 0x0a, 0x1c, 0x50, 0x75, 0x73, + 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, + 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, + 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, - 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, - 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, - 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x75, - 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, 0x69, 0x74, - 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, + 0x2e, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x32, 0x2e, + 0x50, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x4d, 0x6f, 0x6e, + 0x69, 0x74, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, + 0x32, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3591,7 +3591,7 @@ var file_agent_proto_agent_proto_depIdxs = []int32{ 0, // 39: coder.agent.v2.BatchUpdateAppHealthRequest.HealthUpdate.health:type_name -> coder.agent.v2.AppHealth 52, // 40: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.collected_at:type_name -> google.protobuf.Timestamp 48, // 41: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.memory:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.MemoryUsage - 49, // 42: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volume:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage + 49, // 42: coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.volumes:type_name -> coder.agent.v2.PushResourcesMonitoringUsageRequest.Datapoint.VolumeUsage 13, // 43: coder.agent.v2.Agent.GetManifest:input_type -> coder.agent.v2.GetManifestRequest 15, // 44: coder.agent.v2.Agent.GetServiceBanner:input_type -> coder.agent.v2.GetServiceBannerRequest 17, // 45: coder.agent.v2.Agent.UpdateStats:input_type -> coder.agent.v2.UpdateStatsRequest diff --git a/agent/proto/agent.proto b/agent/proto/agent.proto index db2cb4dc05d0b..22181a2a3e4c9 100644 --- a/agent/proto/agent.proto +++ b/agent/proto/agent.proto @@ -301,17 +301,15 @@ message PushResourcesMonitoringUsageRequest { int64 used = 1; int64 total = 2; } - message VolumeUsage { string volume = 1; int64 used = 2; int64 total = 3; } - google.protobuf.Timestamp collected_at = 1; MemoryUsage memory = 2; - repeated VolumeUsage volume = 3; + repeated VolumeUsage volumes = 3; } repeated Datapoint datapoints = 1; diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 91545a02b3d30..aa4c20938c29c 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -8,6 +8,7 @@ import ( "slices" "time" + "cdr.dev/slog" "golang.org/x/xerrors" "github.com/google/uuid" @@ -20,10 +21,19 @@ import ( "github.com/coder/quartz" ) +type VolumeNotFoundError struct { + Volume string +} + +func (e VolumeNotFoundError) Error() string { + return fmt.Sprintf("volume not found: `%s`", e.Volume) +} + type ResourcesMonitoringAPI struct { AgentID uuid.UUID WorkspaceID uuid.UUID + Log slog.Logger Clock quartz.Clock Database database.Store NotificationsEnqueuer notifications.Enqueuer @@ -39,20 +49,20 @@ type ResourcesMonitoringAPI struct { MinimumNOKsToAlert int } -func (m *ResourcesMonitoringAPI) PushResourcesMonitoringUsage(ctx context.Context, req *agentproto.PushResourcesMonitoringUsageRequest) (*agentproto.PushResourcesMonitoringUsageResponse, error) { - if err := m.monitorMemory(ctx, req.Datapoints); err != nil { +func (a *ResourcesMonitoringAPI) PushResourcesMonitoringUsage(ctx context.Context, req *agentproto.PushResourcesMonitoringUsageRequest) (*agentproto.PushResourcesMonitoringUsageResponse, error) { + if err := a.monitorMemory(ctx, req.Datapoints); err != nil { return nil, xerrors.Errorf("monitor memory: %w", err) } - if err := m.monitorVolumes(ctx, req.Datapoints); err != nil { + if err := a.monitorVolumes(ctx, req.Datapoints); err != nil { return nil, xerrors.Errorf("monitor volumes: %w", err) } return &agentproto.PushResourcesMonitoringUsageResponse{}, nil } -func (m *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint) error { - monitor, err := m.Database.FetchMemoryResourceMonitorsByAgentID(ctx, m.AgentID) +func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint) error { + monitor, err := a.Database.FetchMemoryResourceMonitorsByAgentID(ctx, a.AgentID) if err != nil { // It is valid for an agent to not have a memory monitor, so we // do not want to treat it as an error. @@ -75,21 +85,21 @@ func (m *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ usageStates := calculateMemoryUsageStates(monitor, usageDatapoints) oldState := monitor.State - newState := m.calculateNextState(oldState, usageStates) + newState := a.calculateNextState(oldState, usageStates) - shouldNotify := m.Clock.Now().After(monitor.DebouncedUntil) && + shouldNotify := a.Clock.Now().After(monitor.DebouncedUntil) && oldState == database.WorkspaceAgentMonitorStateOK && newState == database.WorkspaceAgentMonitorStateNOK debouncedUntil := monitor.DebouncedUntil if shouldNotify { - debouncedUntil = m.Clock.Now().Add(m.Debounce) + debouncedUntil = a.Clock.Now().Add(a.Debounce) } - err = m.Database.UpdateMemoryResourceMonitor(ctx, database.UpdateMemoryResourceMonitorParams{ - AgentID: m.AgentID, + err = a.Database.UpdateMemoryResourceMonitor(ctx, database.UpdateMemoryResourceMonitorParams{ + AgentID: a.AgentID, State: newState, - UpdatedAt: dbtime.Time(m.Clock.Now()), + UpdatedAt: dbtime.Time(a.Clock.Now()), DebouncedUntil: dbtime.Time(debouncedUntil), }) if err != nil { @@ -97,12 +107,12 @@ func (m *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ } if shouldNotify { - workspace, err := m.Database.GetWorkspaceByID(ctx, m.WorkspaceID) + workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) if err != nil { return xerrors.Errorf("get workspace by id: %w", err) } - _, err = m.NotificationsEnqueuer.Enqueue( + _, err = a.NotificationsEnqueuer.Enqueue( // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, @@ -121,15 +131,15 @@ func (m *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ return nil } -func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint) error { - volumeMonitors, err := m.Database.FetchVolumesResourceMonitorsByAgentID(ctx, m.AgentID) +func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint) error { + volumeMonitors, err := a.Database.FetchVolumesResourceMonitorsByAgentID(ctx, a.AgentID) if err != nil { return xerrors.Errorf("get or insert volume monitor: %w", err) } volumes := make(map[string][]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) for _, datapoint := range datapoints { - for _, volume := range datapoint.Volume { + for _, volume := range datapoint.Volumes { volumeDatapoints := volumes[volume.Volume] volumeDatapoints = append(volumeDatapoints, volume) volumes[volume.Volume] = volumeDatapoints @@ -143,18 +153,23 @@ func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints continue } - usageStates := calculateVolumeUsageStates(monitor, volumes[monitor.Path]) + datapoints, found := volumes[monitor.Path] + if !found { + return VolumeNotFoundError{Volume: monitor.Path} + } + + usageStates := calculateVolumeUsageStates(monitor, datapoints) oldState := monitor.State - newState := m.calculateNextState(oldState, usageStates) + newState := a.calculateNextState(oldState, usageStates) - shouldNotify := m.Clock.Now().After(monitor.DebouncedUntil) && + shouldNotify := a.Clock.Now().After(monitor.DebouncedUntil) && oldState == database.WorkspaceAgentMonitorStateOK && newState == database.WorkspaceAgentMonitorStateNOK debouncedUntil := monitor.DebouncedUntil if shouldNotify { - debouncedUntil = m.Clock.Now().Add(m.Debounce) + debouncedUntil = a.Clock.Now().Add(a.Debounce) outOfDiskVolumes = append(outOfDiskVolumes, map[string]any{ "path": monitor.Path, @@ -162,11 +177,11 @@ func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints }) } - if err := m.Database.UpdateVolumeResourceMonitor(ctx, database.UpdateVolumeResourceMonitorParams{ - AgentID: m.AgentID, + if err := a.Database.UpdateVolumeResourceMonitor(ctx, database.UpdateVolumeResourceMonitorParams{ + AgentID: a.AgentID, Path: monitor.Path, State: newState, - UpdatedAt: dbtime.Time(m.Clock.Now()), + UpdatedAt: dbtime.Time(a.Clock.Now()), DebouncedUntil: dbtime.Time(debouncedUntil), }); err != nil { return xerrors.Errorf("update workspace monitor: %w", err) @@ -174,12 +189,12 @@ func (m *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints } if len(outOfDiskVolumes) != 0 { - workspace, err := m.Database.GetWorkspaceByID(ctx, m.WorkspaceID) + workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) if err != nil { return xerrors.Errorf("get workspace by id: %w", err) } - if _, err := m.NotificationsEnqueuer.EnqueueWithData( + if _, err := a.NotificationsEnqueuer.EnqueueWithData( // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 44d5a031f348a..5bc29d2c36934 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -218,6 +218,17 @@ func TestMemoryResourceMonitor(t *testing.T) { expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, + { + name: "WhenOK/ShouldStayInOK", + memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, { name: "WhenOK/ConsecutiveExceedsThreshold", memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, @@ -251,6 +262,17 @@ func TestMemoryResourceMonitor(t *testing.T) { expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, + { + name: "WhenNOK/ShouldStayInNOK", + memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, { name: "WhenNOK/ConsecutiveExceedsThreshold", memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, @@ -375,7 +397,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ {Volume: firstVolumePath, Used: 10, Total: 10}, {Volume: secondVolumePath, Used: 1, Total: 10}, }, @@ -401,7 +423,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ {Volume: firstVolumePath, Used: 1, Total: 10}, {Volume: secondVolumePath, Used: 10, Total: 10}, }, @@ -427,7 +449,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ {Volume: firstVolumePath, Used: 10, Total: 10}, {Volume: secondVolumePath, Used: 1, Total: 10}, }, @@ -450,7 +472,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ {Volume: firstVolumePath, Used: 1, Total: 10}, {Volume: secondVolumePath, Used: 10, Total: 10}, }, @@ -473,7 +495,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ {Volume: firstVolumePath, Used: 10, Total: 10}, {Volume: secondVolumePath, Used: 1, Total: 10}, }, @@ -499,7 +521,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ {Volume: firstVolumePath, Used: 1, Total: 10}, {Volume: secondVolumePath, Used: 10, Total: 10}, }, @@ -544,6 +566,18 @@ func TestVolumeResourceMonitor(t *testing.T) { expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, + { + name: "WhenOK/ShouldStayInOK", + volumePath: "/home/coder", + volumeUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, + }, { name: "WhenOK/ConsecutiveExceedsThreshold", volumePath: "/home/coder", @@ -580,6 +614,18 @@ func TestVolumeResourceMonitor(t *testing.T) { expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, }, + { + name: "WhenNOK/ShouldStayInNOK", + volumePath: "/home/coder", + volumeUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + volumeTotal: 10, + thresholdPercent: 80, + consecutiveNOKs: 4, + minimumNOKs: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, + }, { name: "WhenNOK/ConsecutiveExceedsThreshold", volumePath: "/home/coder", @@ -631,7 +677,7 @@ func TestVolumeResourceMonitor(t *testing.T) { datapoints = append(datapoints, &agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ CollectedAt: timestamppb.New(collectedAt), - Volume: volumeDatapoints, + Volumes: volumeDatapoints, }) } @@ -684,7 +730,7 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ { CollectedAt: timestamppb.New(clock.Now()), - Volume: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ { Volume: "/home/coder", Used: 10, From abbd5221cad53a674eba4406c3a92e06fa715530 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 12 Feb 2025 12:12:42 +0000 Subject: [PATCH 26/37] chore: appease linter --- coderd/agentapi/resources_monitoring.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index aa4c20938c29c..5d4575645a9b8 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -8,9 +8,10 @@ import ( "slices" "time" - "cdr.dev/slog" "golang.org/x/xerrors" + "cdr.dev/slog" + "github.com/google/uuid" agentproto "github.com/coder/coder/v2/agent/proto" @@ -214,13 +215,13 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints return nil } -func (m *ResourcesMonitoringAPI) calculateNextState( +func (a *ResourcesMonitoringAPI) calculateNextState( oldState database.WorkspaceAgentMonitorState, states []database.WorkspaceAgentMonitorState, ) database.WorkspaceAgentMonitorState { // If we do not have an OK in the last `X` datapoints, then we are // in an alert state. - lastXStates := states[max(len(states)-m.ConsecutiveNOKsToAlert, 0):] + lastXStates := states[max(len(states)-a.ConsecutiveNOKsToAlert, 0):] if !slices.Contains(lastXStates, database.WorkspaceAgentMonitorStateOK) { return database.WorkspaceAgentMonitorStateNOK } @@ -233,7 +234,7 @@ func (m *ResourcesMonitoringAPI) calculateNextState( } // If there are enough NOK datapoints, we should be in an alert state. - if nokCount >= m.MinimumNOKsToAlert { + if nokCount >= a.MinimumNOKsToAlert { return database.WorkspaceAgentMonitorStateNOK } From 1550cc67322cd6521f957c2b0b798417e7b923a0 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 14 Feb 2025 11:11:05 +0000 Subject: [PATCH 27/37] chore: update rbac --- coderd/agentapi/resources_monitoring.go | 4 ++-- coderd/database/dbauthz/dbauthz.go | 24 ++++++++++++++++++++++++ coderd/rbac/object_gen.go | 1 + coderd/rbac/policy/policy.go | 1 + coderd/rbac/roles_test.go | 2 +- codersdk/rbacresources_gen.go | 2 +- site/src/api/rbacresourcesGenerated.ts | 1 + 7 files changed, 31 insertions(+), 4 deletions(-) diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index e97e7aaa3541b..b93216d1325d3 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -136,7 +136,7 @@ func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ debouncedUntil = a.Clock.Now().Add(a.Debounce) } - err = a.Database.UpdateMemoryResourceMonitor(ctx, database.UpdateMemoryResourceMonitorParams{ + err = a.Database.UpdateMemoryResourceMonitor(dbauthz.AsResourceMonitor(ctx), database.UpdateMemoryResourceMonitorParams{ AgentID: a.AgentID, State: newState, UpdatedAt: dbtime.Time(a.Clock.Now()), @@ -217,7 +217,7 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints }) } - if err := a.Database.UpdateVolumeResourceMonitor(ctx, database.UpdateVolumeResourceMonitorParams{ + if err := a.Database.UpdateVolumeResourceMonitor(dbauthz.AsResourceMonitor(ctx), database.UpdateVolumeResourceMonitorParams{ AgentID: a.AgentID, Path: monitor.Path, State: newState, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index c85181b6c2aba..9e616dd79dcbc 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -289,6 +289,24 @@ var ( Scope: rbac.ScopeAll, }.WithCachedASTValue() + subjectResourceMonitor = rbac.Subject{ + FriendlyName: "Resource Monitor", + ID: uuid.Nil.String(), + Roles: rbac.Roles([]rbac.Role{ + { + Identifier: rbac.RoleIdentifier{Name: "resourcemonitor"}, + DisplayName: "Resource Monitor", + Site: rbac.Permissions(map[string][]policy.Action{ + // The workspace monitor needs to be able to update monitors + rbac.ResourceWorkspaceAgentResourceMonitor.Type: {policy.ActionUpdate}, + }), + Org: map[string][]rbac.Permission{}, + User: []rbac.Permission{}, + }, + }), + Scope: rbac.ScopeAll, + }.WithCachedASTValue() + subjectSystemRestricted = rbac.Subject{ FriendlyName: "System", ID: uuid.Nil.String(), @@ -376,6 +394,12 @@ func AsNotifier(ctx context.Context) context.Context { return context.WithValue(ctx, authContextKey{}, subjectNotifier) } +// AsResourceMonitor returns a context with an actor that has permissions required for +// updating resource monitors. +func AsResourceMonitor(ctx context.Context) context.Context { + return context.WithValue(ctx, authContextKey{}, subjectResourceMonitor) +} + // AsSystemRestricted returns a context with an actor that has permissions // required for various system operations (login, logout, metrics cache). func AsSystemRestricted(ctx context.Context) context.Context { diff --git a/coderd/rbac/object_gen.go b/coderd/rbac/object_gen.go index 547e10859b5b7..e5323225120b5 100644 --- a/coderd/rbac/object_gen.go +++ b/coderd/rbac/object_gen.go @@ -299,6 +299,7 @@ var ( // Valid Actions // - "ActionCreate" :: create workspace agent resource monitor // - "ActionRead" :: read workspace agent resource monitor + // - "ActionUpdate" :: update workspace agent resource monitor ResourceWorkspaceAgentResourceMonitor = Object{ Type: "workspace_agent_resource_monitor", } diff --git a/coderd/rbac/policy/policy.go b/coderd/rbac/policy/policy.go index 6dc64f6660248..c06a2117cb4e9 100644 --- a/coderd/rbac/policy/policy.go +++ b/coderd/rbac/policy/policy.go @@ -306,6 +306,7 @@ var RBACPermissions = map[string]PermissionDefinition{ Actions: map[Action]ActionDefinition{ ActionRead: actDef("read workspace agent resource monitor"), ActionCreate: actDef("create workspace agent resource monitor"), + ActionUpdate: actDef("update workspace agent resource monitor"), }, }, } diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 6db591d028454..db0d9832579fc 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -779,7 +779,7 @@ func TestRolePermissions(t *testing.T) { }, { Name: "ResourceMonitor", - Actions: []policy.Action{policy.ActionRead, policy.ActionCreate}, + Actions: []policy.Action{policy.ActionRead, policy.ActionCreate, policy.ActionUpdate}, Resource: rbac.ResourceWorkspaceAgentResourceMonitor, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, diff --git a/codersdk/rbacresources_gen.go b/codersdk/rbacresources_gen.go index 8afb1858ca15c..f4d7790d40b76 100644 --- a/codersdk/rbacresources_gen.go +++ b/codersdk/rbacresources_gen.go @@ -92,7 +92,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{ ResourceTemplate: {ActionCreate, ActionDelete, ActionRead, ActionUpdate, ActionUse, ActionViewInsights}, ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal}, ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate}, - ResourceWorkspaceAgentResourceMonitor: {ActionCreate, ActionRead}, + ResourceWorkspaceAgentResourceMonitor: {ActionCreate, ActionRead, ActionUpdate}, ResourceWorkspaceDormant: {ActionApplicationConnect, ActionCreate, ActionDelete, ActionRead, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate}, ResourceWorkspaceProxy: {ActionCreate, ActionDelete, ActionRead, ActionUpdate}, } diff --git a/site/src/api/rbacresourcesGenerated.ts b/site/src/api/rbacresourcesGenerated.ts index e557ceddbdda6..437f89ec776a7 100644 --- a/site/src/api/rbacresourcesGenerated.ts +++ b/site/src/api/rbacresourcesGenerated.ts @@ -171,6 +171,7 @@ export const RBACResourceActions: Partial< workspace_agent_resource_monitor: { create: "create workspace agent resource monitor", read: "read workspace agent resource monitor", + update: "update workspace agent resource monitor", }, workspace_dormant: { application_connect: "connect to workspace apps via browser", From 7998f89e5a6d31bce78fe13e044561577257edf0 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 14 Feb 2025 11:43:47 +0000 Subject: [PATCH 28/37] chore: handle missing datapoints --- coderd/agentapi/resources_monitoring.go | 69 ++------- coderd/agentapi/resources_monitoring_test.go | 144 ++++++++---------- .../resourcesmonitor/resources_monitor.go | 80 ++++++++++ 3 files changed, 156 insertions(+), 137 deletions(-) create mode 100644 coderd/agentapi/resourcesmonitor/resources_monitor.go diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index b93216d1325d3..8b8e027500935 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "fmt" - "slices" "time" "golang.org/x/xerrors" @@ -15,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -122,7 +122,7 @@ func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ usageDatapoints = append(usageDatapoints, datapoint.Memory) } - usageStates := calculateMemoryUsageStates(monitor, usageDatapoints) + usageStates := resourcesmonitor.CalculateMemoryUsageStates(monitor, usageDatapoints) oldState := monitor.State newState := a.calculateNextState(oldState, usageStates) @@ -198,7 +198,7 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints return VolumeNotFoundError{Volume: monitor.Path} } - usageStates := calculateVolumeUsageStates(monitor, datapoints) + usageStates := resourcesmonitor.CalculateVolumeUsageStates(monitor, datapoints) oldState := monitor.State newState := a.calculateNextState(oldState, usageStates) @@ -256,19 +256,22 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints func (a *ResourcesMonitoringAPI) calculateNextState( oldState database.WorkspaceAgentMonitorState, - states []database.WorkspaceAgentMonitorState, + states []resourcesmonitor.State, ) database.WorkspaceAgentMonitorState { - // If we do not have an OK in the last `X` datapoints, then we are - // in an alert state. - lastXStates := states[max(len(states)-a.ConsecutiveNOKsToAlert, 0):] - if !slices.Contains(lastXStates, database.WorkspaceAgentMonitorStateOK) { + // If there are enough consecutive NOK states, we should be in an + // alert state. + consecutiveNOKs := resourcesmonitor.CalculateConsecutiveNOK(states) + if consecutiveNOKs >= a.ConsecutiveNOKsToAlert { return database.WorkspaceAgentMonitorStateNOK } - nokCount := 0 + nokCount, okCount := 0, 0 for _, state := range states { - if state == database.WorkspaceAgentMonitorStateNOK { - nokCount++ + switch state { + case resourcesmonitor.StateOK: + okCount += 1 + case resourcesmonitor.StateNOK: + nokCount += 1 } } @@ -277,51 +280,11 @@ func (a *ResourcesMonitoringAPI) calculateNextState( return database.WorkspaceAgentMonitorStateNOK } - // If there are no NOK datapoints, we should be in an OK state. - if nokCount == 0 { + // If all datapoints are OK, we should be in an OK state + if okCount == len(states) { return database.WorkspaceAgentMonitorStateOK } // Otherwise we stay in the same state as last. return oldState } - -func calculateMemoryUsageStates( - monitor database.WorkspaceAgentMemoryResourceMonitor, - datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage, -) []database.WorkspaceAgentMonitorState { - states := make([]database.WorkspaceAgentMonitorState, 0, len(datapoints)) - - for _, datapoint := range datapoints { - percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) - - state := database.WorkspaceAgentMonitorStateOK - if percent >= monitor.Threshold { - state = database.WorkspaceAgentMonitorStateNOK - } - - states = append(states, state) - } - - return states -} - -func calculateVolumeUsageStates( - monitor database.WorkspaceAgentVolumeResourceMonitor, - datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage, -) []database.WorkspaceAgentMonitorState { - states := make([]database.WorkspaceAgentMonitorState, 0, len(datapoints)) - - for _, datapoint := range datapoints { - percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) - - state := database.WorkspaceAgentMonitorStateOK - if percent >= monitor.Threshold { - state = database.WorkspaceAgentMonitorStateNOK - } - - states = append(states, state) - } - - return states -} diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 5bc29d2c36934..47e953785e1c3 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -84,6 +84,7 @@ func TestMemoryResourceMonitorDebounce(t *testing.T) { // 5. OK -> NOK |> sends a notification as debounce period exceeded api, user, clock, notifyEnq := resourceMonitorAPI(t) + api.ConsecutiveNOKsToAlert = 1 // Given: A monitor in an OK state dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -197,103 +198,76 @@ func TestMemoryResourceMonitor(t *testing.T) { t.Parallel() tests := []struct { - name string - memoryUsage []int64 - memoryTotal int64 - thresholdPercent int32 - minimumNOKs int - consecutiveNOKs int - previousState database.WorkspaceAgentMonitorState - expectState database.WorkspaceAgentMonitorState - shouldNotify bool + name string + memoryUsage []int64 + memoryTotal int64 + previousState database.WorkspaceAgentMonitorState + expectState database.WorkspaceAgentMonitorState + shouldNotify bool }{ { - name: "WhenOK/NeverExceedsThreshold", - memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, - memoryTotal: 10, - thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateOK, - expectState: database.WorkspaceAgentMonitorStateOK, - shouldNotify: false, + name: "WhenOK/NeverExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, }, { - name: "WhenOK/ShouldStayInOK", - memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, - memoryTotal: 10, - thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateOK, - expectState: database.WorkspaceAgentMonitorStateOK, - shouldNotify: false, + name: "WhenOK/ShouldStayInOK", + memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, }, { - name: "WhenOK/ConsecutiveExceedsThreshold", - memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, - memoryTotal: 10, - thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateOK, - expectState: database.WorkspaceAgentMonitorStateNOK, - shouldNotify: true, + name: "WhenOK/ConsecutiveExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: true, }, { - name: "WhenOK/MinimumExceedsThreshold", - memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, - memoryTotal: 10, - thresholdPercent: 80, - minimumNOKs: 4, - consecutiveNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateOK, - expectState: database.WorkspaceAgentMonitorStateNOK, - shouldNotify: true, + name: "WhenOK/MinimumExceedsThreshold", + memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: true, }, { - name: "WhenNOK/NeverExceedsThreshold", - memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, - memoryTotal: 10, - thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateNOK, - expectState: database.WorkspaceAgentMonitorStateOK, - shouldNotify: false, + name: "WhenNOK/NeverExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateOK, + shouldNotify: false, }, { - name: "WhenNOK/ShouldStayInNOK", - memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, - memoryTotal: 10, - thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateNOK, - expectState: database.WorkspaceAgentMonitorStateNOK, - shouldNotify: false, + name: "WhenNOK/ShouldStayInNOK", + memoryUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, }, { - name: "WhenNOK/ConsecutiveExceedsThreshold", - memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, - memoryTotal: 10, - thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateNOK, - expectState: database.WorkspaceAgentMonitorStateNOK, - shouldNotify: false, + name: "WhenNOK/ConsecutiveExceedsThreshold", + memoryUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, }, { - name: "WhenNOK/MinimumExceedsThreshold", - memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, - memoryTotal: 10, - thresholdPercent: 80, - minimumNOKs: 4, - consecutiveNOKs: 10, - previousState: database.WorkspaceAgentMonitorStateNOK, - expectState: database.WorkspaceAgentMonitorStateNOK, - shouldNotify: false, + name: "WhenNOK/MinimumExceedsThreshold", + memoryUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, + memoryTotal: 10, + previousState: database.WorkspaceAgentMonitorStateNOK, + expectState: database.WorkspaceAgentMonitorStateNOK, + shouldNotify: false, }, } @@ -304,8 +278,8 @@ func TestMemoryResourceMonitor(t *testing.T) { t.Parallel() api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.MinimumNOKsToAlert = tt.minimumNOKs - api.ConsecutiveNOKsToAlert = tt.consecutiveNOKs + api.MinimumNOKsToAlert = 4 + api.ConsecutiveNOKsToAlert = 10 datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.memoryUsage)) collectedAt := clock.Now() @@ -323,7 +297,7 @@ func TestMemoryResourceMonitor(t *testing.T) { dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ AgentID: api.AgentID, State: tt.previousState, - Threshold: tt.thresholdPercent, + Threshold: 80, }) clock.Set(collectedAt) @@ -373,6 +347,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { secondVolumePath := "/dev/coder" api, _, clock, notifyEnq := resourceMonitorAPI(t) + api.MinimumNOKsToAlert = 1 // Given: // - First monitor in an OK state @@ -709,6 +684,7 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { t.Parallel() api, _, clock, notifyEnq := resourceMonitorAPI(t) + api.ConsecutiveNOKsToAlert = 1 // Given: two different volume resource monitors dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ diff --git a/coderd/agentapi/resourcesmonitor/resources_monitor.go b/coderd/agentapi/resourcesmonitor/resources_monitor.go new file mode 100644 index 0000000000000..8cd46e5245f4f --- /dev/null +++ b/coderd/agentapi/resourcesmonitor/resources_monitor.go @@ -0,0 +1,80 @@ +package resourcesmonitor + +import ( + "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/database" +) + +type State int + +const ( + StateOK State = iota + StateNOK + StateUnknown +) + +func CalculateMemoryUsageStates( + monitor database.WorkspaceAgentMemoryResourceMonitor, + datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage, +) []State { + states := make([]State, 0, len(datapoints)) + + for _, datapoint := range datapoints { + state := StateUnknown + + if datapoint != nil { + percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) + + if percent < monitor.Threshold { + state = StateOK + } else { + state = StateNOK + } + } + + states = append(states, state) + } + + return states +} + +func CalculateVolumeUsageStates( + monitor database.WorkspaceAgentVolumeResourceMonitor, + datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage, +) []State { + states := make([]State, 0, len(datapoints)) + + for _, datapoint := range datapoints { + state := StateUnknown + + if datapoint != nil { + percent := int32(float64(datapoint.Used) / float64(datapoint.Total) * 100) + + if percent < monitor.Threshold { + state = StateOK + } else { + state = StateNOK + } + } + + states = append(states, state) + } + + return states +} + +func CalculateConsecutiveNOK(states []State) int { + maxLength := 0 + curLength := 0 + + for _, state := range states { + if state == StateNOK { + curLength += 1 + } else { + maxLength = max(maxLength, curLength) + curLength = 0 + } + } + + return max(maxLength, curLength) +} From bda8f298242da846ce7a19d3ab814ab5bb774aed Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 14 Feb 2025 12:05:16 +0000 Subject: [PATCH 29/37] chore: add tests for unknown state on memory monitor --- coderd/agentapi/resources_monitoring.go | 18 +++- coderd/agentapi/resources_monitoring_test.go | 98 +++++++++++++++++++ .../resourcesmonitor/resources_monitor.go | 2 +- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 8b8e027500935..14af958a28f58 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -136,6 +136,7 @@ func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ debouncedUntil = a.Clock.Now().Add(a.Debounce) } + //nolint:gocritic // We need to be able to update the resource monitor here. err = a.Database.UpdateMemoryResourceMonitor(dbauthz.AsResourceMonitor(ctx), database.UpdateMemoryResourceMonitorParams{ AgentID: a.AgentID, State: newState, @@ -152,7 +153,7 @@ func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ return xerrors.Errorf("get workspace by id: %w", err) } - _, err = a.NotificationsEnqueuer.Enqueue( + _, err = a.NotificationsEnqueuer.EnqueueWithData( // nolint:gocritic // We need to be able to send the notification. dbauthz.AsNotifier(ctx), workspace.OwnerID, @@ -161,6 +162,12 @@ func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ "workspace": workspace.Name, "threshold": fmt.Sprintf("%d%%", monitor.Threshold), }, + map[string]any{ + // NOTE(DanielleMaywood): + // We are injecting a timestamp to circumvent the notification + // deduplication logic. + "timestamp": a.Clock.Now(), + }, "workspace-monitor-memory", ) if err != nil { @@ -217,6 +224,7 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints }) } + //nolint:gocritic // We need to be able to update the resource monitor here. if err := a.Database.UpdateVolumeResourceMonitor(dbauthz.AsResourceMonitor(ctx), database.UpdateVolumeResourceMonitorParams{ AgentID: a.AgentID, Path: monitor.Path, @@ -244,6 +252,10 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints }, map[string]any{ "volumes": outOfDiskVolumes, + // NOTE(DanielleMaywood): + // We are injecting a timestamp to circumvent the notification + // deduplication logic. + "timestamp": a.Clock.Now(), }, "workspace-monitor-volumes", ); err != nil { @@ -269,9 +281,9 @@ func (a *ResourcesMonitoringAPI) calculateNextState( for _, state := range states { switch state { case resourcesmonitor.StateOK: - okCount += 1 + okCount++ case resourcesmonitor.StateNOK: - nokCount += 1 + nokCount++ } } diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 47e953785e1c3..28dd43e5d1925 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -317,6 +317,104 @@ func TestMemoryResourceMonitor(t *testing.T) { } } +func TestMemoryResourceMonitorMissingData(t *testing.T) { + t.Parallel() + + t.Run("UnknownPreventsMovingIntoAlertState", func(t *testing.T) { + t.Parallel() + + api, _, clock, notifyEnq := resourceMonitorAPI(t) + api.ConsecutiveNOKsToAlert = 2 + api.MinimumNOKsToAlert = 10 + + // Given: A monitor in an OK state. + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two NOK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Memory: nil, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 10, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect no notifications, as this unknown prevents us knowing we should alert. + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfMemory)) + require.Len(t, sent, 0) + + // Then: We expect the monitor to still be in an OK state. + monitor, err := api.Database.FetchMemoryResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Equal(t, database.WorkspaceAgentMonitorStateOK, monitor.State) + }) + + t.Run("UnknownPreventsMovingOutOfAlertState", func(t *testing.T) { + t.Parallel() + + api, _, clock, _ := resourceMonitorAPI(t) + api.ConsecutiveNOKsToAlert = 2 + api.MinimumNOKsToAlert = 10 + + // Given: A monitor in a NOK state. + dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ + AgentID: api.AgentID, + State: database.WorkspaceAgentMonitorStateNOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two OK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Memory: nil, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Memory: &agentproto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage{ + Used: 1, + Total: 10, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect the monitor to still be in a NOK state. + monitor, err := api.Database.FetchMemoryResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Equal(t, database.WorkspaceAgentMonitorStateNOK, monitor.State) + }) +} + func TestVolumeResourceMonitorDebounce(t *testing.T) { t.Parallel() diff --git a/coderd/agentapi/resourcesmonitor/resources_monitor.go b/coderd/agentapi/resourcesmonitor/resources_monitor.go index 8cd46e5245f4f..ecf860a3d27d6 100644 --- a/coderd/agentapi/resourcesmonitor/resources_monitor.go +++ b/coderd/agentapi/resourcesmonitor/resources_monitor.go @@ -69,7 +69,7 @@ func CalculateConsecutiveNOK(states []State) int { for _, state := range states { if state == StateNOK { - curLength += 1 + curLength++ } else { maxLength = max(maxLength, curLength) curLength = 0 From 9d662a36f29fe3944888d63f6d67589d3758908c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 14 Feb 2025 12:16:35 +0000 Subject: [PATCH 30/37] chore: add tests for missing datapoints in volume monitors --- coderd/agentapi/resources_monitoring.go | 34 ++---- coderd/agentapi/resources_monitoring_test.go | 118 +++++++++++++++++++ 2 files changed, 131 insertions(+), 21 deletions(-) diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 14af958a28f58..3d4dbd07f1ccd 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -22,14 +22,6 @@ import ( "github.com/coder/quartz" ) -type VolumeNotFoundError struct { - Volume string -} - -func (e VolumeNotFoundError) Error() string { - return fmt.Sprintf("volume not found: `%s`", e.Volume) -} - type ResourcesMonitoringAPI struct { AgentID uuid.UUID WorkspaceID uuid.UUID @@ -184,15 +176,6 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints return xerrors.Errorf("get or insert volume monitor: %w", err) } - volumes := make(map[string][]*proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage) - for _, datapoint := range datapoints { - for _, volume := range datapoint.Volumes { - volumeDatapoints := volumes[volume.Volume] - volumeDatapoints = append(volumeDatapoints, volume) - volumes[volume.Volume] = volumeDatapoints - } - } - outOfDiskVolumes := make([]map[string]any, 0) for _, monitor := range volumeMonitors { @@ -200,12 +183,21 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints continue } - datapoints, found := volumes[monitor.Path] - if !found { - return VolumeNotFoundError{Volume: monitor.Path} + usageDatapoints := make([]*proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage, 0, len(datapoints)) + for _, datapoint := range datapoints { + var usage *proto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage + + for _, volume := range datapoint.Volumes { + if volume.Volume == monitor.Path { + usage = volume + break + } + } + + usageDatapoints = append(usageDatapoints, usage) } - usageStates := resourcesmonitor.CalculateVolumeUsageStates(monitor, datapoints) + usageStates := resourcesmonitor.CalculateVolumeUsageStates(monitor, usageDatapoints) oldState := monitor.State newState := a.calculateNextState(oldState, usageStates) diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 28dd43e5d1925..0d0c2cef3126e 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -831,6 +831,124 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { require.Equal(t, "/dev/coder", volumes[1]["path"]) } +func TestVolumeResourceMonitorMissingData(t *testing.T) { + t.Parallel() + + t.Run("UnknownPreventsMovingIntoAlertState", func(t *testing.T) { + t.Parallel() + + volumePath := "/home/coder" + + api, _, clock, notifyEnq := resourceMonitorAPI(t) + api.ConsecutiveNOKsToAlert = 2 + api.MinimumNOKsToAlert = 10 + + // Given: A monitor in an OK state. + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: volumePath, + State: database.WorkspaceAgentMonitorStateOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two NOK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 10, + Total: 10, + }, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{}, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 10, + Total: 10, + }, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect no notifications, as this unknown prevents us knowing we should alert. + sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceOutOfDisk)) + require.Len(t, sent, 0) + + // Then: We expect the monitor to still be in an OK state. + monitors, err := api.Database.FetchVolumesResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Len(t, monitors, 1) + require.Equal(t, database.WorkspaceAgentMonitorStateOK, monitors[0].State) + }) + + t.Run("UnknownPreventsMovingOutOfAlertState", func(t *testing.T) { + t.Parallel() + + volumePath := "/home/coder" + + api, _, clock, _ := resourceMonitorAPI(t) + api.ConsecutiveNOKsToAlert = 2 + api.MinimumNOKsToAlert = 10 + + // Given: A monitor in a NOK state. + dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ + AgentID: api.AgentID, + Path: volumePath, + State: database.WorkspaceAgentMonitorStateNOK, + Threshold: 80, + }) + + // When: A datapoint is missing, surrounded by two OK datapoints. + _, err := api.PushResourcesMonitoringUsage(context.Background(), &agentproto.PushResourcesMonitoringUsageRequest{ + Datapoints: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint{ + { + CollectedAt: timestamppb.New(clock.Now()), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 1, + Total: 10, + }, + }, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(10 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{}, + }, + { + CollectedAt: timestamppb.New(clock.Now().Add(20 * time.Second)), + Volumes: []*agentproto.PushResourcesMonitoringUsageRequest_Datapoint_VolumeUsage{ + { + Volume: volumePath, + Used: 1, + Total: 10, + }, + }, + }, + }, + }) + require.NoError(t, err) + + // Then: We expect the monitor to still be in a NOK state. + monitors, err := api.Database.FetchVolumesResourceMonitorsByAgentID(context.Background(), api.AgentID) + require.NoError(t, err) + require.Len(t, monitors, 1) + require.Equal(t, database.WorkspaceAgentMonitorStateNOK, monitors[0].State) + }) +} + func requireVolumeData(t *testing.T, notif *notificationstest.FakeNotification) []map[string]any { t.Helper() From bff48dc799734a379085bd170684e34416928951 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 14 Feb 2025 12:17:59 +0000 Subject: [PATCH 31/37] chore: add default debounce of 5 minutes --- coderd/agentapi/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index fb615b9ab5209..2b2065897d6aa 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -117,6 +117,7 @@ func New(opts Options) *API { Clock: opts.Clock, Database: opts.Database, NotificationsEnqueuer: opts.NotificationsEnqueuer, + Debounce: 5 * time.Minute, // These values assume a window of 20 MinimumNOKsToAlert: 4, From c343a701e3656fe3e155f02dc9c4edd25896764c Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 14 Feb 2025 15:24:08 +0000 Subject: [PATCH 32/37] chore: implement feedback --- coderd/agentapi/api.go | 7 +- coderd/agentapi/resources_monitoring.go | 64 ++----------------- coderd/agentapi/resources_monitoring_test.go | 49 +++++++------- .../resourcesmonitor/resources_monitor.go | 45 ++++++++++--- coderd/database/modelmethods.go | 28 ++++++++ coderd/util/slice/slice.go | 16 +++++ 6 files changed, 116 insertions(+), 93 deletions(-) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index 2b2065897d6aa..c9f3854ccb9ae 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -17,6 +17,7 @@ import ( "cdr.dev/slog" agentproto "github.com/coder/coder/v2/agent/proto" + "github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor" "github.com/coder/coder/v2/coderd/appearance" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/pubsub" @@ -120,8 +121,10 @@ func New(opts Options) *API { Debounce: 5 * time.Minute, // These values assume a window of 20 - MinimumNOKsToAlert: 4, - ConsecutiveNOKsToAlert: 10, + Config: resourcesmonitor.Config{ + MinimumNOKsToAlert: 4, + ConsecutiveNOKsToAlert: 10, + }, } api.StatsAPI = &StatsAPI{ diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 3d4dbd07f1ccd..ae48369ef4461 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -32,14 +32,7 @@ type ResourcesMonitoringAPI struct { NotificationsEnqueuer notifications.Enqueuer Debounce time.Duration - - // How many datapoints in a row are required to - // put the monitor in an alert state. - ConsecutiveNOKsToAlert int - - // How many datapoints in total are required to - // put the monitor in an alert state. - MinimumNOKsToAlert int + Config resourcesmonitor.Config } func (a *ResourcesMonitoringAPI) GetResourcesMonitoringConfiguration(ctx context.Context, _ *proto.GetResourcesMonitoringConfigurationRequest) (*proto.GetResourcesMonitoringConfigurationResponse, error) { @@ -117,16 +110,9 @@ func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ usageStates := resourcesmonitor.CalculateMemoryUsageStates(monitor, usageDatapoints) oldState := monitor.State - newState := a.calculateNextState(oldState, usageStates) - - shouldNotify := a.Clock.Now().After(monitor.DebouncedUntil) && - oldState == database.WorkspaceAgentMonitorStateOK && - newState == database.WorkspaceAgentMonitorStateNOK + newState := resourcesmonitor.NextState(a.Config, oldState, usageStates) - debouncedUntil := monitor.DebouncedUntil - if shouldNotify { - debouncedUntil = a.Clock.Now().Add(a.Debounce) - } + debouncedUntil, shouldNotify := monitor.Debounce(a.Debounce, a.Clock.Now(), oldState, newState) //nolint:gocritic // We need to be able to update the resource monitor here. err = a.Database.UpdateMemoryResourceMonitor(dbauthz.AsResourceMonitor(ctx), database.UpdateMemoryResourceMonitorParams{ @@ -200,16 +186,11 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints usageStates := resourcesmonitor.CalculateVolumeUsageStates(monitor, usageDatapoints) oldState := monitor.State - newState := a.calculateNextState(oldState, usageStates) + newState := resourcesmonitor.NextState(a.Config, oldState, usageStates) - shouldNotify := a.Clock.Now().After(monitor.DebouncedUntil) && - oldState == database.WorkspaceAgentMonitorStateOK && - newState == database.WorkspaceAgentMonitorStateNOK + debouncedUntil, shouldNotify := monitor.Debounce(a.Debounce, a.Clock.Now(), oldState, newState) - debouncedUntil := monitor.DebouncedUntil if shouldNotify { - debouncedUntil = a.Clock.Now().Add(a.Debounce) - outOfDiskVolumes = append(outOfDiskVolumes, map[string]any{ "path": monitor.Path, "threshold": fmt.Sprintf("%d%%", monitor.Threshold), @@ -257,38 +238,3 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints return nil } - -func (a *ResourcesMonitoringAPI) calculateNextState( - oldState database.WorkspaceAgentMonitorState, - states []resourcesmonitor.State, -) database.WorkspaceAgentMonitorState { - // If there are enough consecutive NOK states, we should be in an - // alert state. - consecutiveNOKs := resourcesmonitor.CalculateConsecutiveNOK(states) - if consecutiveNOKs >= a.ConsecutiveNOKsToAlert { - return database.WorkspaceAgentMonitorStateNOK - } - - nokCount, okCount := 0, 0 - for _, state := range states { - switch state { - case resourcesmonitor.StateOK: - okCount++ - case resourcesmonitor.StateNOK: - nokCount++ - } - } - - // If there are enough NOK datapoints, we should be in an alert state. - if nokCount >= a.MinimumNOKsToAlert { - return database.WorkspaceAgentMonitorStateNOK - } - - // If all datapoints are OK, we should be in an OK state - if okCount == len(states) { - return database.WorkspaceAgentMonitorStateOK - } - - // Otherwise we stay in the same state as last. - return oldState -} diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 0d0c2cef3126e..39b68c2df6275 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -11,6 +11,7 @@ import ( agentproto "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/agentapi" + "github.com/coder/coder/v2/coderd/agentapi/resourcesmonitor" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" @@ -58,14 +59,16 @@ func resourceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, databas clock := quartz.NewMock(t) return &agentapi.ResourcesMonitoringAPI{ - AgentID: agent.ID, - WorkspaceID: workspace.ID, - Clock: clock, - Database: db, - NotificationsEnqueuer: notifyEnq, - MinimumNOKsToAlert: 4, - ConsecutiveNOKsToAlert: 10, - Debounce: 1 * time.Minute, + AgentID: agent.ID, + WorkspaceID: workspace.ID, + Clock: clock, + Database: db, + NotificationsEnqueuer: notifyEnq, + Config: resourcesmonitor.Config{ + MinimumNOKsToAlert: 4, + ConsecutiveNOKsToAlert: 10, + }, + Debounce: 1 * time.Minute, }, user, clock, notifyEnq } @@ -84,7 +87,7 @@ func TestMemoryResourceMonitorDebounce(t *testing.T) { // 5. OK -> NOK |> sends a notification as debounce period exceeded api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.ConsecutiveNOKsToAlert = 1 + api.Config.ConsecutiveNOKsToAlert = 1 // Given: A monitor in an OK state dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -278,8 +281,8 @@ func TestMemoryResourceMonitor(t *testing.T) { t.Parallel() api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.MinimumNOKsToAlert = 4 - api.ConsecutiveNOKsToAlert = 10 + api.Config.MinimumNOKsToAlert = 4 + api.Config.ConsecutiveNOKsToAlert = 10 datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.memoryUsage)) collectedAt := clock.Now() @@ -324,8 +327,8 @@ func TestMemoryResourceMonitorMissingData(t *testing.T) { t.Parallel() api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.ConsecutiveNOKsToAlert = 2 - api.MinimumNOKsToAlert = 10 + api.Config.ConsecutiveNOKsToAlert = 2 + api.Config.MinimumNOKsToAlert = 10 // Given: A monitor in an OK state. dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -373,8 +376,8 @@ func TestMemoryResourceMonitorMissingData(t *testing.T) { t.Parallel() api, _, clock, _ := resourceMonitorAPI(t) - api.ConsecutiveNOKsToAlert = 2 - api.MinimumNOKsToAlert = 10 + api.Config.ConsecutiveNOKsToAlert = 2 + api.Config.MinimumNOKsToAlert = 10 // Given: A monitor in a NOK state. dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -445,7 +448,7 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { secondVolumePath := "/dev/coder" api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.MinimumNOKsToAlert = 1 + api.Config.MinimumNOKsToAlert = 1 // Given: // - First monitor in an OK state @@ -732,8 +735,8 @@ func TestVolumeResourceMonitor(t *testing.T) { t.Parallel() api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.MinimumNOKsToAlert = tt.minimumNOKs - api.ConsecutiveNOKsToAlert = tt.consecutiveNOKs + api.Config.MinimumNOKsToAlert = tt.minimumNOKs + api.Config.ConsecutiveNOKsToAlert = tt.consecutiveNOKs datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.volumeUsage)) collectedAt := clock.Now() @@ -782,7 +785,7 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { t.Parallel() api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.ConsecutiveNOKsToAlert = 1 + api.Config.ConsecutiveNOKsToAlert = 1 // Given: two different volume resource monitors dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ @@ -840,8 +843,8 @@ func TestVolumeResourceMonitorMissingData(t *testing.T) { volumePath := "/home/coder" api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.ConsecutiveNOKsToAlert = 2 - api.MinimumNOKsToAlert = 10 + api.Config.ConsecutiveNOKsToAlert = 2 + api.Config.MinimumNOKsToAlert = 10 // Given: A monitor in an OK state. dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ @@ -899,8 +902,8 @@ func TestVolumeResourceMonitorMissingData(t *testing.T) { volumePath := "/home/coder" api, _, clock, _ := resourceMonitorAPI(t) - api.ConsecutiveNOKsToAlert = 2 - api.MinimumNOKsToAlert = 10 + api.Config.ConsecutiveNOKsToAlert = 2 + api.Config.MinimumNOKsToAlert = 10 // Given: A monitor in a NOK state. dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ diff --git a/coderd/agentapi/resourcesmonitor/resources_monitor.go b/coderd/agentapi/resourcesmonitor/resources_monitor.go index ecf860a3d27d6..5803a30af5d7c 100644 --- a/coderd/agentapi/resourcesmonitor/resources_monitor.go +++ b/coderd/agentapi/resourcesmonitor/resources_monitor.go @@ -3,6 +3,7 @@ package resourcesmonitor import ( "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/util/slice" ) type State int @@ -13,6 +14,16 @@ const ( StateUnknown ) +type Config struct { + // How many datapoints in a row are required to + // put the monitor in an alert state. + ConsecutiveNOKsToAlert int + + // How many datapoints in total are required to + // put the monitor in an alert state. + MinimumNOKsToAlert int +} + func CalculateMemoryUsageStates( monitor database.WorkspaceAgentMemoryResourceMonitor, datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint_MemoryUsage, @@ -63,18 +74,34 @@ func CalculateVolumeUsageStates( return states } -func CalculateConsecutiveNOK(states []State) int { - maxLength := 0 - curLength := 0 +func NextState(c Config, oldState database.WorkspaceAgentMonitorState, states []State) database.WorkspaceAgentMonitorState { + // If there are enough consecutive NOK states, we should be in an + // alert state. + consecutiveNOKs := slice.CountConsecutive(StateNOK, states...) + if consecutiveNOKs >= c.ConsecutiveNOKsToAlert { + return database.WorkspaceAgentMonitorStateNOK + } + nokCount, okCount := 0, 0 for _, state := range states { - if state == StateNOK { - curLength++ - } else { - maxLength = max(maxLength, curLength) - curLength = 0 + switch state { + case StateOK: + okCount++ + case StateNOK: + nokCount++ } } - return max(maxLength, curLength) + // If there are enough NOK datapoints, we should be in an alert state. + if nokCount >= c.MinimumNOKsToAlert { + return database.WorkspaceAgentMonitorStateNOK + } + + // If all datapoints are OK, we should be in an OK state + if okCount == len(states) { + return database.WorkspaceAgentMonitorStateOK + } + + // Otherwise we stay in the same state as last. + return oldState } diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 63e03ccb27f40..171c0454563de 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -527,3 +527,31 @@ func (k CryptoKey) CanVerify(now time.Time) bool { func (r GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow) RBACObject() rbac.Object { return r.ProvisionerJob.RBACObject() } + +func (m WorkspaceAgentMemoryResourceMonitor) Debounce( + by time.Duration, + now time.Time, + oldState, newState WorkspaceAgentMonitorState, +) (time.Time, bool) { + if now.After(m.DebouncedUntil) && + oldState == WorkspaceAgentMonitorStateOK && + newState == WorkspaceAgentMonitorStateNOK { + return now.Add(by), true + } + + return m.DebouncedUntil, false +} + +func (m WorkspaceAgentVolumeResourceMonitor) Debounce( + by time.Duration, + now time.Time, + oldState, newState WorkspaceAgentMonitorState, +) (debouncedUntil time.Time, shouldNotify bool) { + if now.After(m.DebouncedUntil) && + oldState == WorkspaceAgentMonitorStateOK && + newState == WorkspaceAgentMonitorStateNOK { + return now.Add(by), true + } + + return m.DebouncedUntil, false +} diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 2a62e23592d84..508827dfaae81 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -177,3 +177,19 @@ func DifferenceFunc[T any](a []T, b []T, equal func(a, b T) bool) []T { } return tmp } + +func CountConsecutive[T comparable](needle T, haystack ...T) int { + maxLength := 0 + curLength := 0 + + for _, v := range haystack { + if v == needle { + curLength++ + } else { + maxLength = max(maxLength, curLength) + curLength = 0 + } + } + + return max(maxLength, curLength) +} From babc48f8cfd6faf3a73e3af3c3d7d40c4a43b7f5 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 17 Feb 2025 10:11:59 +0000 Subject: [PATCH 33/37] chore: feedback --- coderd/agentapi/resources_monitoring.go | 112 ++++++++++-------- .../resourcesmonitor/resources_monitor.go | 3 + coderd/database/dbmem/dbmem.go | 6 + 3 files changed, 72 insertions(+), 49 deletions(-) diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index ae48369ef4461..22d34ed2f87b1 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -125,32 +125,39 @@ func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints [ return xerrors.Errorf("update workspace monitor: %w", err) } - if shouldNotify { - workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) - if err != nil { - return xerrors.Errorf("get workspace by id: %w", err) - } + if !shouldNotify { + return nil + } - _, err = a.NotificationsEnqueuer.EnqueueWithData( - // nolint:gocritic // We need to be able to send the notification. - dbauthz.AsNotifier(ctx), - workspace.OwnerID, - notifications.TemplateWorkspaceOutOfMemory, - map[string]string{ - "workspace": workspace.Name, - "threshold": fmt.Sprintf("%d%%", monitor.Threshold), - }, - map[string]any{ - // NOTE(DanielleMaywood): - // We are injecting a timestamp to circumvent the notification - // deduplication logic. - "timestamp": a.Clock.Now(), - }, - "workspace-monitor-memory", - ) - if err != nil { - return xerrors.Errorf("notify workspace OOM: %w", err) - } + workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by id: %w", err) + } + + _, err = a.NotificationsEnqueuer.EnqueueWithData( + // nolint:gocritic // We need to be able to send the notification. + dbauthz.AsNotifier(ctx), + workspace.OwnerID, + notifications.TemplateWorkspaceOutOfMemory, + map[string]string{ + "workspace": workspace.Name, + "threshold": fmt.Sprintf("%d%%", monitor.Threshold), + }, + map[string]any{ + // NOTE(DanielleMaywood): + // When notifications are enqueued, they are checked to be + // unique within a single day. This means that if we attempt + // to send two OOM notifications for the same workspace on + // the same day, the enqueuer will prevent us from sending + // a second one. We are inject a timestamp to make the + // notifications appear different enough to circumvent this + // deduplication logic. + "timestamp": a.Clock.Now(), + }, + "workspace-monitor-memory", + ) + if err != nil { + return xerrors.Errorf("notify workspace OOM: %w", err) } return nil @@ -209,31 +216,38 @@ func (a *ResourcesMonitoringAPI) monitorVolumes(ctx context.Context, datapoints } } - if len(outOfDiskVolumes) != 0 { - workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) - if err != nil { - return xerrors.Errorf("get workspace by id: %w", err) - } + if len(outOfDiskVolumes) == 0 { + return nil + } - if _, err := a.NotificationsEnqueuer.EnqueueWithData( - // nolint:gocritic // We need to be able to send the notification. - dbauthz.AsNotifier(ctx), - workspace.OwnerID, - notifications.TemplateWorkspaceOutOfDisk, - map[string]string{ - "workspace": workspace.Name, - }, - map[string]any{ - "volumes": outOfDiskVolumes, - // NOTE(DanielleMaywood): - // We are injecting a timestamp to circumvent the notification - // deduplication logic. - "timestamp": a.Clock.Now(), - }, - "workspace-monitor-volumes", - ); err != nil { - return xerrors.Errorf("notify workspace OOD: %w", err) - } + workspace, err := a.Database.GetWorkspaceByID(ctx, a.WorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by id: %w", err) + } + + if _, err := a.NotificationsEnqueuer.EnqueueWithData( + // nolint:gocritic // We need to be able to send the notification. + dbauthz.AsNotifier(ctx), + workspace.OwnerID, + notifications.TemplateWorkspaceOutOfDisk, + map[string]string{ + "workspace": workspace.Name, + }, + map[string]any{ + "volumes": outOfDiskVolumes, + // NOTE(DanielleMaywood): + // When notifications are enqueued, they are checked to be + // unique within a single day. This means that if we attempt + // to send two OOM notifications for the same workspace on + // the same day, the enqueuer will prevent us from sending + // a second one. We are inject a timestamp to make the + // notifications appear different enough to circumvent this + // deduplication logic. + "timestamp": a.Clock.Now(), + }, + "workspace-monitor-volumes", + ); err != nil { + return xerrors.Errorf("notify workspace OOD: %w", err) } return nil diff --git a/coderd/agentapi/resourcesmonitor/resources_monitor.go b/coderd/agentapi/resourcesmonitor/resources_monitor.go index 5803a30af5d7c..153143d896b14 100644 --- a/coderd/agentapi/resourcesmonitor/resources_monitor.go +++ b/coderd/agentapi/resourcesmonitor/resources_monitor.go @@ -82,6 +82,9 @@ func NextState(c Config, oldState database.WorkspaceAgentMonitorState, states [] return database.WorkspaceAgentMonitorStateNOK } + // We do not explicitly handle StateUnknown because it could have + // been either StateOK or StateNOK if collection didn't fail. As + // it could be either, our best bet is to ignore it. nokCount, okCount := 0, 0 for _, state := range states { switch state { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 11ba64696cf5f..d2534dbbe74d5 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9649,6 +9649,9 @@ func (q *FakeQuerier) UpdateMemoryResourceMonitor(_ context.Context, arg databas return err } + q.mutex.Lock() + defer q.mutex.Unlock() + for i, monitor := range q.workspaceAgentMemoryResourceMonitors { if monitor.AgentID != arg.AgentID { continue @@ -10448,6 +10451,9 @@ func (q *FakeQuerier) UpdateVolumeResourceMonitor(_ context.Context, arg databas return err } + q.mutex.Lock() + defer q.mutex.Unlock() + for i, monitor := range q.workspaceAgentVolumeResourceMonitors { if monitor.AgentID != arg.AgentID || monitor.Path != arg.Path { continue From 01ca5499101cb83c48338f62507f07af25934bc4 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 17 Feb 2025 12:34:27 +0000 Subject: [PATCH 34/37] chore: feedback --- coderd/agentapi/resources_monitoring.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 22d34ed2f87b1..6641ffcf43ab9 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -75,15 +75,17 @@ func (a *ResourcesMonitoringAPI) GetResourcesMonitoringConfiguration(ctx context } func (a *ResourcesMonitoringAPI) PushResourcesMonitoringUsage(ctx context.Context, req *proto.PushResourcesMonitoringUsageRequest) (*proto.PushResourcesMonitoringUsageResponse, error) { - if err := a.monitorMemory(ctx, req.Datapoints); err != nil { - return nil, xerrors.Errorf("monitor memory: %w", err) + var err error + + if memoryErr := a.monitorMemory(ctx, req.Datapoints); memoryErr != nil { + err = errors.Join(err, fmt.Errorf("monitor memory: %w", memoryErr)) } - if err := a.monitorVolumes(ctx, req.Datapoints); err != nil { - return nil, xerrors.Errorf("monitor volumes: %w", err) + if volumeErr := a.monitorVolumes(ctx, req.Datapoints); volumeErr != nil { + err = errors.Join(err, fmt.Errorf("monitor volume: %w", volumeErr)) } - return &proto.PushResourcesMonitoringUsageResponse{}, nil + return &proto.PushResourcesMonitoringUsageResponse{}, err } func (a *ResourcesMonitoringAPI) monitorMemory(ctx context.Context, datapoints []*proto.PushResourcesMonitoringUsageRequest_Datapoint) error { From a975810ecb708c00145faaa25b9f3e05ed0eae4a Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 17 Feb 2025 12:43:55 +0000 Subject: [PATCH 35/37] chore: forgot to run the linter --- coderd/agentapi/resources_monitoring.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 6641ffcf43ab9..9bccf4a9ddb2e 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -78,11 +78,11 @@ func (a *ResourcesMonitoringAPI) PushResourcesMonitoringUsage(ctx context.Contex var err error if memoryErr := a.monitorMemory(ctx, req.Datapoints); memoryErr != nil { - err = errors.Join(err, fmt.Errorf("monitor memory: %w", memoryErr)) + err = errors.Join(err, xerrors.Errorf("monitor memory: %w", memoryErr)) } if volumeErr := a.monitorVolumes(ctx, req.Datapoints); volumeErr != nil { - err = errors.Join(err, fmt.Errorf("monitor volume: %w", volumeErr)) + err = errors.Join(err, xerrors.Errorf("monitor volume: %w", volumeErr)) } return &proto.PushResourcesMonitoringUsageResponse{}, err From ee35d855472158bfdb30c86cb3adcac0747ae3c1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 17 Feb 2025 16:30:12 +0000 Subject: [PATCH 36/37] chore: use percentages for alert config --- coderd/agentapi/api.go | 10 ++-- coderd/agentapi/resources_monitoring.go | 4 +- coderd/agentapi/resources_monitoring_test.go | 52 ++++++------------- .../resourcesmonitor/resources_monitor.go | 36 ++++++++++--- 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/coderd/agentapi/api.go b/coderd/agentapi/api.go index c9f3854ccb9ae..3922dfc4bcad0 100644 --- a/coderd/agentapi/api.go +++ b/coderd/agentapi/api.go @@ -120,10 +120,14 @@ func New(opts Options) *API { NotificationsEnqueuer: opts.NotificationsEnqueuer, Debounce: 5 * time.Minute, - // These values assume a window of 20 Config: resourcesmonitor.Config{ - MinimumNOKsToAlert: 4, - ConsecutiveNOKsToAlert: 10, + NumDatapoints: 20, + CollectionInterval: 10 * time.Second, + + Alert: resourcesmonitor.AlertConfig{ + MinimumNOKsPercent: 20, + ConsecutiveNOKsPercent: 50, + }, }, } diff --git a/coderd/agentapi/resources_monitoring.go b/coderd/agentapi/resources_monitoring.go index 9bccf4a9ddb2e..e21c9bc7581d8 100644 --- a/coderd/agentapi/resources_monitoring.go +++ b/coderd/agentapi/resources_monitoring.go @@ -48,8 +48,8 @@ func (a *ResourcesMonitoringAPI) GetResourcesMonitoringConfiguration(ctx context return &proto.GetResourcesMonitoringConfigurationResponse{ Config: &proto.GetResourcesMonitoringConfigurationResponse_Config{ - CollectionIntervalSeconds: 10, - NumDatapoints: 20, + CollectionIntervalSeconds: int32(a.Config.CollectionInterval.Seconds()), + NumDatapoints: a.Config.NumDatapoints, }, Memory: func() *proto.GetResourcesMonitoringConfigurationResponse_Memory { if memoryErr != nil { diff --git a/coderd/agentapi/resources_monitoring_test.go b/coderd/agentapi/resources_monitoring_test.go index 39b68c2df6275..087ccfd24e459 100644 --- a/coderd/agentapi/resources_monitoring_test.go +++ b/coderd/agentapi/resources_monitoring_test.go @@ -65,8 +65,13 @@ func resourceMonitorAPI(t *testing.T) (*agentapi.ResourcesMonitoringAPI, databas Database: db, NotificationsEnqueuer: notifyEnq, Config: resourcesmonitor.Config{ - MinimumNOKsToAlert: 4, - ConsecutiveNOKsToAlert: 10, + NumDatapoints: 20, + CollectionInterval: 10 * time.Second, + + Alert: resourcesmonitor.AlertConfig{ + MinimumNOKsPercent: 20, + ConsecutiveNOKsPercent: 50, + }, }, Debounce: 1 * time.Minute, }, user, clock, notifyEnq @@ -87,7 +92,7 @@ func TestMemoryResourceMonitorDebounce(t *testing.T) { // 5. OK -> NOK |> sends a notification as debounce period exceeded api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.Config.ConsecutiveNOKsToAlert = 1 + api.Config.Alert.ConsecutiveNOKsPercent = 100 // Given: A monitor in an OK state dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -281,8 +286,6 @@ func TestMemoryResourceMonitor(t *testing.T) { t.Parallel() api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.Config.MinimumNOKsToAlert = 4 - api.Config.ConsecutiveNOKsToAlert = 10 datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.memoryUsage)) collectedAt := clock.Now() @@ -327,8 +330,8 @@ func TestMemoryResourceMonitorMissingData(t *testing.T) { t.Parallel() api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.Config.ConsecutiveNOKsToAlert = 2 - api.Config.MinimumNOKsToAlert = 10 + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 // Given: A monitor in an OK state. dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -376,8 +379,8 @@ func TestMemoryResourceMonitorMissingData(t *testing.T) { t.Parallel() api, _, clock, _ := resourceMonitorAPI(t) - api.Config.ConsecutiveNOKsToAlert = 2 - api.Config.MinimumNOKsToAlert = 10 + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 // Given: A monitor in a NOK state. dbgen.WorkspaceAgentMemoryResourceMonitor(t, api.Database, database.WorkspaceAgentMemoryResourceMonitor{ @@ -448,7 +451,6 @@ func TestVolumeResourceMonitorDebounce(t *testing.T) { secondVolumePath := "/dev/coder" api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.Config.MinimumNOKsToAlert = 1 // Given: // - First monitor in an OK state @@ -627,8 +629,6 @@ func TestVolumeResourceMonitor(t *testing.T) { previousState database.WorkspaceAgentMonitorState expectState database.WorkspaceAgentMonitorState shouldNotify bool - minimumNOKs int - consecutiveNOKs int }{ { name: "WhenOK/NeverExceedsThreshold", @@ -636,8 +636,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, volumeTotal: 10, thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, previousState: database.WorkspaceAgentMonitorStateOK, expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, @@ -648,8 +646,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, volumeTotal: 10, thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, previousState: database.WorkspaceAgentMonitorStateOK, expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, @@ -660,8 +656,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, volumeTotal: 10, thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, previousState: database.WorkspaceAgentMonitorStateOK, expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: true, @@ -672,8 +666,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, volumeTotal: 10, thresholdPercent: 80, - minimumNOKs: 4, - consecutiveNOKs: 10, previousState: database.WorkspaceAgentMonitorStateOK, expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: true, @@ -684,8 +676,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, volumeTotal: 10, thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, previousState: database.WorkspaceAgentMonitorStateNOK, expectState: database.WorkspaceAgentMonitorStateOK, shouldNotify: false, @@ -696,8 +686,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{9, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 2, 3, 1, 2}, volumeTotal: 10, thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, previousState: database.WorkspaceAgentMonitorStateNOK, expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: false, @@ -708,8 +696,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{2, 3, 2, 4, 2, 3, 2, 1, 2, 3, 4, 4, 1, 8, 9, 8, 9}, volumeTotal: 10, thresholdPercent: 80, - consecutiveNOKs: 4, - minimumNOKs: 10, previousState: database.WorkspaceAgentMonitorStateNOK, expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: false, @@ -720,8 +706,6 @@ func TestVolumeResourceMonitor(t *testing.T) { volumeUsage: []int64{2, 8, 2, 9, 2, 8, 2, 9, 2, 8, 4, 9, 1, 8, 2, 8, 9}, volumeTotal: 10, thresholdPercent: 80, - minimumNOKs: 4, - consecutiveNOKs: 10, previousState: database.WorkspaceAgentMonitorStateNOK, expectState: database.WorkspaceAgentMonitorStateNOK, shouldNotify: false, @@ -735,8 +719,6 @@ func TestVolumeResourceMonitor(t *testing.T) { t.Parallel() api, user, clock, notifyEnq := resourceMonitorAPI(t) - api.Config.MinimumNOKsToAlert = tt.minimumNOKs - api.Config.ConsecutiveNOKsToAlert = tt.consecutiveNOKs datapoints := make([]*agentproto.PushResourcesMonitoringUsageRequest_Datapoint, 0, len(tt.volumeUsage)) collectedAt := clock.Now() @@ -785,7 +767,7 @@ func TestVolumeResourceMonitorMultiple(t *testing.T) { t.Parallel() api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.Config.ConsecutiveNOKsToAlert = 1 + api.Config.Alert.ConsecutiveNOKsPercent = 100 // Given: two different volume resource monitors dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ @@ -843,8 +825,8 @@ func TestVolumeResourceMonitorMissingData(t *testing.T) { volumePath := "/home/coder" api, _, clock, notifyEnq := resourceMonitorAPI(t) - api.Config.ConsecutiveNOKsToAlert = 2 - api.Config.MinimumNOKsToAlert = 10 + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 // Given: A monitor in an OK state. dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ @@ -902,8 +884,8 @@ func TestVolumeResourceMonitorMissingData(t *testing.T) { volumePath := "/home/coder" api, _, clock, _ := resourceMonitorAPI(t) - api.Config.ConsecutiveNOKsToAlert = 2 - api.Config.MinimumNOKsToAlert = 10 + api.Config.Alert.ConsecutiveNOKsPercent = 50 + api.Config.Alert.MinimumNOKsPercent = 100 // Given: A monitor in a NOK state. dbgen.WorkspaceAgentVolumeResourceMonitor(t, api.Database, database.WorkspaceAgentVolumeResourceMonitor{ diff --git a/coderd/agentapi/resourcesmonitor/resources_monitor.go b/coderd/agentapi/resourcesmonitor/resources_monitor.go index 153143d896b14..deee4e0c862a1 100644 --- a/coderd/agentapi/resourcesmonitor/resources_monitor.go +++ b/coderd/agentapi/resourcesmonitor/resources_monitor.go @@ -1,6 +1,9 @@ package resourcesmonitor import ( + "math" + "time" + "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/util/slice" @@ -14,14 +17,25 @@ const ( StateUnknown ) +type AlertConfig struct { + // What percentage of datapoints in a row are + // required to put the monitor in an alert state. + ConsecutiveNOKsPercent int + + // What percentage of datapoints in a window are + // required to put the monitor in an alert state. + MinimumNOKsPercent int +} + type Config struct { - // How many datapoints in a row are required to - // put the monitor in an alert state. - ConsecutiveNOKsToAlert int + // How many datapoints should the agent send + NumDatapoints int32 - // How many datapoints in total are required to - // put the monitor in an alert state. - MinimumNOKsToAlert int + // How long between each datapoint should + // collection occur. + CollectionInterval time.Duration + + Alert AlertConfig } func CalculateMemoryUsageStates( @@ -75,10 +89,11 @@ func CalculateVolumeUsageStates( } func NextState(c Config, oldState database.WorkspaceAgentMonitorState, states []State) database.WorkspaceAgentMonitorState { + // If there are enough consecutive NOK states, we should be in an // alert state. consecutiveNOKs := slice.CountConsecutive(StateNOK, states...) - if consecutiveNOKs >= c.ConsecutiveNOKsToAlert { + if percent(consecutiveNOKs, len(states)) >= c.Alert.ConsecutiveNOKsPercent { return database.WorkspaceAgentMonitorStateNOK } @@ -96,7 +111,7 @@ func NextState(c Config, oldState database.WorkspaceAgentMonitorState, states [] } // If there are enough NOK datapoints, we should be in an alert state. - if nokCount >= c.MinimumNOKsToAlert { + if percent(nokCount, len(states)) >= c.Alert.MinimumNOKsPercent { return database.WorkspaceAgentMonitorStateNOK } @@ -108,3 +123,8 @@ func NextState(c Config, oldState database.WorkspaceAgentMonitorState, states [] // Otherwise we stay in the same state as last. return oldState } + +func percent[T int](numerator, denominator T) int { + percent := float64(numerator*100) / float64(denominator) + return int(math.Round(percent)) +} From 27d78d1d171a45f7e16fa8bed8213f8b402b4a16 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 17 Feb 2025 16:34:26 +0000 Subject: [PATCH 37/37] chore: fmt and bump migration number --- coderd/agentapi/resourcesmonitor/resources_monitor.go | 1 - ...s_state.down.sql => 000294_workspace_monitors_state.down.sql} | 0 ...itors_state.up.sql => 000294_workspace_monitors_state.up.sql} | 0 3 files changed, 1 deletion(-) rename coderd/database/migrations/{000293_workspace_monitors_state.down.sql => 000294_workspace_monitors_state.down.sql} (100%) rename coderd/database/migrations/{000293_workspace_monitors_state.up.sql => 000294_workspace_monitors_state.up.sql} (100%) diff --git a/coderd/agentapi/resourcesmonitor/resources_monitor.go b/coderd/agentapi/resourcesmonitor/resources_monitor.go index deee4e0c862a1..9b1749cd0abd6 100644 --- a/coderd/agentapi/resourcesmonitor/resources_monitor.go +++ b/coderd/agentapi/resourcesmonitor/resources_monitor.go @@ -89,7 +89,6 @@ func CalculateVolumeUsageStates( } func NextState(c Config, oldState database.WorkspaceAgentMonitorState, states []State) database.WorkspaceAgentMonitorState { - // If there are enough consecutive NOK states, we should be in an // alert state. consecutiveNOKs := slice.CountConsecutive(StateNOK, states...) diff --git a/coderd/database/migrations/000293_workspace_monitors_state.down.sql b/coderd/database/migrations/000294_workspace_monitors_state.down.sql similarity index 100% rename from coderd/database/migrations/000293_workspace_monitors_state.down.sql rename to coderd/database/migrations/000294_workspace_monitors_state.down.sql diff --git a/coderd/database/migrations/000293_workspace_monitors_state.up.sql b/coderd/database/migrations/000294_workspace_monitors_state.up.sql similarity index 100% rename from coderd/database/migrations/000293_workspace_monitors_state.up.sql rename to coderd/database/migrations/000294_workspace_monitors_state.up.sql