Skip to content

Commit e1df8ef

Browse files
easyCZpull[bot]
authored andcommitted
[usage] List workspace instances
1 parent 0b4e27c commit e1df8ef

File tree

6 files changed

+155
-12
lines changed

6 files changed

+155
-12
lines changed

components/usage/cmd/run.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func run() *cobra.Command {
3131
Run: func(cmd *cobra.Command, args []string) {
3232
log.Init(ServiceName, Version, true, verbose)
3333

34-
_, err := db.Connect(db.ConnectionParams{
34+
conn, err := db.Connect(db.ConnectionParams{
3535
User: os.Getenv("DB_USERNAME"),
3636
Password: os.Getenv("DB_PASSWORD"),
3737
Host: net.JoinHostPort(os.Getenv("DB_HOST"), os.Getenv("DB_PORT")),
@@ -41,7 +41,7 @@ func run() *cobra.Command {
4141
log.WithError(err).Fatal("Failed to establish database connection.")
4242
}
4343

44-
ctrl, err := controller.New(1*time.Minute, controller.ReconcilerFunc(controller.HelloWorldReconciler))
44+
ctrl, err := controller.New(1*time.Minute, controller.NewUsageReconciler(conn))
4545
if err != nil {
4646
log.WithError(err).Fatal("Failed to initialize usage controller.")
4747
}

components/usage/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ require (
6060
github.com/google/uuid v1.1.2
6161
github.com/relvacode/iso8601 v1.1.0
6262
github.com/robfig/cron v1.2.0
63+
github.com/sirupsen/logrus v1.8.1
6364
github.com/spf13/cobra v1.4.0
6465
github.com/stretchr/testify v1.7.0
6566
gorm.io/datatypes v1.0.6
@@ -86,7 +87,6 @@ require (
8687
github.com/prometheus/client_model v0.2.0 // indirect
8788
github.com/prometheus/common v0.32.1 // indirect
8889
github.com/prometheus/procfs v0.7.3 // indirect
89-
github.com/sirupsen/logrus v1.8.1 // indirect
9090
github.com/spf13/pflag v1.0.5 // indirect
9191
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
9292
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect

components/usage/pkg/controller/reconciler.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44

55
package controller
66

7-
import "github.com/gitpod-io/gitpod/common-go/log"
7+
import (
8+
"context"
9+
"fmt"
10+
"github.com/gitpod-io/gitpod/common-go/log"
11+
"github.com/gitpod-io/gitpod/usage/pkg/db"
12+
"gorm.io/gorm"
13+
"time"
14+
)
815

916
type Reconciler interface {
1017
Reconcile() error
@@ -16,7 +23,31 @@ func (f ReconcilerFunc) Reconcile() error {
1623
return f()
1724
}
1825

19-
func HelloWorldReconciler() error {
20-
log.Info("Hello world reconciler!")
26+
func NewUsageReconciler(conn *gorm.DB) *UsageReconciler {
27+
return &UsageReconciler{conn: conn}
28+
}
29+
30+
type UsageReconciler struct {
31+
conn *gorm.DB
32+
}
33+
34+
func (u *UsageReconciler) Reconcile() error {
35+
ctx := context.Background()
36+
now := time.Now().UTC()
37+
38+
startOfCurrentMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
39+
startOfNextMonth := startOfCurrentMonth.AddDate(0, 1, 0)
40+
41+
return u.reconcile(ctx, startOfCurrentMonth, startOfNextMonth)
42+
}
43+
44+
func (u *UsageReconciler) reconcile(ctx context.Context, from, to time.Time) error {
45+
log.Infof("Gathering usage data from %s to %s", from, to)
46+
instances, err := db.ListWorkspaceInstancesInRange(ctx, u.conn, from, to)
47+
if err != nil {
48+
return fmt.Errorf("failed to list instances: %w", err)
49+
}
50+
51+
log.Infof("Identified %d instances between %s and %s", len(instances), from, to)
2152
return nil
2253
}

components/usage/pkg/db/conn_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
// Licensed under the GNU Affero General Public License (AGPL).
33
// See License-AGPL.txt in the project root for license information.
44

5-
package db
5+
package db_test
66

77
import (
8+
"github.com/gitpod-io/gitpod/usage/pkg/db"
89
"github.com/stretchr/testify/require"
910
"testing"
1011
)
1112

1213
func TestConnectForTests(t *testing.T) {
13-
conn := ConnectForTests(t)
14+
conn := db.ConnectForTests(t)
1415
require.NotNil(t, conn)
1516
}

components/usage/pkg/db/workspace_instance.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
package db
66

77
import (
8+
"context"
89
"database/sql"
10+
"fmt"
911
"github.com/google/uuid"
1012
"gorm.io/datatypes"
13+
"gorm.io/gorm"
1114
"time"
1215
)
1316

@@ -28,10 +31,11 @@ type WorkspaceInstance struct {
2831
LastModified time.Time `gorm:"column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"`
2932
StoppingTime VarcharTime `gorm:"column:stoppingTime;type:varchar;size:255;" json:"stoppingTime"`
3033

31-
LastHeartbeat string `gorm:"column:lastHeartbeat;type:varchar;size:255;" json:"lastHeartbeat"`
32-
StatusOld sql.NullString `gorm:"column:status_old;type:varchar;size:255;" json:"status_old"`
33-
Status datatypes.JSON `gorm:"column:status;type:json;" json:"status"`
34-
Phase sql.NullString `gorm:"column:phase;type:char;size:32;" json:"phase"`
34+
LastHeartbeat string `gorm:"column:lastHeartbeat;type:varchar;size:255;" json:"lastHeartbeat"`
35+
StatusOld sql.NullString `gorm:"column:status_old;type:varchar;size:255;" json:"status_old"`
36+
Status datatypes.JSON `gorm:"column:status;type:json;" json:"status"`
37+
// Phase is derived from Status by extracting JSON from it. Read-only (-> property).
38+
Phase sql.NullString `gorm:"->:column:phase;type:char;size:32;" json:"phase"`
3539
PhasePersisted string `gorm:"column:phasePersisted;type:char;size:32;" json:"phasePersisted"`
3640

3741
// deleted is restricted for use by db-sync
@@ -42,3 +46,25 @@ type WorkspaceInstance struct {
4246
func (d *WorkspaceInstance) TableName() string {
4347
return "d_b_workspace_instance"
4448
}
49+
50+
// ListWorkspaceInstancesInRange lists WorkspaceInstances between from (inclusive) and to (exclusive).
51+
// This results in all instances which have existed in the specified period, regardless of their current status, this includes:
52+
// - terminated
53+
// - running
54+
// - instances which only just terminated after the start period
55+
// - instances which only just started in the period specified
56+
func ListWorkspaceInstancesInRange(ctx context.Context, conn *gorm.DB, from, to time.Time) ([]WorkspaceInstance, error) {
57+
var instances []WorkspaceInstance
58+
tx := conn.WithContext(ctx).
59+
Where(
60+
conn.Where("stoppedTime >= ?", from).Or("stoppedTime = ?", ""),
61+
).
62+
Where("creationTime < ?", to).
63+
Where("creationTime != ?", "").
64+
Find(&instances)
65+
if tx.Error != nil {
66+
return nil, fmt.Errorf("failed to list workspace instances: %w", tx.Error)
67+
}
68+
69+
return instances, nil
70+
}

components/usage/pkg/db/workspace_instance_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
package db_test
66

77
import (
8+
"context"
89
"fmt"
910
"github.com/gitpod-io/gitpod/usage/pkg/db"
1011
"github.com/google/uuid"
1112
"github.com/stretchr/testify/require"
1213
"gorm.io/gorm"
1314
"strings"
1415
"testing"
16+
"time"
1517
)
1618

1719
var workspaceInstanceJSON = map[string]interface{}{
@@ -93,3 +95,86 @@ func insertRawObject(t *testing.T, conn *gorm.DB, columns []string, statement st
9395

9496
return id
9597
}
98+
99+
func TestListWorkspaceInstancesInRange(t *testing.T) {
100+
conn := db.ConnectForTests(t)
101+
102+
workspaceID := "gitpodio-gitpod-gyjr82jkfnd"
103+
status := []byte(`{"phase": "stopped", "conditions": {"deployed": false, "pullingImages": false, "serviceExists": false}}`)
104+
valid := []*db.WorkspaceInstance{
105+
// In the middle of May
106+
{
107+
ID: uuid.New(),
108+
WorkspaceID: workspaceID,
109+
CreationTime: db.NewVarcharTime(time.Date(2022, 05, 15, 12, 00, 00, 00, time.UTC)),
110+
StoppedTime: db.NewVarcharTime(time.Date(2022, 05, 15, 13, 00, 00, 00, time.UTC)),
111+
Status: status,
112+
},
113+
// Start of May
114+
{
115+
ID: uuid.New(),
116+
WorkspaceID: workspaceID,
117+
CreationTime: db.NewVarcharTime(time.Date(2022, 05, 1, 0, 00, 00, 00, time.UTC)),
118+
StoppedTime: db.NewVarcharTime(time.Date(2022, 05, 1, 1, 00, 00, 00, time.UTC)),
119+
Status: status,
120+
},
121+
// End of May
122+
{
123+
ID: uuid.New(),
124+
WorkspaceID: workspaceID,
125+
CreationTime: db.NewVarcharTime(time.Date(2022, 05, 31, 23, 00, 00, 00, time.UTC)),
126+
StoppedTime: db.NewVarcharTime(time.Date(2022, 05, 31, 23, 59, 59, 999999, time.UTC)),
127+
Status: status,
128+
},
129+
// Started in April, but continued into May
130+
{
131+
ID: uuid.New(),
132+
WorkspaceID: workspaceID,
133+
CreationTime: db.NewVarcharTime(time.Date(2022, 04, 30, 23, 00, 00, 00, time.UTC)),
134+
StoppedTime: db.NewVarcharTime(time.Date(2022, 05, 1, 0, 0, 0, 0, time.UTC)),
135+
Status: status,
136+
},
137+
// Started in May, but continued into June
138+
{
139+
ID: uuid.New(),
140+
WorkspaceID: workspaceID,
141+
CreationTime: db.NewVarcharTime(time.Date(2022, 05, 31, 23, 00, 00, 00, time.UTC)),
142+
StoppedTime: db.NewVarcharTime(time.Date(2022, 06, 1, 1, 0, 0, 0, time.UTC)),
143+
Status: status,
144+
},
145+
// Started in April, but continued into June (ran for all of May)
146+
{
147+
ID: uuid.New(),
148+
WorkspaceID: workspaceID,
149+
CreationTime: db.NewVarcharTime(time.Date(2022, 04, 31, 23, 00, 00, 00, time.UTC)),
150+
StoppedTime: db.NewVarcharTime(time.Date(2022, 06, 1, 1, 0, 0, 0, time.UTC)),
151+
Status: status,
152+
},
153+
}
154+
invalid := []*db.WorkspaceInstance{
155+
// Start of June
156+
{
157+
ID: uuid.New(),
158+
WorkspaceID: workspaceID,
159+
CreationTime: db.NewVarcharTime(time.Date(2022, 06, 1, 00, 00, 00, 00, time.UTC)),
160+
StoppedTime: db.NewVarcharTime(time.Date(2022, 06, 1, 1, 0, 0, 0, time.UTC)),
161+
Status: status,
162+
},
163+
}
164+
165+
var all []*db.WorkspaceInstance
166+
all = append(all, valid...)
167+
all = append(all, invalid...)
168+
169+
for _, instance := range all {
170+
tx := conn.Create(instance)
171+
require.NoError(t, tx.Error)
172+
}
173+
174+
startOfMay := time.Date(2022, 05, 1, 0, 00, 00, 00, time.UTC)
175+
startOfJune := time.Date(2022, 06, 1, 0, 00, 00, 00, time.UTC)
176+
retrieved, err := db.ListWorkspaceInstancesInRange(context.Background(), conn, startOfMay, startOfJune)
177+
require.NoError(t, err)
178+
179+
require.Len(t, retrieved, len(valid))
180+
}

0 commit comments

Comments
 (0)