mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat: notify on workspace update (#15979)
Relates to https://github.com/coder/coder/issues/15845 When the `/workspace/<name>/builds` endpoint is hit, we check if the requested template version is different to the previously used template version. If these values differ, we can assume that the workspace has been manually updated and send the appropriate notification. Automatic updates happen in the lifecycle executor and bypasses this endpoint entirely.
This commit is contained in:
@ -0,0 +1 @@
|
|||||||
|
DELETE FROM notification_templates WHERE id = 'd089fe7b-d5c5-4c0c-aaf5-689859f7d392';
|
@ -0,0 +1,30 @@
|
|||||||
|
INSERT INTO notification_templates
|
||||||
|
(id, name, title_template, body_template, "group", actions)
|
||||||
|
VALUES (
|
||||||
|
'd089fe7b-d5c5-4c0c-aaf5-689859f7d392',
|
||||||
|
'Workspace Manually Updated',
|
||||||
|
E'Workspace ''{{.Labels.workspace}}'' has been manually updated',
|
||||||
|
E'Hello {{.UserName}},\n\n'||
|
||||||
|
E'A new workspace build has been manually created for your workspace **{{.Labels.workspace}}** by **{{.Labels.initiator}}** to update it to version **{{.Labels.version}}** of template **{{.Labels.template}}**.',
|
||||||
|
'Workspace Events',
|
||||||
|
'[
|
||||||
|
{
|
||||||
|
"label": "View workspace",
|
||||||
|
"url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "View template version",
|
||||||
|
"url": "{{base_url}}/templates/{{.Labels.organization}}/{{.Labels.template}}/versions/{{.Labels.version}}"
|
||||||
|
}
|
||||||
|
]'::jsonb
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE notification_templates
|
||||||
|
SET
|
||||||
|
actions = '[
|
||||||
|
{
|
||||||
|
"label": "View workspace",
|
||||||
|
"url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"
|
||||||
|
}
|
||||||
|
]'::jsonb
|
||||||
|
WHERE id = '281fdf73-c6d6-4cbb-8ff5-888baf8a2fff';
|
@ -8,6 +8,7 @@ import "github.com/google/uuid"
|
|||||||
// Workspace-related events.
|
// Workspace-related events.
|
||||||
var (
|
var (
|
||||||
TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff")
|
TemplateWorkspaceCreated = uuid.MustParse("281fdf73-c6d6-4cbb-8ff5-888baf8a2fff")
|
||||||
|
TemplateWorkspaceManuallyUpdated = uuid.MustParse("d089fe7b-d5c5-4c0c-aaf5-689859f7d392")
|
||||||
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
|
TemplateWorkspaceDeleted = uuid.MustParse("f517da0b-cdc9-410f-ab89-a86107c420ed")
|
||||||
TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
|
TemplateWorkspaceAutobuildFailed = uuid.MustParse("381df2a9-c0c0-4749-420f-80a9280c66f9")
|
||||||
TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0")
|
TemplateWorkspaceDormant = uuid.MustParse("0ea69165-ec14-4314-91f1-69566ac3c5a0")
|
||||||
|
@ -1048,6 +1048,22 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "TemplateWorkspaceManuallyUpdated",
|
||||||
|
id: notifications.TemplateWorkspaceManuallyUpdated,
|
||||||
|
payload: types.MessagePayload{
|
||||||
|
UserName: "Bobby",
|
||||||
|
UserEmail: "bobby@coder.com",
|
||||||
|
UserUsername: "bobby",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"organization": "bobby-organization",
|
||||||
|
"initiator": "bobby",
|
||||||
|
"workspace": "bobby-workspace",
|
||||||
|
"template": "bobby-template",
|
||||||
|
"version": "alpha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must have a test case for every notification_template. This is enforced below:
|
// We must have a test case for every notification_template. This is enforced below:
|
||||||
|
@ -16,7 +16,7 @@ The workspace bobby-workspace has been created from the template bobby-temp=
|
|||||||
late using version alpha.
|
late using version alpha.
|
||||||
|
|
||||||
|
|
||||||
See workspace: http://test.com/@bobby/bobby-workspace
|
View workspace: http://test.com/@bobby/bobby-workspace
|
||||||
|
|
||||||
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||||
Content-Transfer-Encoding: quoted-printable
|
Content-Transfer-Encoding: quoted-printable
|
||||||
@ -57,7 +57,7 @@ ng>.</p>
|
|||||||
<a href=3D"http://test.com/@bobby/bobby-workspace" style=3D"display=
|
<a href=3D"http://test.com/@bobby/bobby-workspace" style=3D"display=
|
||||||
: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fa=
|
: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fa=
|
||||||
fc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
|
fc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
|
||||||
See workspace
|
View workspace
|
||||||
</a>
|
</a>
|
||||||
=20
|
=20
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
From: system@coder.com
|
||||||
|
To: bobby@coder.com
|
||||||
|
Subject: Workspace 'bobby-workspace' has been manually updated
|
||||||
|
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
|
||||||
|
|
||||||
|
Hello Bobby,
|
||||||
|
|
||||||
|
A new workspace build has been manually created for your workspace bobby-wo=
|
||||||
|
rkspace by bobby to update it to version alpha of template bobby-template.
|
||||||
|
|
||||||
|
|
||||||
|
View workspace: http://test.com/@bobby/bobby-workspace
|
||||||
|
|
||||||
|
View template version: http://test.com/templates/bobby-organization/bobby-t=
|
||||||
|
emplate/versions/alpha
|
||||||
|
|
||||||
|
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang=3D"en">
|
||||||
|
<head>
|
||||||
|
<meta charset=3D"UTF-8" />
|
||||||
|
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
|
||||||
|
=3D1.0" />
|
||||||
|
<title>Workspace 'bobby-workspace' has been manually updated</title>
|
||||||
|
</head>
|
||||||
|
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
|
||||||
|
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
|
||||||
|
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
|
||||||
|
; background: #f8fafc;">
|
||||||
|
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
|
||||||
|
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
|
||||||
|
n: left; font-size: 14px; line-height: 1.5;">
|
||||||
|
<div style=3D"text-align: center;">
|
||||||
|
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
|
||||||
|
er Logo" style=3D"height: 40px;" />
|
||||||
|
</div>
|
||||||
|
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
|
||||||
|
argin: 8px 0 32px; line-height: 1.5;">
|
||||||
|
Workspace 'bobby-workspace' has been manually updated
|
||||||
|
</h1>
|
||||||
|
<div style=3D"line-height: 1.5;">
|
||||||
|
<p>Hello Bobby,</p>
|
||||||
|
|
||||||
|
<p>A new workspace build has been manually created for your workspace <stro=
|
||||||
|
ng>bobby-workspace</strong> by <strong>bobby</strong> to update it to versi=
|
||||||
|
on <strong>alpha</strong> of template <strong>bobby-template</strong>.</p>
|
||||||
|
</div>
|
||||||
|
<div style=3D"text-align: center; margin-top: 32px;">
|
||||||
|
=20
|
||||||
|
<a href=3D"http://test.com/@bobby/bobby-workspace" style=3D"display=
|
||||||
|
: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fa=
|
||||||
|
fc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
|
||||||
|
View workspace
|
||||||
|
</a>
|
||||||
|
=20
|
||||||
|
<a href=3D"http://test.com/templates/bobby-organization/bobby-templ=
|
||||||
|
ate/versions/alpha" style=3D"display: inline-block; padding: 13px 24px; bac=
|
||||||
|
kground-color: #020617; color: #f8fafc; text-decoration: none; border-radiu=
|
||||||
|
s: 8px; margin: 0 4px;">
|
||||||
|
View template version
|
||||||
|
</a>
|
||||||
|
=20
|
||||||
|
</div>
|
||||||
|
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
|
||||||
|
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
|
||||||
|
<p>© 2024 Coder. All rights reserved - <a =
|
||||||
|
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
|
||||||
|
ttp://test.com</a></p>
|
||||||
|
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
|
||||||
|
r: #2563eb; text-decoration: none;">Click here to manage your notification =
|
||||||
|
settings</a></p>
|
||||||
|
<p><a href=3D"http://test.com/settings/notifications?disabled=3Dd08=
|
||||||
|
9fe7b-d5c5-4c0c-aaf5-689859f7d392" style=3D"color: #2563eb; text-decoration=
|
||||||
|
: none;">Stop receiving emails like this</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
|
@ -11,7 +11,7 @@
|
|||||||
"user_username": "bobby",
|
"user_username": "bobby",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"label": "See workspace",
|
"label": "View workspace",
|
||||||
"url": "http://test.com/@bobby/bobby-workspace"
|
"url": "http://test.com/@bobby/bobby-workspace"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"_version": "1.1",
|
||||||
|
"msg_id": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"payload": {
|
||||||
|
"_version": "1.1",
|
||||||
|
"notification_name": "Workspace Manually Updated",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "View template version",
|
||||||
|
"url": "http://test.com/templates/bobby-organization/bobby-template/versions/alpha"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"labels": {
|
||||||
|
"initiator": "bobby",
|
||||||
|
"organization": "bobby-organization",
|
||||||
|
"template": "bobby-template",
|
||||||
|
"version": "alpha",
|
||||||
|
"workspace": "bobby-workspace"
|
||||||
|
},
|
||||||
|
"data": null
|
||||||
|
},
|
||||||
|
"title": "Workspace 'bobby-workspace' has been manually updated",
|
||||||
|
"title_markdown": "Workspace 'bobby-workspace' has been manually updated",
|
||||||
|
"body": "Hello Bobby,\n\nA new workspace build has been manually created for your workspace bobby-workspace by bobby to update it to version alpha of template bobby-template.",
|
||||||
|
"body_markdown": "Hello Bobby,\n\nA new workspace build has been manually created for your workspace **bobby-workspace** by **bobby** to update it to version **alpha** of template **bobby-template**."
|
||||||
|
}
|
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
|
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
|
||||||
"github.com/coder/coder/v2/coderd/httpapi"
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
"github.com/coder/coder/v2/coderd/httpmw"
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
"github.com/coder/coder/v2/coderd/provisionerdserver"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
@ -333,37 +334,59 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||||||
LogLevel(string(createBuild.LogLevel)).
|
LogLevel(string(createBuild.LogLevel)).
|
||||||
DeploymentValues(api.Options.DeploymentValues)
|
DeploymentValues(api.Options.DeploymentValues)
|
||||||
|
|
||||||
if createBuild.TemplateVersionID != uuid.Nil {
|
var (
|
||||||
builder = builder.VersionID(createBuild.TemplateVersionID)
|
previousWorkspaceBuild database.WorkspaceBuild
|
||||||
}
|
workspaceBuild *database.WorkspaceBuild
|
||||||
|
provisionerJob *database.ProvisionerJob
|
||||||
|
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
|
||||||
|
)
|
||||||
|
|
||||||
if createBuild.Orphan {
|
err := api.Database.InTx(func(tx database.Store) error {
|
||||||
if createBuild.Transition != codersdk.WorkspaceTransitionDelete {
|
var err error
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
||||||
Message: "Orphan is only permitted when deleting a workspace.",
|
previousWorkspaceBuild, err = tx.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
||||||
|
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||||
|
api.Logger.Error(ctx, "failed fetching previous workspace build", slog.F("workspace_id", workspace.ID), slog.Error(err))
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Internal error fetching previous workspace build",
|
||||||
|
Detail: err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if createBuild.TemplateVersionID != uuid.Nil {
|
||||||
|
builder = builder.VersionID(createBuild.TemplateVersionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if createBuild.Orphan {
|
||||||
|
if createBuild.Transition != codersdk.WorkspaceTransitionDelete {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Orphan is only permitted when deleting a workspace.",
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(createBuild.ProvisionerState) > 0 {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.",
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
builder = builder.Orphan()
|
||||||
}
|
}
|
||||||
if len(createBuild.ProvisionerState) > 0 {
|
if len(createBuild.ProvisionerState) > 0 {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
builder = builder.State(createBuild.ProvisionerState)
|
||||||
Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
builder = builder.Orphan()
|
|
||||||
}
|
|
||||||
if len(createBuild.ProvisionerState) > 0 {
|
|
||||||
builder = builder.State(createBuild.ProvisionerState)
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceBuild, provisionerJob, provisionerDaemons, err := builder.Build(
|
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(
|
||||||
ctx,
|
ctx,
|
||||||
api.Database,
|
tx,
|
||||||
func(action policy.Action, object rbac.Objecter) bool {
|
func(action policy.Action, object rbac.Objecter) bool {
|
||||||
return api.Authorize(r, action, object)
|
return api.Authorize(r, action, object)
|
||||||
},
|
},
|
||||||
audit.WorkspaceBuildBaggageFromRequest(r),
|
audit.WorkspaceBuildBaggageFromRequest(r),
|
||||||
)
|
)
|
||||||
|
return err
|
||||||
|
}, nil)
|
||||||
var buildErr wsbuilder.BuildError
|
var buildErr wsbuilder.BuildError
|
||||||
if xerrors.As(err, &buildErr) {
|
if xerrors.As(err, &buildErr) {
|
||||||
var authErr dbauthz.NotAuthorizedError
|
var authErr dbauthz.NotAuthorizedError
|
||||||
@ -420,6 +443,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this workspace build has a different template version ID to the previous build
|
||||||
|
// we can assume it has just been updated.
|
||||||
|
if createBuild.TemplateVersionID != uuid.Nil && createBuild.TemplateVersionID != previousWorkspaceBuild.TemplateVersionID {
|
||||||
|
api.notifyWorkspaceUpdated(ctx, apiKey.UserID, workspace, createBuild.RichParameterValues)
|
||||||
|
}
|
||||||
|
|
||||||
api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{
|
api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{
|
||||||
Kind: wspubsub.WorkspaceEventKindStateChange,
|
Kind: wspubsub.WorkspaceEventKindStateChange,
|
||||||
WorkspaceID: workspace.ID,
|
WorkspaceID: workspace.ID,
|
||||||
@ -428,6 +457,73 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||||||
httpapi.Write(ctx, rw, http.StatusCreated, apiBuild)
|
httpapi.Write(ctx, rw, http.StatusCreated, apiBuild)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) notifyWorkspaceUpdated(
|
||||||
|
ctx context.Context,
|
||||||
|
initiatorID uuid.UUID,
|
||||||
|
workspace database.Workspace,
|
||||||
|
parameters []codersdk.WorkspaceBuildParameter,
|
||||||
|
) {
|
||||||
|
log := api.Logger.With(slog.F("workspace_id", workspace.ID))
|
||||||
|
|
||||||
|
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(ctx, "failed to fetch template for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(ctx, "failed to fetch template version for workspace creation notification", slog.F("template_id", workspace.TemplateID), slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
initiator, err := api.Database.GetUserByID(ctx, initiatorID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("initiator_id", initiatorID), slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(ctx, "failed to fetch user for workspace update notification", slog.F("owner_id", workspace.OwnerID), slog.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParameters := make([]map[string]any, len(parameters))
|
||||||
|
for idx, parameter := range parameters {
|
||||||
|
buildParameters[idx] = map[string]any{
|
||||||
|
"name": parameter.Name,
|
||||||
|
"value": parameter.Value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
|
||||||
|
// nolint:gocritic // Need notifier actor to enqueue notifications
|
||||||
|
dbauthz.AsNotifier(ctx),
|
||||||
|
workspace.OwnerID,
|
||||||
|
notifications.TemplateWorkspaceManuallyUpdated,
|
||||||
|
map[string]string{
|
||||||
|
"organization": template.OrganizationName,
|
||||||
|
"initiator": initiator.Name,
|
||||||
|
"workspace": workspace.Name,
|
||||||
|
"template": template.Name,
|
||||||
|
"version": version.Name,
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"workspace": map[string]any{"id": workspace.ID, "name": workspace.Name},
|
||||||
|
"template": map[string]any{"id": template.ID, "name": template.Name},
|
||||||
|
"template_version": map[string]any{"id": version.ID, "name": version.Name},
|
||||||
|
"owner": map[string]any{"id": owner.ID, "name": owner.Name},
|
||||||
|
"parameters": buildParameters,
|
||||||
|
},
|
||||||
|
"api-workspaces-updated",
|
||||||
|
// Associate this notification with all the related entities
|
||||||
|
workspace.ID, workspace.OwnerID, workspace.TemplateID, workspace.OrganizationID,
|
||||||
|
); err != nil {
|
||||||
|
log.Warn(ctx, "failed to notify of workspace update", slog.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @Summary Cancel workspace build
|
// @Summary Cancel workspace build
|
||||||
// @ID cancel-workspace-build
|
// @ID cancel-workspace-build
|
||||||
// @Security CoderSessionToken
|
// @Security CoderSessionToken
|
||||||
|
@ -27,6 +27,8 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||||
"github.com/coder/coder/v2/coderd/externalauth"
|
"github.com/coder/coder/v2/coderd/externalauth"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications"
|
||||||
|
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/provisioner/echo"
|
"github.com/coder/coder/v2/provisioner/echo"
|
||||||
@ -560,6 +562,104 @@ func TestWorkspaceBuildResources(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OnlyOneNotification", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
notify := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify})
|
||||||
|
first := coderdtest.CreateFirstUser(t, client)
|
||||||
|
userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||||
|
|
||||||
|
// Create a template with an initial version
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
|
||||||
|
|
||||||
|
// Create a workspace using this template
|
||||||
|
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||||
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||||
|
|
||||||
|
// Create a new version of the template
|
||||||
|
newVersion := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||||
|
ctvr.TemplateID = template.ID
|
||||||
|
})
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID)
|
||||||
|
|
||||||
|
// Create a workspace build using this new template version
|
||||||
|
build := coderdtest.CreateWorkspaceBuild(t, userClient, workspace, database.WorkspaceTransitionStart, func(cwbr *codersdk.CreateWorkspaceBuildRequest) {
|
||||||
|
cwbr.TemplateVersionID = newVersion.ID
|
||||||
|
})
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
|
||||||
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||||
|
|
||||||
|
// Create the workspace build _again_. We are doing this to ensure we only create 1 notification.
|
||||||
|
build = coderdtest.CreateWorkspaceBuild(t, userClient, workspace, database.WorkspaceTransitionStart, func(cwbr *codersdk.CreateWorkspaceBuildRequest) {
|
||||||
|
cwbr.TemplateVersionID = newVersion.ID
|
||||||
|
})
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
|
||||||
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||||
|
|
||||||
|
// Ensure we receive only 1 workspace manually updated notification
|
||||||
|
sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated))
|
||||||
|
require.Len(t, sent, 1)
|
||||||
|
require.Equal(t, user.ID, sent[0].UserID)
|
||||||
|
require.Contains(t, sent[0].Targets, template.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ToCorrectUser", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
notify := ¬ificationstest.FakeEnqueuer{}
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify})
|
||||||
|
first := coderdtest.CreateFirstUser(t, client)
|
||||||
|
userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||||
|
|
||||||
|
// Create a template with an initial version
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
|
||||||
|
|
||||||
|
// Create a workspace using this template
|
||||||
|
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||||
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||||
|
|
||||||
|
// Create a new version of the template
|
||||||
|
newVersion := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||||
|
ctvr.TemplateID = template.ID
|
||||||
|
})
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID)
|
||||||
|
|
||||||
|
// Create a workspace build using this new template version from a different user
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||||
|
Transition: codersdk.WorkspaceTransitionStart,
|
||||||
|
TemplateVersionID: newVersion.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
|
||||||
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||||
|
|
||||||
|
// Ensure we receive only 1 workspace manually updated notification and to the right user
|
||||||
|
sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated))
|
||||||
|
require.Len(t, sent, 1)
|
||||||
|
require.Equal(t, user.ID, sent[0].UserID)
|
||||||
|
require.Contains(t, sent[0].Targets, template.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
||||||
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func assertWorkspaceResource(t *testing.T, actual codersdk.WorkspaceResource, name, aType string, numAgents int) {
|
func assertWorkspaceResource(t *testing.T, actual codersdk.WorkspaceResource, name, aType string, numAgents int) {
|
||||||
assert.Equal(t, name, actual.Name)
|
assert.Equal(t, name, actual.Name)
|
||||||
assert.Equal(t, aType, actual.Type)
|
assert.Equal(t, aType, actual.Type)
|
||||||
|
Reference in New Issue
Block a user