Skip to content

Commit 605a9f8

Browse files
committed
feat(engine): allow configuring timezone in DLE CLI configuration (#335)
* create a custom type - `LocalTime` * show the time depending on locale in CLI, use UTC by default and for API responses * consider clones, snapshots, instance, pools * update time display format: `2006-01-02 15:04:05 UTC` -> `2006-01-02 15:04:05 +00:00` * add commands to view and manage CLI settings * `dblab config show-global` * `dblab config set-global` * process environment variable `TZ` which overrides CLI settings from a file
1 parent c2174d7 commit 605a9f8

33 files changed

+315
-105
lines changed

engine/.gitlab-ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ build-image-swagger-latest:
360360
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
361361
changes:
362362
- engine/**/*
363+
artifacts:
364+
paths:
365+
- engine/bin
363366
script:
364367
- bash engine/test/1.synthetic.sh
365368
- bash engine/test/2.logical_generic.sh

engine/cmd/cli/commands/client.go

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
FwServerURLKey = "forwarding-server-url"
2424
FwLocalPortKey = "forwarding-local-port"
2525
IdentityFileKey = "identity-file"
26+
TZKey = "tz"
2627
)
2728

2829
// ClientByCLIContext creates a new Database Lab API client.

engine/cmd/cli/commands/config/actions.go

+47
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,50 @@ func removeEnvironment() func(*cli.Context) error {
260260
return commands.ToActionError(err)
261261
}
262262
}
263+
264+
// showSettings shows global CLI settings.
265+
func showSettings(cliCtx *cli.Context) error {
266+
configFilename, err := GetFilename()
267+
if err != nil {
268+
return commands.ToActionError(err)
269+
}
270+
271+
cfg, err := Load(configFilename)
272+
if err != nil {
273+
return commands.ToActionError(err)
274+
}
275+
276+
settings, err := json.MarshalIndent(cfg.Settings, "", " ")
277+
if err != nil {
278+
return commands.ToActionError(err)
279+
}
280+
281+
_, err = fmt.Fprintln(cliCtx.App.Writer, string(settings))
282+
283+
return commands.ToActionError(err)
284+
}
285+
286+
// updateSettings updates CLI settings.
287+
func updateSettings(cliCtx *cli.Context) error {
288+
configFilename, err := GetFilename()
289+
if err != nil {
290+
return commands.ToActionError(err)
291+
}
292+
293+
cfg, err := Load(configFilename)
294+
if err != nil {
295+
return commands.ToActionError(err)
296+
}
297+
298+
if err := updateSettingsInConfig(cliCtx, cfg); err != nil {
299+
return commands.ToActionError(err)
300+
}
301+
302+
if err := SaveConfig(configFilename, cfg); err != nil {
303+
return commands.ToActionError(err)
304+
}
305+
306+
_, err = fmt.Fprintf(cliCtx.App.Writer, "CLI settings has been successfully updated.\n")
307+
308+
return commands.ToActionError(err)
309+
}

engine/cmd/cli/commands/config/command_list.go

+16
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,22 @@ func CommandList() []*cli.Command {
113113
ArgsUsage: "ENVIRONMENT_ID",
114114
Action: removeEnvironment(),
115115
},
116+
{
117+
Name: "show-global",
118+
Usage: "show global CLI settings",
119+
Action: showSettings,
120+
},
121+
{
122+
Name: "set-global",
123+
Usage: "update global CLI settings",
124+
Action: updateSettings,
125+
Flags: []cli.Flag{
126+
&cli.StringFlag{
127+
Name: "tz",
128+
Usage: "Timezone to display time in DLE responses",
129+
},
130+
},
131+
},
116132
},
117133
},
118134
}

engine/cmd/cli/commands/config/environment.go

+19
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
type CLIConfig struct {
1616
CurrentEnvironment string `yaml:"current_environment" json:"current_environment"`
1717
Environments map[string]Environment `yaml:"environments" json:"environments"`
18+
Settings Settings `yaml:"settings" json:"settings"`
1819
}
1920

2021
// Environment defines a format of environment configuration.
@@ -34,6 +35,11 @@ type Forwarding struct {
3435
IdentityFile string `yaml:"identity_file" json:"identity_file"`
3536
}
3637

38+
// Settings defines global CLI settings.
39+
type Settings struct {
40+
TZ string `yaml:"tz" json:"tz"`
41+
}
42+
3743
// AddEnvironmentToConfig adds a new environment to CLIConfig.
3844
func AddEnvironmentToConfig(c *cli.Context, cfg *CLIConfig, environmentID string) error {
3945
if environmentID == "" {
@@ -162,3 +168,16 @@ func removeByID(cfg *CLIConfig, environmentID string) error {
162168

163169
return nil
164170
}
171+
172+
// updateSettings updates CLI settings.
173+
func updateSettingsInConfig(c *cli.Context, cfg *CLIConfig) error {
174+
if c.NumFlags() == 0 {
175+
return errors.New("config unchanged. Set options to update")
176+
}
177+
178+
if c.IsSet(commands.TZKey) {
179+
cfg.Settings.TZ = c.String(commands.TZKey)
180+
}
181+
182+
return nil
183+
}

engine/cmd/cli/main.go

+8
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func main() {
9191
}
9292
}
9393

94+
const tzConst = "TZ"
95+
9496
// loadEnvironmentParams loads environment params from a Database Lab's config file.
9597
func loadEnvironmentParams(c *cli.Context) error {
9698
dblabLog.SetDebug(c.IsSet("debug"))
@@ -106,6 +108,12 @@ func loadEnvironmentParams(c *cli.Context) error {
106108
return nil
107109
}
108110

111+
if _, ok := os.LookupEnv(tzConst); !ok {
112+
if err := os.Setenv(tzConst, cfg.Settings.TZ); err != nil {
113+
return err
114+
}
115+
}
116+
109117
currentEnv := cfg.CurrentEnvironment
110118
if env, ok := cfg.Environments[currentEnv]; ok {
111119
if !c.IsSet(commands.URLKey) {

engine/internal/cloning/base.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func (c *Base) CreateClone(cloneRequest *types.CloneCreateRequest) (*models.Clon
160160
ID: cloneRequest.ID,
161161
Snapshot: snapshot,
162162
Protected: cloneRequest.Protected,
163-
CreatedAt: util.FormatTime(createdAt),
163+
CreatedAt: models.NewLocalTime(createdAt),
164164
Status: models.Status{
165165
Code: models.StatusCreating,
166166
Message: models.CloneMessageCreating,
@@ -459,7 +459,7 @@ func (c *Base) ResetClone(cloneID string, resetOptions types.ResetCloneRequest)
459459
c.tm.SendEvent(context.Background(), telemetry.CloneResetEvent, telemetry.CloneCreated{
460460
ID: util.HashID(w.Clone.ID),
461461
CloningTime: w.Clone.Metadata.CloningTime,
462-
DSADiff: util.GetDataFreshness(snapshot.DataStateAt),
462+
DSADiff: util.GetDataFreshness(snapshot.DataStateAt.Time),
463463
})
464464
}()
465465

@@ -512,7 +512,7 @@ func (c *Base) GetClones() []*models.Clone {
512512
c.cloneMutex.RUnlock()
513513

514514
sort.Slice(clones, func(i, j int) bool {
515-
return clones[i].CreatedAt > clones[j].CreatedAt
515+
return clones[i].CreatedAt.After(clones[j].CreatedAt.Time)
516516
})
517517

518518
return clones

engine/internal/cloning/base_test.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cloning
66

77
import (
88
"testing"
9+
"time"
910

1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
@@ -52,9 +53,9 @@ func (s *BaseCloningSuite) TestFindWrapper() {
5253
}
5354

5455
func (s *BaseCloningSuite) TestCloneList() {
55-
clone1 := &models.Clone{CreatedAt: "2020-02-20 01:23:45 UTC"}
56-
clone2 := &models.Clone{CreatedAt: "2020-06-23 10:31:27 UTC"}
57-
clone3 := &models.Clone{CreatedAt: "2020-05-20 00:43:21 UTC"}
56+
clone1 := &models.Clone{CreatedAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 01, 23, 45, 0, time.UTC)}}
57+
clone2 := &models.Clone{CreatedAt: &models.LocalTime{Time: time.Date(2020, 06, 23, 10, 31, 27, 0, time.UTC)}}
58+
clone3 := &models.Clone{CreatedAt: &models.LocalTime{Time: time.Date(2020, 05, 20, 00, 43, 21, 0, time.UTC)}}
5859

5960
s.cloning.setWrapper("testCloneID1", &CloneWrapper{Clone: clone1})
6061
s.cloning.setWrapper("testCloneID2", &CloneWrapper{Clone: clone2})
@@ -66,9 +67,9 @@ func (s *BaseCloningSuite) TestCloneList() {
6667

6768
// Check clone order.
6869
assert.Equal(s.T(), []*models.Clone{
69-
{CreatedAt: "2020-06-23 10:31:27 UTC"},
70-
{CreatedAt: "2020-05-20 00:43:21 UTC"},
71-
{CreatedAt: "2020-02-20 01:23:45 UTC"},
70+
{CreatedAt: &models.LocalTime{Time: time.Date(2020, 06, 23, 10, 31, 27, 0, time.UTC)}},
71+
{CreatedAt: &models.LocalTime{Time: time.Date(2020, 05, 20, 00, 43, 21, 0, time.UTC)}},
72+
{CreatedAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 01, 23, 45, 0, time.UTC)}},
7273
}, list)
7374
}
7475

engine/internal/cloning/snapshots.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212

1313
"gitlab.com/postgres-ai/database-lab/v3/pkg/log"
1414
"gitlab.com/postgres-ai/database-lab/v3/pkg/models"
15-
"gitlab.com/postgres-ai/database-lab/v3/pkg/util"
1615
)
1716

1817
// SnapshotBox contains instance snapshots.
@@ -44,8 +43,8 @@ func (c *Base) fetchSnapshots() error {
4443

4544
currentSnapshot := &models.Snapshot{
4645
ID: entry.ID,
47-
CreatedAt: util.FormatTime(entry.CreatedAt),
48-
DataStateAt: util.FormatTime(entry.DataStateAt),
46+
CreatedAt: models.NewLocalTime(entry.CreatedAt),
47+
DataStateAt: models.NewLocalTime(entry.DataStateAt),
4948
PhysicalSize: entry.Used,
5049
LogicalSize: entry.LogicalReferenced,
5150
Pool: entry.Pool,
@@ -82,7 +81,7 @@ func (c *Base) addSnapshot(snapshot *models.Snapshot) {
8281

8382
// defineLatestSnapshot compares two snapshots and defines the latest one.
8483
func defineLatestSnapshot(latest, challenger *models.Snapshot) *models.Snapshot {
85-
if latest == nil || latest.DataStateAt == "" || latest.DataStateAt < challenger.DataStateAt {
84+
if latest == nil || latest.DataStateAt == nil || latest.DataStateAt.IsZero() || latest.DataStateAt.Before(challenger.DataStateAt.Time) {
8685
return challenger
8786
}
8887

@@ -162,7 +161,7 @@ func (c *Base) getSnapshotList() []models.Snapshot {
162161
}
163162

164163
sort.Slice(snapshots, func(i, j int) bool {
165-
return snapshots[i].CreatedAt > snapshots[j].CreatedAt
164+
return snapshots[i].CreatedAt.After(snapshots[j].CreatedAt.Time)
166165
})
167166

168167
return snapshots

engine/internal/cloning/snapshots_test.go

+14-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cloning
66

77
import (
88
"testing"
9+
"time"
910

1011
"github.com/stretchr/testify/require"
1112

@@ -17,14 +18,14 @@ func (s *BaseCloningSuite) TestLatestSnapshot() {
1718

1819
snapshot1 := &models.Snapshot{
1920
ID: "TestSnapshotID1",
20-
CreatedAt: "2020-02-20 01:23:45",
21-
DataStateAt: "2020-02-19 00:00:00",
21+
CreatedAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 01, 23, 45, 0, time.UTC)},
22+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 19, 0, 0, 0, 0, time.UTC)},
2223
}
2324

2425
snapshot2 := &models.Snapshot{
2526
ID: "TestSnapshotID2",
26-
CreatedAt: "2020-02-20 05:43:21",
27-
DataStateAt: "2020-02-20 00:00:00",
27+
CreatedAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 05, 43, 21, 0, time.UTC)},
28+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 0, 0, 0, 0, time.UTC)},
2829
}
2930

3031
require.Equal(s.T(), 0, len(s.cloning.snapshotBox.items))
@@ -41,8 +42,8 @@ func (s *BaseCloningSuite) TestLatestSnapshot() {
4142

4243
snapshot3 := &models.Snapshot{
4344
ID: "TestSnapshotID3",
44-
CreatedAt: "2020-02-21 05:43:21",
45-
DataStateAt: "2020-02-21 00:00:00",
45+
CreatedAt: &models.LocalTime{Time: time.Date(2020, 02, 21, 05, 43, 21, 0, time.UTC)},
46+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 21, 0, 0, 0, 0, time.UTC)},
4647
}
4748

4849
snapshotMap := make(map[string]*models.Snapshot)
@@ -62,14 +63,14 @@ func (s *BaseCloningSuite) TestSnapshotByID() {
6263

6364
snapshot1 := &models.Snapshot{
6465
ID: "TestSnapshotID1",
65-
CreatedAt: "2020-02-20 01:23:45",
66-
DataStateAt: "2020-02-19 00:00:00",
66+
CreatedAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 01, 23, 45, 0, time.UTC)},
67+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 19, 0, 0, 0, 0, time.UTC)},
6768
}
6869

6970
snapshot2 := &models.Snapshot{
7071
ID: "TestSnapshotID2",
71-
CreatedAt: "2020-02-20 05:43:21",
72-
DataStateAt: "2020-02-20 00:00:00",
72+
CreatedAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 05, 43, 21, 0, time.UTC)},
73+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 20, 0, 0, 0, 0, time.UTC)},
7374
}
7475

7576
require.Equal(s.T(), 0, len(s.cloning.snapshotBox.items))
@@ -122,13 +123,13 @@ func TestCloneCounter(t *testing.T) {
122123

123124
func TestLatestSnapshots(t *testing.T) {
124125
baseSnapshot := &models.Snapshot{
125-
DataStateAt: "2020-02-19 00:00:00",
126+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 19, 0, 0, 0, 0, time.UTC)},
126127
}
127128
newSnapshot := &models.Snapshot{
128-
DataStateAt: "2020-02-21 00:00:00",
129+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 21, 0, 0, 0, 0, time.UTC)},
129130
}
130131
oldSnapshot := &models.Snapshot{
131-
DataStateAt: "2020-02-01 00:00:00",
132+
DataStateAt: &models.LocalTime{Time: time.Date(2020, 02, 01, 0, 0, 0, 0, time.UTC)},
132133
}
133134

134135
testCases := []struct {

engine/internal/cloning/storage_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ const (
1919
"id": "c5bfsk0hmvjd7kau71jg",
2020
"snapshot": {
2121
"id": "east5@snapshot_20211001112229",
22-
"createdAt": "2021-10-01 11:23:11 UTC",
23-
"dataStateAt": "2021-10-01 11:22:29 UTC"
22+
"createdAt": "2021-10-01 11:23:11 +00:00",
23+
"dataStateAt": "2021-10-01 11:22:29 +00:00"
2424
},
2525
"protected": false,
2626
"deleteAt": "",
27-
"createdAt": "2021-10-01 12:25:52 UTC",
27+
"createdAt": "2021-10-01 12:25:52 +00:00",
2828
"status": {
2929
"code": "OK",
3030
"message": "Clone is ready to accept Postgres connections."

engine/internal/provision/mode_local.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ func (p *Provisioner) ResetSession(session *resources.Session, snapshotID string
291291

292292
snapshotModel := &models.Snapshot{
293293
ID: snapshot.ID,
294-
CreatedAt: util.FormatTime(snapshot.CreatedAt),
295-
DataStateAt: util.FormatTime(snapshot.DataStateAt),
294+
CreatedAt: models.NewLocalTime(snapshot.CreatedAt),
295+
DataStateAt: models.NewLocalTime(snapshot.DataStateAt),
296296
}
297297

298298
return snapshotModel, nil
@@ -366,15 +366,15 @@ func buildPoolEntry(fsm pool.FSManager) (models.PoolEntry, error) {
366366
log.Err(fmt.Sprintf("failed to get disk stats for the pool %s", fsmPool.Name))
367367
}
368368

369-
var dataStateAt string
369+
var dataStateAt time.Time
370370
if !fsmPool.DSA.IsZero() {
371-
dataStateAt = fsmPool.DSA.String()
371+
dataStateAt = fsmPool.DSA
372372
}
373373

374374
poolEntry := models.PoolEntry{
375375
Name: fsmPool.Name,
376376
Mode: fsmPool.Mode,
377-
DataStateAt: dataStateAt,
377+
DataStateAt: models.NewLocalTime(dataStateAt),
378378
CloneList: listClones,
379379
FileSystem: fileSystem,
380380
Status: fsm.Pool().Status(),

engine/internal/provision/mode_local_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func TestBuildPoolEntry(t *testing.T) {
121121
expectedEntry: models.PoolEntry{
122122
Name: "TestPool",
123123
Mode: "zfs",
124-
DataStateAt: "2021-08-01 00:00:00 +0000 UTC",
124+
DataStateAt: &models.LocalTime{Time: time.Date(2021, 8, 01, 0, 0, 0, 0, time.UTC)},
125125
Status: resources.ActivePool,
126126
CloneList: []string{"test_clone_0001", "test_clone_0002"},
127127
FileSystem: models.FileSystem{Mode: "zfs"},
@@ -137,7 +137,7 @@ func TestBuildPoolEntry(t *testing.T) {
137137
expectedEntry: models.PoolEntry{
138138
Name: "TestPoolWithoutDSA",
139139
Mode: "zfs",
140-
DataStateAt: "",
140+
DataStateAt: &models.LocalTime{},
141141
Status: resources.EmptyPool,
142142
CloneList: []string{},
143143
FileSystem: models.FileSystem{Mode: "zfs"},

0 commit comments

Comments
 (0)