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 + + + + + + + Workspace "bobby-workspace" reached resource threshold + + +
+
+ 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 + + + + + + + Your workspace "bobby-workspace" is low on disk + + +
+
+ 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 + + + + + + + Your workspace "bobby-workspace" is low on memory + + +
+
+ 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 - Your workspace "bobby-workspace" is low on disk + Your workspace "bobby-workspace" is low on volume space

- 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 + + + + + + + Your workspace "bobby-workspace" is low on volume space + + +
+
+ 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 + + + + + + + Your workspace "bobby-workspace" is low on volume space + + +
+
+ 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 - - - - - - - Your workspace "bobby-workspace" is low on volume space - - -
-
- 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 - - - - - - - Workspace "bobby-workspace" reached resource threshold - - -
-
- 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy