mirror of https://github.com/coder/coder.git
feat: Add `codersdk.NullTime`, change workspace build deadline (#3552)
Fixes #2015 Co-authored-by: Joe Previte <jjprevite@gmail.com>
This commit is contained in:
parent
a21a6d2f4a
commit
78a24941fe
|
@ -38,8 +38,8 @@ func workspaceListRowFromWorkspace(now time.Time, usersByID map[uuid.UUID]coders
|
||||||
if !ptr.NilOrZero(workspace.TTLMillis) {
|
if !ptr.NilOrZero(workspace.TTLMillis) {
|
||||||
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
|
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
|
||||||
autostopDisplay = durationDisplay(dur)
|
autostopDisplay = durationDisplay(dur)
|
||||||
if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Deadline.After(now) && status == "Running" {
|
if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Deadline.Time.After(now) && status == "Running" {
|
||||||
remaining := time.Until(workspace.LatestBuild.Deadline)
|
remaining := time.Until(workspace.LatestBuild.Deadline.Time)
|
||||||
autostopDisplay = fmt.Sprintf("%s (%s)", autostopDisplay, relative(remaining))
|
autostopDisplay = fmt.Sprintf("%s (%s)", autostopDisplay, relative(remaining))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -280,8 +280,8 @@ func displaySchedule(workspace codersdk.Workspace, out io.Writer) error {
|
||||||
if workspace.LatestBuild.Transition != "start" {
|
if workspace.LatestBuild.Transition != "start" {
|
||||||
schedNextStop = "-"
|
schedNextStop = "-"
|
||||||
} else {
|
} else {
|
||||||
schedNextStop = workspace.LatestBuild.Deadline.In(loc).Format(timeFormat + " on " + dateFormat)
|
schedNextStop = workspace.LatestBuild.Deadline.Time.In(loc).Format(timeFormat + " on " + dateFormat)
|
||||||
schedNextStop = fmt.Sprintf("%s (in %s)", schedNextStop, durationDisplay(time.Until(workspace.LatestBuild.Deadline)))
|
schedNextStop = fmt.Sprintf("%s (in %s)", schedNextStop, durationDisplay(time.Until(workspace.LatestBuild.Deadline.Time)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -239,7 +239,7 @@ func TestScheduleOverride(t *testing.T) {
|
||||||
|
|
||||||
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
|
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
|
||||||
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
|
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
|
||||||
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
|
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline.Time, time.Minute)
|
||||||
|
|
||||||
cmd, root := clitest.New(t, cmdArgs...)
|
cmd, root := clitest.New(t, cmdArgs...)
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
|
@ -252,7 +252,7 @@ func TestScheduleOverride(t *testing.T) {
|
||||||
// Then: the deadline of the latest build is updated assuming the units are minutes
|
// Then: the deadline of the latest build is updated assuming the units are minutes
|
||||||
updated, err := client.Workspace(ctx, workspace.ID)
|
updated, err := client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline, time.Minute)
|
require.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline.Time, time.Minute)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidDuration", func(t *testing.T) {
|
t.Run("InvalidDuration", func(t *testing.T) {
|
||||||
|
@ -279,7 +279,7 @@ func TestScheduleOverride(t *testing.T) {
|
||||||
|
|
||||||
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
|
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
|
||||||
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
|
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
|
||||||
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
|
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline.Time, time.Minute)
|
||||||
|
|
||||||
cmd, root := clitest.New(t, cmdArgs...)
|
cmd, root := clitest.New(t, cmdArgs...)
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
|
@ -33,8 +33,10 @@ import (
|
||||||
"github.com/coder/coder/peer/peerwg"
|
"github.com/coder/coder/peer/peerwg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var workspacePollInterval = time.Minute
|
var (
|
||||||
var autostopNotifyCountdown = []time.Duration{30 * time.Minute}
|
workspacePollInterval = time.Minute
|
||||||
|
autostopNotifyCountdown = []time.Duration{30 * time.Minute}
|
||||||
|
)
|
||||||
|
|
||||||
func ssh() *cobra.Command {
|
func ssh() *cobra.Command {
|
||||||
var (
|
var (
|
||||||
|
@ -385,7 +387,7 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u
|
||||||
return time.Time{}, nil
|
return time.Time{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deadline = ws.LatestBuild.Deadline
|
deadline = ws.LatestBuild.Deadline.Time
|
||||||
callback = func() {
|
callback = func() {
|
||||||
ttl := deadline.Sub(now)
|
ttl := deadline.Sub(now)
|
||||||
var title, body string
|
var title, body string
|
||||||
|
|
|
@ -193,7 +193,7 @@ func TestExecutorAutostopOK(t *testing.T) {
|
||||||
|
|
||||||
// When: the autobuild executor ticks *after* the deadline:
|
// When: the autobuild executor ticks *after* the deadline:
|
||||||
go func() {
|
go func() {
|
||||||
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||||
close(tickCh)
|
close(tickCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -229,7 +229,7 @@ func TestExecutorAutostopExtend(t *testing.T) {
|
||||||
require.NotZero(t, originalDeadline)
|
require.NotZero(t, originalDeadline)
|
||||||
|
|
||||||
// Given: we extend the workspace deadline
|
// Given: we extend the workspace deadline
|
||||||
newDeadline := originalDeadline.Add(30 * time.Minute)
|
newDeadline := originalDeadline.Time.Add(30 * time.Minute)
|
||||||
err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
||||||
Deadline: newDeadline,
|
Deadline: newDeadline,
|
||||||
})
|
})
|
||||||
|
@ -237,7 +237,7 @@ func TestExecutorAutostopExtend(t *testing.T) {
|
||||||
|
|
||||||
// When: the autobuild executor ticks *after* the original deadline:
|
// When: the autobuild executor ticks *after* the original deadline:
|
||||||
go func() {
|
go func() {
|
||||||
tickCh <- originalDeadline.Add(time.Minute)
|
tickCh <- originalDeadline.Time.Add(time.Minute)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Then: nothing should happen and the workspace should stay running
|
// Then: nothing should happen and the workspace should stay running
|
||||||
|
@ -281,7 +281,7 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) {
|
||||||
|
|
||||||
// When: the autobuild executor ticks past the TTL
|
// When: the autobuild executor ticks past the TTL
|
||||||
go func() {
|
go func() {
|
||||||
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||||
close(tickCh)
|
close(tickCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -323,7 +323,7 @@ func TestExecutorAutostopNotEnabled(t *testing.T) {
|
||||||
|
|
||||||
// When: the autobuild executor ticks past the TTL
|
// When: the autobuild executor ticks past the TTL
|
||||||
go func() {
|
go func() {
|
||||||
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||||
close(tickCh)
|
close(tickCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -415,7 +415,7 @@ func TestExecutorWorkspaceAutostopBeforeDeadline(t *testing.T) {
|
||||||
|
|
||||||
// When: the autobuild executor ticks before the TTL
|
// When: the autobuild executor ticks before the TTL
|
||||||
go func() {
|
go func() {
|
||||||
tickCh <- workspace.LatestBuild.Deadline.Add(-1 * time.Minute)
|
tickCh <- workspace.LatestBuild.Deadline.Time.Add(-1 * time.Minute)
|
||||||
close(tickCh)
|
close(tickCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -447,11 +447,11 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
|
||||||
|
|
||||||
// Then: the deadline should still be the original value
|
// Then: the deadline should still be the original value
|
||||||
updated := coderdtest.MustWorkspace(t, client, workspace.ID)
|
updated := coderdtest.MustWorkspace(t, client, workspace.ID)
|
||||||
assert.WithinDuration(t, workspace.LatestBuild.Deadline, updated.LatestBuild.Deadline, time.Minute)
|
assert.WithinDuration(t, workspace.LatestBuild.Deadline.Time, updated.LatestBuild.Deadline.Time, time.Minute)
|
||||||
|
|
||||||
// When: the autobuild executor ticks after the original deadline
|
// When: the autobuild executor ticks after the original deadline
|
||||||
go func() {
|
go func() {
|
||||||
tickCh <- workspace.LatestBuild.Deadline.Add(time.Minute)
|
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Then: the workspace should stop
|
// Then: the workspace should stop
|
||||||
|
@ -478,7 +478,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
|
||||||
|
|
||||||
// When: the relentless onward march of time continues
|
// When: the relentless onward march of time continues
|
||||||
go func() {
|
go func() {
|
||||||
tickCh <- workspace.LatestBuild.Deadline.Add(newTTL + time.Minute)
|
tickCh <- workspace.LatestBuild.Deadline.Time.Add(newTTL + time.Minute)
|
||||||
close(tickCh)
|
close(tickCh)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -638,7 +638,8 @@ func convertWorkspaceBuild(
|
||||||
buildInitiator *database.User,
|
buildInitiator *database.User,
|
||||||
workspace database.Workspace,
|
workspace database.Workspace,
|
||||||
workspaceBuild database.WorkspaceBuild,
|
workspaceBuild database.WorkspaceBuild,
|
||||||
job database.ProvisionerJob) codersdk.WorkspaceBuild {
|
job database.ProvisionerJob,
|
||||||
|
) codersdk.WorkspaceBuild {
|
||||||
//nolint:unconvert
|
//nolint:unconvert
|
||||||
if workspace.ID != workspaceBuild.WorkspaceID {
|
if workspace.ID != workspaceBuild.WorkspaceID {
|
||||||
panic("workspace and build do not match")
|
panic("workspace and build do not match")
|
||||||
|
@ -671,7 +672,7 @@ func convertWorkspaceBuild(
|
||||||
InitiatorID: workspaceBuild.InitiatorID,
|
InitiatorID: workspaceBuild.InitiatorID,
|
||||||
InitiatorUsername: initiatorName,
|
InitiatorUsername: initiatorName,
|
||||||
Job: convertProvisionerJob(job),
|
Job: convertProvisionerJob(job),
|
||||||
Deadline: workspaceBuild.Deadline,
|
Deadline: codersdk.NewNullTime(workspaceBuild.Deadline, !workspaceBuild.Deadline.IsZero()),
|
||||||
Reason: codersdk.BuildReason(workspaceBuild.Reason),
|
Reason: codersdk.BuildReason(workspaceBuild.Reason),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1169,7 +1169,7 @@ func TestWorkspaceExtend(t *testing.T) {
|
||||||
|
|
||||||
workspace, err := client.Workspace(ctx, workspace.ID)
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err, "fetch provisioned workspace")
|
require.NoError(t, err, "fetch provisioned workspace")
|
||||||
oldDeadline := workspace.LatestBuild.Deadline
|
oldDeadline := workspace.LatestBuild.Deadline.Time
|
||||||
|
|
||||||
// Updating the deadline should succeed
|
// Updating the deadline should succeed
|
||||||
req := codersdk.PutExtendWorkspaceRequest{
|
req := codersdk.PutExtendWorkspaceRequest{
|
||||||
|
@ -1181,7 +1181,7 @@ func TestWorkspaceExtend(t *testing.T) {
|
||||||
// Ensure deadline set correctly
|
// Ensure deadline set correctly
|
||||||
updated, err := client.Workspace(ctx, workspace.ID)
|
updated, err := client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err, "failed to fetch updated workspace")
|
require.NoError(t, err, "failed to fetch updated workspace")
|
||||||
require.WithinDuration(t, newDeadline, updated.LatestBuild.Deadline, time.Minute)
|
require.WithinDuration(t, newDeadline, updated.LatestBuild.Deadline.Time, time.Minute)
|
||||||
|
|
||||||
// Zero time should fail
|
// Zero time should fail
|
||||||
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
err = client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
||||||
|
@ -1220,7 +1220,7 @@ func TestWorkspaceExtend(t *testing.T) {
|
||||||
// Ensure deadline still set correctly
|
// Ensure deadline still set correctly
|
||||||
updated, err = client.Workspace(ctx, workspace.ID)
|
updated, err = client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err, "failed to fetch updated workspace")
|
require.NoError(t, err, "failed to fetch updated workspace")
|
||||||
require.WithinDuration(t, oldDeadline.Add(-time.Hour), updated.LatestBuild.Deadline, time.Minute)
|
require.WithinDuration(t, oldDeadline.Add(-time.Hour), updated.LatestBuild.Deadline.Time, time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkspaceWatcher(t *testing.T) {
|
func TestWorkspaceWatcher(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package codersdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nullBytes = []byte("null")
|
||||||
|
|
||||||
|
// NullTime represents a nullable time.Time.
|
||||||
|
// @typescript-ignore NullTime
|
||||||
|
type NullTime struct {
|
||||||
|
sql.NullTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNullTime returns a new NullTime with the given time.Time.
|
||||||
|
func NewNullTime(t time.Time, valid bool) NullTime {
|
||||||
|
return NullTime{
|
||||||
|
NullTime: sql.NullTime{
|
||||||
|
Time: t,
|
||||||
|
Valid: valid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (t NullTime) MarshalJSON() ([]byte, error) {
|
||||||
|
if !t.Valid {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
b, err := t.Time.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("codersdk.NullTime: json encode failed: %w", err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
|
func (t *NullTime) UnmarshalJSON(data []byte) error {
|
||||||
|
t.Valid = false
|
||||||
|
if bytes.Equal(data, nullBytes) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(data, &t.Time)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("codersdk.NullTime: json decode failed: %w", err)
|
||||||
|
}
|
||||||
|
t.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero return true if the time is null or zero.
|
||||||
|
func (t NullTime) IsZero() bool {
|
||||||
|
return !t.Valid || t.Time.IsZero()
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
package codersdk_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNullTime_MarshalJSON(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t1, err := time.Parse(time.RFC3339, "2022-08-18T00:00:00Z")
|
||||||
|
require.NoError(t, err)
|
||||||
|
bt1, err := json.Marshal(t1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input sql.NullTime
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid zero",
|
||||||
|
input: sql.NullTime{Valid: true},
|
||||||
|
want: `"0001-01-01T00:00:00Z"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid zero",
|
||||||
|
input: sql.NullTime{Valid: false},
|
||||||
|
want: "null",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid time",
|
||||||
|
input: sql.NullTime{Time: t1, Valid: true},
|
||||||
|
want: string(bt1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null time",
|
||||||
|
input: sql.NullTime{Time: t1, Valid: false},
|
||||||
|
want: "null",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tr := codersdk.NewNullTime(tt.input.Time, tt.input.Valid)
|
||||||
|
got, err := tr.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.want, string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNullTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t1, err := time.Parse(time.RFC3339, "2022-08-18T00:00:00Z")
|
||||||
|
require.NoError(t, err)
|
||||||
|
bt1, err := json.Marshal(t1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type request struct {
|
||||||
|
Time codersdk.NullTime `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
want codersdk.NullTime
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "null",
|
||||||
|
data: `{"time": null}`,
|
||||||
|
want: codersdk.NullTime{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
data: `{}`,
|
||||||
|
want: codersdk.NullTime{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
data: `{"time": ""}`,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid time",
|
||||||
|
data: fmt.Sprintf(`{"time": %s}`, bt1),
|
||||||
|
want: codersdk.NewNullTime(t1, true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid time",
|
||||||
|
data: fmt.Sprintf(`{"time": %q}`, `2022-08-18T00:00:00`),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var req request
|
||||||
|
err := json.Unmarshal([]byte(tt.data), &req)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.want, req.Time)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNullTime_IsZero(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input sql.NullTime
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero",
|
||||||
|
input: sql.NullTime{},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not zero",
|
||||||
|
input: sql.NullTime{Time: time.Now(), Valid: true},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "null is zero",
|
||||||
|
input: sql.NullTime{Time: time.Now(), Valid: false},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tr := codersdk.NullTime{NullTime: tt.input}
|
||||||
|
require.Equal(t, tt.want, tr.IsZero())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,7 +50,7 @@ type WorkspaceBuild struct {
|
||||||
InitiatorID uuid.UUID `json:"initiator_id"`
|
InitiatorID uuid.UUID `json:"initiator_id"`
|
||||||
InitiatorUsername string `json:"initiator_name"`
|
InitiatorUsername string `json:"initiator_name"`
|
||||||
Job ProvisionerJob `json:"job"`
|
Job ProvisionerJob `json:"job"`
|
||||||
Deadline time.Time `json:"deadline"`
|
Deadline NullTime `json:"deadline,omitempty"`
|
||||||
Reason BuildReason `db:"reason" json:"reason"`
|
Reason BuildReason `db:"reason" json:"reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -415,15 +415,6 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
|
||||||
}
|
}
|
||||||
case *types.Named:
|
case *types.Named:
|
||||||
n := ty
|
n := ty
|
||||||
// First see if the type is defined elsewhere. If it is, we can just
|
|
||||||
// put the name as it will be defined in the typescript codeblock
|
|
||||||
// we generate.
|
|
||||||
name := n.Obj().Name()
|
|
||||||
if obj := g.pkg.Types.Scope().Lookup(name); obj != nil {
|
|
||||||
// Sweet! Using other typescript types as fields. This could be an
|
|
||||||
// enum or another struct
|
|
||||||
return TypescriptType{ValueType: name}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// These are external named types that we handle uniquely.
|
// These are external named types that we handle uniquely.
|
||||||
switch n.String() {
|
switch n.String() {
|
||||||
|
@ -434,12 +425,24 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
|
||||||
return TypescriptType{ValueType: "string"}, nil
|
return TypescriptType{ValueType: "string"}, nil
|
||||||
case "database/sql.NullTime":
|
case "database/sql.NullTime":
|
||||||
return TypescriptType{ValueType: "string", Optional: true}, nil
|
return TypescriptType{ValueType: "string", Optional: true}, nil
|
||||||
|
case "github.com/coder/coder/codersdk.NullTime":
|
||||||
|
return TypescriptType{ValueType: "string", Optional: true}, nil
|
||||||
case "github.com/google/uuid.NullUUID":
|
case "github.com/google/uuid.NullUUID":
|
||||||
return TypescriptType{ValueType: "string", Optional: true}, nil
|
return TypescriptType{ValueType: "string", Optional: true}, nil
|
||||||
case "github.com/google/uuid.UUID":
|
case "github.com/google/uuid.UUID":
|
||||||
return TypescriptType{ValueType: "string"}, nil
|
return TypescriptType{ValueType: "string"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then see if the type is defined elsewhere. If it is, we can just
|
||||||
|
// put the name as it will be defined in the typescript codeblock
|
||||||
|
// we generate.
|
||||||
|
name := n.Obj().Name()
|
||||||
|
if obj := g.pkg.Types.Scope().Lookup(name); obj != nil {
|
||||||
|
// Sweet! Using other typescript types as fields. This could be an
|
||||||
|
// enum or another struct
|
||||||
|
return TypescriptType{ValueType: name}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// If it's a struct, just use the name of the struct type
|
// If it's a struct, just use the name of the struct type
|
||||||
if _, ok := n.Underlying().(*types.Struct); ok {
|
if _, ok := n.Underlying().(*types.Struct); ok {
|
||||||
return TypescriptType{ValueType: "any", AboveTypeLine: fmt.Sprintf("%s\n%s",
|
return TypescriptType{ValueType: "any", AboveTypeLine: fmt.Sprintf("%s\n%s",
|
||||||
|
|
|
@ -527,7 +527,7 @@ export interface WorkspaceBuild {
|
||||||
readonly initiator_id: string
|
readonly initiator_id: string
|
||||||
readonly initiator_name: string
|
readonly initiator_name: string
|
||||||
readonly job: ProvisionerJob
|
readonly job: ProvisionerJob
|
||||||
readonly deadline: string
|
readonly deadline?: string
|
||||||
readonly reason: BuildReason
|
readonly reason: BuildReason
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,7 @@ NoTTL.args = {
|
||||||
...Mocks.MockWorkspace,
|
...Mocks.MockWorkspace,
|
||||||
latest_build: {
|
latest_build: {
|
||||||
...Mocks.MockWorkspaceBuild,
|
...Mocks.MockWorkspaceBuild,
|
||||||
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
|
deadline: undefined,
|
||||||
// SEE: #1834
|
|
||||||
deadline: "0001-01-01T00:00:00Z",
|
|
||||||
},
|
},
|
||||||
ttl_ms: undefined,
|
ttl_ms: undefined,
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,12 +10,12 @@ describe("WorkspaceScheduleBanner", () => {
|
||||||
describe("shouldDisplay", () => {
|
describe("shouldDisplay", () => {
|
||||||
// Manual TTL case
|
// Manual TTL case
|
||||||
it("should not display if the build does not have a deadline", () => {
|
it("should not display if the build does not have a deadline", () => {
|
||||||
// Given: a workspace with deadline of '"0001-01-01T00:00:00Z"'
|
// Given: a workspace with deadline of undefined.
|
||||||
const workspace: TypesGen.Workspace = {
|
const workspace: TypesGen.Workspace = {
|
||||||
...Mocks.MockWorkspace,
|
...Mocks.MockWorkspace,
|
||||||
latest_build: {
|
latest_build: {
|
||||||
...Mocks.MockWorkspaceBuild,
|
...Mocks.MockWorkspaceBuild,
|
||||||
deadline: "0001-01-01T00:00:00Z",
|
deadline: undefined,
|
||||||
transition: "start",
|
transition: "start",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,16 +23,12 @@ export interface WorkspaceScheduleBannerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
|
export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
|
||||||
if (!isWorkspaceOn(workspace)) {
|
if (!isWorkspaceOn(workspace) || !workspace.latest_build.deadline) {
|
||||||
return false
|
return false
|
||||||
} else {
|
|
||||||
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
|
|
||||||
// SEE: #1834
|
|
||||||
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
|
||||||
const hasDeadline = deadline.year() > 1
|
|
||||||
const thirtyMinutesFromNow = dayjs().add(30, "minutes").utc()
|
|
||||||
return hasDeadline && deadline.isSameOrBefore(thirtyMinutesFromNow)
|
|
||||||
}
|
}
|
||||||
|
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
||||||
|
const thirtyMinutesFromNow = dayjs().add(30, "minutes").utc()
|
||||||
|
return deadline.isSameOrBefore(thirtyMinutesFromNow)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceScheduleBanner: FC<React.PropsWithChildren<WorkspaceScheduleBannerProps>> = ({
|
export const WorkspaceScheduleBanner: FC<React.PropsWithChildren<WorkspaceScheduleBannerProps>> = ({
|
||||||
|
|
|
@ -40,9 +40,7 @@ NoTTL.args = {
|
||||||
...Mocks.MockWorkspace,
|
...Mocks.MockWorkspace,
|
||||||
latest_build: {
|
latest_build: {
|
||||||
...Mocks.MockWorkspaceBuild,
|
...Mocks.MockWorkspaceBuild,
|
||||||
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
|
deadline: undefined,
|
||||||
// SEE: #1834
|
|
||||||
deadline: "0001-01-01T00:00:00Z",
|
|
||||||
},
|
},
|
||||||
ttl_ms: undefined,
|
ttl_ms: undefined,
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,11 +28,7 @@ dayjs.extend(relativeTime)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
export const shouldDisplayPlusMinus = (workspace: Workspace): boolean => {
|
export const shouldDisplayPlusMinus = (workspace: Workspace): boolean => {
|
||||||
if (!isWorkspaceOn(workspace)) {
|
return isWorkspaceOn(workspace) && Boolean(workspace.latest_build.deadline)
|
||||||
return false
|
|
||||||
}
|
|
||||||
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
|
||||||
return deadline.year() > 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceScheduleButtonProps {
|
export interface WorkspaceScheduleButtonProps {
|
||||||
|
|
|
@ -73,27 +73,26 @@ export const autoStartDisplay = (schedule: string | undefined): string => {
|
||||||
|
|
||||||
export const isShuttingDown = (workspace: Workspace, deadline?: Dayjs): boolean => {
|
export const isShuttingDown = (workspace: Workspace, deadline?: Dayjs): boolean => {
|
||||||
if (!deadline) {
|
if (!deadline) {
|
||||||
|
if (!workspace.latest_build.deadline) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
deadline = dayjs(workspace.latest_build.deadline).utc()
|
deadline = dayjs(workspace.latest_build.deadline).utc()
|
||||||
}
|
}
|
||||||
const hasDeadline = deadline.year() > 1
|
|
||||||
const now = dayjs().utc()
|
const now = dayjs().utc()
|
||||||
return isWorkspaceOn(workspace) && hasDeadline && now.isAfter(deadline)
|
return isWorkspaceOn(workspace) && now.isAfter(deadline)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const autoStopDisplay = (workspace: Workspace): string => {
|
export const autoStopDisplay = (workspace: Workspace): string => {
|
||||||
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
|
||||||
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
|
|
||||||
// SEE: #1834
|
|
||||||
const hasDeadline = deadline.year() > 1
|
|
||||||
const ttl = workspace.ttl_ms
|
const ttl = workspace.ttl_ms
|
||||||
|
|
||||||
if (isWorkspaceOn(workspace) && hasDeadline) {
|
if (isWorkspaceOn(workspace) && workspace.latest_build.deadline) {
|
||||||
// Workspace is on --> derive from latest_build.deadline. Note that the
|
// Workspace is on --> derive from latest_build.deadline. Note that the
|
||||||
// user may modify their workspace object (ttl) while the workspace is
|
// user may modify their workspace object (ttl) while the workspace is
|
||||||
// running and depending on system semantics, the deadline may still
|
// running and depending on system semantics, the deadline may still
|
||||||
// represent the previously defined ttl. Thus, we always derive from the
|
// represent the previously defined ttl. Thus, we always derive from the
|
||||||
// deadline as the source of truth.
|
// deadline as the source of truth.
|
||||||
|
|
||||||
|
const deadline = dayjs(workspace.latest_build.deadline).utc()
|
||||||
if (isShuttingDown(workspace, deadline)) {
|
if (isShuttingDown(workspace, deadline)) {
|
||||||
return Language.workspaceShuttingDownLabel
|
return Language.workspaceShuttingDownLabel
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue