internal/relui: add approved_at to tasks
This change adds an approved_at column to the tasks table for checking
task approval, replacing the flimsy log-based determination for whether
a task has been approved.
Updates golang/go#53295
Change-Id: I3c2bd73134dc4a81e13c328aa085065a7b24a5d5
Reviewed-on: https://go-review.googlesource.com/c/build/+/413427
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alex Rakoczy <alex@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Alex Rakoczy <alex@golang.org>
diff --git a/internal/relui/db/models.go b/internal/relui/db/models.go
index c376a03..8377f14 100644
--- a/internal/relui/db/models.go
+++ b/internal/relui/db/models.go
@@ -19,6 +19,7 @@
Error sql.NullString
CreatedAt time.Time
UpdatedAt time.Time
+ ApprovedAt sql.NullTime
}
type TaskLog struct {
diff --git a/internal/relui/db/workflows.sql.go b/internal/relui/db/workflows.sql.go
index ed512da..9ea56e8 100644
--- a/internal/relui/db/workflows.sql.go
+++ b/internal/relui/db/workflows.sql.go
@@ -13,10 +13,41 @@
"github.com/google/uuid"
)
+const approveTask = `-- name: ApproveTask :one
+UPDATE tasks
+SET approved_at = $3,
+ updated_at = $3
+WHERE workflow_id = $1
+ AND name = $2
+RETURNING workflow_id, name, finished, result, error, created_at, updated_at, approved_at
+`
+
+type ApproveTaskParams struct {
+ WorkflowID uuid.UUID
+ Name string
+ ApprovedAt sql.NullTime
+}
+
+func (q *Queries) ApproveTask(ctx context.Context, arg ApproveTaskParams) (Task, error) {
+ row := q.db.QueryRow(ctx, approveTask, arg.WorkflowID, arg.Name, arg.ApprovedAt)
+ var i Task
+ err := row.Scan(
+ &i.WorkflowID,
+ &i.Name,
+ &i.Finished,
+ &i.Result,
+ &i.Error,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ &i.ApprovedAt,
+ )
+ return i, err
+}
+
const createTask = `-- name: CreateTask :one
-INSERT INTO tasks (workflow_id, name, finished, result, error, created_at, updated_at)
-VALUES ($1, $2, $3, $4, $5, $6, $7)
-RETURNING workflow_id, name, finished, result, error, created_at, updated_at
+INSERT INTO tasks (workflow_id, name, finished, result, error, created_at, updated_at, approved_at)
+VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+RETURNING workflow_id, name, finished, result, error, created_at, updated_at, approved_at
`
type CreateTaskParams struct {
@@ -27,6 +58,7 @@
Error sql.NullString
CreatedAt time.Time
UpdatedAt time.Time
+ ApprovedAt sql.NullTime
}
func (q *Queries) CreateTask(ctx context.Context, arg CreateTaskParams) (Task, error) {
@@ -38,6 +70,7 @@
arg.Error,
arg.CreatedAt,
arg.UpdatedAt,
+ arg.ApprovedAt,
)
var i Task
err := row.Scan(
@@ -48,6 +81,7 @@
&i.Error,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.ApprovedAt,
)
return i, err
}
@@ -122,7 +156,7 @@
updated_at = $3
WHERE workflow_id = $1
AND name = $2
-RETURNING workflow_id, name, finished, result, error, created_at, updated_at
+RETURNING workflow_id, name, finished, result, error, created_at, updated_at, approved_at
`
type ResetTaskParams struct {
@@ -142,6 +176,7 @@
&i.Error,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.ApprovedAt,
)
return i, err
}
@@ -178,7 +213,7 @@
}
const task = `-- name: Task :one
-SELECT tasks.workflow_id, tasks.name, tasks.finished, tasks.result, tasks.error, tasks.created_at, tasks.updated_at
+SELECT tasks.workflow_id, tasks.name, tasks.finished, tasks.result, tasks.error, tasks.created_at, tasks.updated_at, tasks.approved_at
FROM tasks
WHERE workflow_id = $1
AND name = $2
@@ -201,6 +236,7 @@
&i.Error,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.ApprovedAt,
)
return i, err
}
@@ -279,7 +315,7 @@
}
const tasks = `-- name: Tasks :many
-SELECT tasks.workflow_id, tasks.name, tasks.finished, tasks.result, tasks.error, tasks.created_at, tasks.updated_at
+SELECT tasks.workflow_id, tasks.name, tasks.finished, tasks.result, tasks.error, tasks.created_at, tasks.updated_at, tasks.approved_at
FROM tasks
ORDER BY updated_at
`
@@ -301,6 +337,7 @@
&i.Error,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.ApprovedAt,
); err != nil {
return nil, err
}
@@ -313,7 +350,7 @@
}
const tasksForWorkflow = `-- name: TasksForWorkflow :many
-SELECT tasks.workflow_id, tasks.name, tasks.finished, tasks.result, tasks.error, tasks.created_at, tasks.updated_at
+SELECT tasks.workflow_id, tasks.name, tasks.finished, tasks.result, tasks.error, tasks.created_at, tasks.updated_at, tasks.approved_at
FROM tasks
WHERE workflow_id = $1
ORDER BY created_at
@@ -336,6 +373,7 @@
&i.Error,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.ApprovedAt,
); err != nil {
return nil, err
}
@@ -392,7 +430,7 @@
result = excluded.result,
error = excluded.error,
updated_at = excluded.updated_at
-RETURNING workflow_id, name, finished, result, error, created_at, updated_at
+RETURNING workflow_id, name, finished, result, error, created_at, updated_at, approved_at
`
type UpsertTaskParams struct {
@@ -424,6 +462,7 @@
&i.Error,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.ApprovedAt,
)
return i, err
}
diff --git a/internal/relui/migrations/20220621201140_add_approved_at_to_tasks.down.sql b/internal/relui/migrations/20220621201140_add_approved_at_to_tasks.down.sql
new file mode 100644
index 0000000..62eaa1b
--- /dev/null
+++ b/internal/relui/migrations/20220621201140_add_approved_at_to_tasks.down.sql
@@ -0,0 +1,29 @@
+-- Copyright 2022 The Go Authors. All rights reserved.
+-- Use of this source code is governed by a BSD-style
+-- license that can be found in the LICENSE file.
+
+BEGIN;
+
+WITH approved_task_logs AS (
+ SELECT DISTINCT workflow_id, task_name
+ FROM task_logs
+ WHERE body = 'USER-APPROVED'
+)
+
+INSERT
+INTO task_logs (workflow_id, task_name, body, created_at, updated_at)
+SELECT tasks.workflow_id, tasks.name, 'USER-APPROVED', tasks.approved_at, tasks.approved_at
+FROM tasks
+WHERE tasks.approved_at IS NOT NULL
+ AND NOT EXISTS(
+ SELECT DISTINCT 1
+ FROM approved_task_logs atl
+ WHERE atl.workflow_id = tasks.workflow_id
+ AND atl.task_name = tasks.name
+ )
+;
+
+ALTER TABLE tasks
+ DROP COLUMN approved_at;
+
+COMMIT;
diff --git a/internal/relui/migrations/20220621201140_add_approved_at_to_tasks.up.sql b/internal/relui/migrations/20220621201140_add_approved_at_to_tasks.up.sql
new file mode 100644
index 0000000..0e51d8d
--- /dev/null
+++ b/internal/relui/migrations/20220621201140_add_approved_at_to_tasks.up.sql
@@ -0,0 +1,23 @@
+-- Copyright 2022 The Go Authors. All rights reserved.
+-- Use of this source code is governed by a BSD-style
+-- license that can be found in the LICENSE file.
+
+BEGIN;
+
+ALTER TABLE tasks
+ ADD COLUMN approved_at timestamp WITH TIME ZONE NULL;
+
+WITH approved_tasks AS (
+ SELECT workflow_id, task_name, max(created_at) AS created_at
+ FROM task_logs
+ WHERE body = 'USER-APPROVED'
+ GROUP BY workflow_id, task_name
+)
+
+UPDATE tasks
+SET approved_at = approved_tasks.created_at
+FROM approved_tasks
+WHERE tasks.workflow_id = approved_tasks.workflow_id
+ AND tasks.name = approved_tasks.task_name;
+
+COMMIT;
diff --git a/internal/relui/queries/workflows.sql b/internal/relui/queries/workflows.sql
index 09c8e52..f85926b 100644
--- a/internal/relui/queries/workflows.sql
+++ b/internal/relui/queries/workflows.sql
@@ -18,8 +18,8 @@
RETURNING *;
-- name: CreateTask :one
-INSERT INTO tasks (workflow_id, name, finished, result, error, created_at, updated_at)
-VALUES ($1, $2, $3, $4, $5, $6, $7)
+INSERT INTO tasks (workflow_id, name, finished, result, error, created_at, updated_at, approved_at)
+VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: UpsertTask :one
@@ -101,3 +101,11 @@
updated_at = $2
WHERE id = $1
RETURNING *;
+
+-- name: ApproveTask :one
+UPDATE tasks
+SET approved_at = $3,
+ updated_at = $3
+WHERE workflow_id = $1
+ AND name = $2
+RETURNING *;
diff --git a/internal/relui/static/styles.css b/internal/relui/static/styles.css
index 56a0469..cc77db1 100644
--- a/internal/relui/static/styles.css
+++ b/internal/relui/static/styles.css
@@ -231,6 +231,11 @@
color: white;
padding: 0.5rem 1rem;
}
+.TaskList-itemLogLineApproved {
+ background-color: #3b65b3;
+ color: white;
+ padding: 0.5rem 1rem;
+}
.TaskList-itemHeader {
align-items: center;
font-size: 0.8125rem;
diff --git a/internal/relui/templates/task_list.html b/internal/relui/templates/task_list.html
index c198478..48cfc4f 100644
--- a/internal/relui/templates/task_list.html
+++ b/internal/relui/templates/task_list.html
@@ -54,7 +54,11 @@
{{$task.UpdatedAt.UTC.Format "Mon Jan _2 2006 15:04:05"}}
</td>
<td class="TaskList-itemCol TaskList-itemResult">
- {{ $resultDetail.Kind }}
+ {{if $task.ApprovedAt.Valid}}
+ Approved
+ {{else}}
+ {{ $resultDetail.Kind }}
+ {{end}}
</td>
<td class="TaskList-itemCol TaskList-itemAction">
{{if $task.Error.Valid}}
@@ -65,7 +69,7 @@
</form>
</div>
{{end}}
- {{if and (not $task.Finished) (hasPrefix $task.Name "APPROVE-")}}
+ {{if and (not $task.ApprovedAt.Valid) (hasPrefix $task.Name "APPROVE-")}}
<div class="TaskList-approveTask">
<form action="{{baseLink (printf "/workflows/%s/tasks/%s/approve" $workflow.ID $task.Name)}}" method="post">
<input type="hidden" id="workflow.id" name="workflow.id" value="{{$workflow.ID}}" />
@@ -82,6 +86,11 @@
{{- $task.Error.Value -}}
</div>
{{end}}
+ {{if $task.ApprovedAt.Valid}}
+ <div class="TaskList-itemLogLine TaskList-itemLogLineApproved">
+ {{- printf "Approved at: %s" ($task.ApprovedAt.Value.UTC.Format "2006/01/02 15:04:05") -}}
+ </div>
+ {{end}}
{{range $log := index $.TaskLogs $task.Name}}
<div class="TaskList-itemLogLine">
{{- printf "%s %s" ($log.CreatedAt.UTC.Format "2006/01/02 15:04:05") $log.Body -}}
diff --git a/internal/relui/web.go b/internal/relui/web.go
index b44de81..651763e 100644
--- a/internal/relui/web.go
+++ b/internal/relui/web.go
@@ -329,16 +329,19 @@
return
}
q := db.New(s.db)
- t, err := q.Task(r.Context(), db.TaskParams{WorkflowID: id, Name: params.ByName("name")})
+ t, err := q.ApproveTask(r.Context(), db.ApproveTaskParams{
+ WorkflowID: id,
+ Name: params.ByName("name"),
+ ApprovedAt: sql.NullTime{Time: time.Now(), Valid: true},
+ })
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, pgx.ErrNoRows) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
} else if err != nil {
- log.Printf("q.Task(_, %q): %v", id, err)
+ log.Printf("q.ApproveTask(_, %q) = %v, %v", id, t, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
- // This log entry serves as approval.
s.w.l.Logger(id, t.Name).Printf("USER-APPROVED")
http.Redirect(w, r, s.BaseLink("/"), http.StatusSeeOther)
}
diff --git a/internal/relui/web_test.go b/internal/relui/web_test.go
index c3c72f0..25b8f8c 100644
--- a/internal/relui/web_test.go
+++ b/internal/relui/web_test.go
@@ -585,41 +585,65 @@
params map[string]string
wantCode int
wantHeaders map[string]string
- wantLogs []db.TaskLog
+ want db.Task
}{
{
desc: "no params",
wantCode: http.StatusNotFound,
+ want: db.Task{
+ WorkflowID: wfID,
+ Name: "APPROVE-please",
+ CreatedAt: hourAgo,
+ UpdatedAt: hourAgo,
+ },
},
{
desc: "invalid workflow id",
params: map[string]string{"id": "invalid", "name": "greeting"},
wantCode: http.StatusBadRequest,
+ want: db.Task{
+ WorkflowID: wfID,
+ Name: "APPROVE-please",
+ CreatedAt: hourAgo,
+ UpdatedAt: hourAgo,
+ },
},
{
desc: "wrong workflow id",
params: map[string]string{"id": uuid.New().String(), "name": "greeting"},
wantCode: http.StatusNotFound,
+ want: db.Task{
+ WorkflowID: wfID,
+ Name: "APPROVE-please",
+ CreatedAt: hourAgo,
+ UpdatedAt: hourAgo,
+ },
},
{
desc: "invalid task name",
params: map[string]string{"id": wfID.String(), "name": "invalid"},
wantCode: http.StatusNotFound,
+ want: db.Task{
+ WorkflowID: wfID,
+ Name: "APPROVE-please",
+ CreatedAt: hourAgo,
+ UpdatedAt: hourAgo,
+ },
},
{
- desc: "successful reset",
+ desc: "successful approval",
params: map[string]string{"id": wfID.String(), "name": "APPROVE-please"},
wantCode: http.StatusSeeOther,
wantHeaders: map[string]string{
"Location": "/",
},
- wantLogs: []db.TaskLog{{
+ want: db.Task{
WorkflowID: wfID,
- TaskName: "APPROVE-please",
- Body: "USER-APPROVED",
- CreatedAt: time.Now(),
+ Name: "APPROVE-please",
+ CreatedAt: hourAgo,
UpdatedAt: time.Now(),
- }},
+ ApprovedAt: sql.NullTime{Time: time.Now(), Valid: true},
+ },
},
}
for _, c := range cases {
@@ -640,8 +664,7 @@
gtg := db.CreateTaskParams{
WorkflowID: wf.ID,
Name: "APPROVE-please",
- Finished: true,
- Error: nullString("internal explosion"),
+ Finished: false,
CreatedAt: hourAgo,
UpdatedAt: hourAgo,
}
@@ -668,15 +691,15 @@
if c.wantCode == http.StatusBadRequest {
return
}
- logs, err := q.TaskLogsForTask(ctx, db.TaskLogsForTaskParams{
- WorkflowID: wfID,
- TaskName: "APPROVE-please",
+ task, err := q.Task(ctx, db.TaskParams{
+ WorkflowID: wf.ID,
+ Name: "APPROVE-please",
})
if err != nil {
- t.Fatalf("q.TaskLogsForTask() = %v, %v, wanted no error", logs, err)
+ t.Fatalf("q.Task() = %v, %v, wanted no error", task, err)
}
- if diff := cmp.Diff(c.wantLogs, logs, SameUUIDVariant(), cmpopts.EquateApproxTime(time.Minute), cmpopts.IgnoreFields(db.TaskLog{}, "ID")); diff != "" {
- t.Fatalf("q.TaskLogsForTask() mismatch (-want +got):\n%s", diff)
+ if diff := cmp.Diff(c.want, task, cmpopts.EquateApproxTime(time.Minute)); diff != "" {
+ t.Fatalf("q.Task() mismatch (-want +got):\n%s", diff)
}
})
}
diff --git a/internal/relui/workflows.go b/internal/relui/workflows.go
index 9990ff5..56ed742 100644
--- a/internal/relui/workflows.go
+++ b/internal/relui/workflows.go
@@ -13,7 +13,6 @@
"math/rand"
"net/http"
"path"
- "strings"
"sync"
"time"
@@ -323,19 +322,11 @@
func checkTaskApproved(ctx *workflow.TaskContext, p *pgxpool.Pool, taskName string) (bool, error) {
q := db.New(p)
- logs, err := q.TaskLogsForTask(ctx, db.TaskLogsForTaskParams{
+ t, err := q.Task(ctx, db.TaskParams{
WorkflowID: ctx.WorkflowID,
- TaskName: taskName,
+ Name: taskName,
})
- if err != nil {
- return false, err
- }
- for _, l := range logs {
- if strings.Contains(l.Body, "USER-APPROVED") {
- return true, nil
- }
- }
- return false, nil
+ return t.ApprovedAt.Valid, err
}
func approveActionDep(p *pgxpool.Pool, taskName string) func(*workflow.TaskContext, interface{}) error {
diff --git a/internal/relui/workflows_test.go b/internal/relui/workflows_test.go
index 4cecad1..b81f977 100644
--- a/internal/relui/workflows_test.go
+++ b/internal/relui/workflows_test.go
@@ -2,6 +2,7 @@
import (
"context"
+ "database/sql"
"errors"
"testing"
"time"
@@ -129,14 +130,14 @@
t.Errorf("checkTaskApproved(_, %v, %q) = %t, %v wanted %t, %v", p, gtg.Name, got, err, false, nil)
}
- ctlp := db.CreateTaskLogParams{
+ atp := db.ApproveTaskParams{
WorkflowID: wf.ID,
- TaskName: gtg.Name,
- Body: "USER-APPROVED",
+ Name: gtg.Name,
+ ApprovedAt: sql.NullTime{Time: time.Now(), Valid: true},
}
- _, err = q.CreateTaskLog(ctx, ctlp)
+ _, err = q.ApproveTask(ctx, atp)
if err != nil {
- t.Errorf("CreateTaskLog(_, %v) = _, %v, wanted no error", ctlp, err)
+ t.Errorf("q.ApproveTask(_, %v) = _, %v, wanted no error", atp, err)
}
got, err = checkTaskApproved(tctx, p, gtg.Name)