Skip to content

feat: notify on workspace update #15979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = 'd089fe7b-d5c5-4c0c-aaf5-689859f7d392';
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions coderd/notifications/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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")
Expand Down
16 changes: 16 additions & 0 deletions coderd/notifications/notifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The workspace bobby-workspace has been created from the template bobby-temp=
late using version alpha.


See workspace: http://test.com/@bobby/bobby-workspace
View workspace: http://test.com/@bobby/bobby-workspace

--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
Expand Down Expand Up @@ -57,7 +57,7 @@ ng>.</p>
<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;">
See workspace
View workspace
</a>
=20
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<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--
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"user_username": "bobby",
"actions": [
{
"label": "See workspace",
"label": "View workspace",
"url": "http://test.com/@bobby/bobby-workspace"
}
],
Expand Down
Original file line number Diff line number Diff line change
@@ -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**."
}
146 changes: 121 additions & 25 deletions coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
"github.com/coder/coder/v2/coderd/httpapi"
"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/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
Expand Down Expand Up @@ -333,37 +334,59 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
LogLevel(string(createBuild.LogLevel)).
DeploymentValues(api.Options.DeploymentValues)

if createBuild.TemplateVersionID != uuid.Nil {
builder = builder.VersionID(createBuild.TemplateVersionID)
}
var (
previousWorkspaceBuild database.WorkspaceBuild
workspaceBuild *database.WorkspaceBuild
provisionerJob *database.ProvisionerJob
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
)

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.",
err := api.Database.InTx(func(tx database.Store) error {
var err error

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 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "ProvisionerState cannot be set alongside Orphan since state intent is unclear.",
})
return
builder = builder.State(createBuild.ProvisionerState)
}
builder = builder.Orphan()
}
if len(createBuild.ProvisionerState) > 0 {
builder = builder.State(createBuild.ProvisionerState)
}

workspaceBuild, provisionerJob, provisionerDaemons, err := builder.Build(
ctx,
api.Database,
func(action policy.Action, object rbac.Objecter) bool {
return api.Authorize(r, action, object)
},
audit.WorkspaceBuildBaggageFromRequest(r),
)
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(
ctx,
tx,
func(action policy.Action, object rbac.Objecter) bool {
return api.Authorize(r, action, object)
},
audit.WorkspaceBuildBaggageFromRequest(r),
)
return err
}, nil)
var buildErr wsbuilder.BuildError
if xerrors.As(err, &buildErr) {
var authErr dbauthz.NotAuthorizedError
Expand Down Expand Up @@ -420,6 +443,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
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{
Kind: wspubsub.WorkspaceEventKindStateChange,
WorkspaceID: workspace.ID,
Expand All @@ -428,6 +457,73 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
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
// @ID cancel-workspace-build
// @Security CoderSessionToken
Expand Down
Loading
Loading