chore: track terraform modules in telemetry (#15450)

Addresses https://github.com/coder/nexus/issues/35.

This PR:

- Adds a `workspace_modules` table to track modules used by the
Terraform provisioner in provisioner jobs.
- Adds a `module_path` column to the `workspace_resources` table,
allowing to identify which module a resource originates from.
- Starts pushing this new information into telemetry.

For the person reviewing this PR, do not fret about the 1,500 new lines
- ~1,000 of them are auto-generated.
This commit is contained in:
Hugo Dutka
2024-11-16 21:56:19 +01:00
committed by GitHub
parent 968c52bc36
commit aa0dc2daa1
35 changed files with 1633 additions and 412 deletions

View File

@ -2666,6 +2666,20 @@ func (q *querier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceApp
return fetch(q.log, q.auth, q.db.GetWorkspaceByWorkspaceAppID)(ctx, workspaceAppID)
}
func (q *querier) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceModule, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetWorkspaceModulesByJobID(ctx, jobID)
}
func (q *querier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceModule, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
}
return q.db.GetWorkspaceModulesCreatedAfter(ctx, createdAt)
}
func (q *querier) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, func(ctx context.Context, _ interface{}) ([]database.WorkspaceProxy, error) {
return q.db.GetWorkspaceProxies(ctx)
@ -3222,6 +3236,13 @@ func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg databa
return q.db.InsertWorkspaceBuildParameters(ctx, arg)
}
func (q *querier) InsertWorkspaceModule(ctx context.Context, arg database.InsertWorkspaceModuleParams) (database.WorkspaceModule, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
return database.WorkspaceModule{}, err
}
return q.db.InsertWorkspaceModule(ctx, arg)
}
func (q *querier) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) {
return insert(q.log, q.auth, rbac.ResourceWorkspaceProxy, q.db.InsertWorkspaceProxy)(ctx, arg)
}

View File

@ -2907,6 +2907,21 @@ func (s *MethodTestSuite) TestSystemFunctions() {
}
check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows)
}))
s.Run("InsertWorkspaceModule", s.Subtest(func(db database.Store, check *expects) {
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
check.Args(database.InsertWorkspaceModuleParams{
JobID: j.ID,
Transition: database.WorkspaceTransitionStart,
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
}))
s.Run("GetWorkspaceModulesByJobID", s.Subtest(func(db database.Store, check *expects) {
check.Args(uuid.New()).Asserts(rbac.ResourceSystem, policy.ActionRead)
}))
s.Run("GetWorkspaceModulesCreatedAfter", s.Subtest(func(db database.Store, check *expects) {
check.Args(dbtime.Now()).Asserts(rbac.ResourceSystem, policy.ActionRead)
}))
}
func (s *MethodTestSuite) TestNotifications() {

View File

@ -657,11 +657,29 @@ func WorkspaceResource(t testing.TB, db database.Store, orig database.WorkspaceR
Valid: takeFirst(orig.InstanceType.Valid, false),
},
DailyCost: takeFirst(orig.DailyCost, 0),
ModulePath: sql.NullString{
String: takeFirst(orig.ModulePath.String, ""),
Valid: takeFirst(orig.ModulePath.Valid, true),
},
})
require.NoError(t, err, "insert resource")
return resource
}
func WorkspaceModule(t testing.TB, db database.Store, orig database.WorkspaceModule) database.WorkspaceModule {
module, err := db.InsertWorkspaceModule(genCtx, database.InsertWorkspaceModuleParams{
ID: takeFirst(orig.ID, uuid.New()),
JobID: takeFirst(orig.JobID, uuid.New()),
Transition: takeFirst(orig.Transition, database.WorkspaceTransitionStart),
Source: takeFirst(orig.Source, "test-source"),
Version: takeFirst(orig.Version, "v1.0.0"),
Key: takeFirst(orig.Key, "test-key"),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
})
require.NoError(t, err, "insert workspace module")
return module
}
func WorkspaceResourceMetadatums(t testing.TB, db database.Store, seed database.WorkspaceResourceMetadatum) []database.WorkspaceResourceMetadatum {
meta, err := db.InsertWorkspaceResourceMetadata(genCtx, database.InsertWorkspaceResourceMetadataParams{
WorkspaceResourceID: takeFirst(seed.WorkspaceResourceID, uuid.New()),

View File

@ -73,6 +73,7 @@ func New() database.Store {
workspaceAgents: make([]database.WorkspaceAgent, 0),
provisionerJobLogs: make([]database.ProvisionerJobLog, 0),
workspaceResources: make([]database.WorkspaceResource, 0),
workspaceModules: make([]database.WorkspaceModule, 0),
workspaceResourceMetadata: make([]database.WorkspaceResourceMetadatum, 0),
provisionerJobs: make([]database.ProvisionerJob, 0),
templateVersions: make([]database.TemplateVersionTable, 0),
@ -232,6 +233,7 @@ type data struct {
workspaceBuildParameters []database.WorkspaceBuildParameter
workspaceResourceMetadata []database.WorkspaceResourceMetadatum
workspaceResources []database.WorkspaceResource
workspaceModules []database.WorkspaceModule
workspaces []database.WorkspaceTable
workspaceProxies []database.WorkspaceProxy
customRoles []database.CustomRole
@ -6671,6 +6673,32 @@ func (q *FakeQuerier) GetWorkspaceByWorkspaceAppID(_ context.Context, workspaceA
return database.Workspace{}, sql.ErrNoRows
}
func (q *FakeQuerier) GetWorkspaceModulesByJobID(_ context.Context, jobID uuid.UUID) ([]database.WorkspaceModule, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
modules := make([]database.WorkspaceModule, 0)
for _, module := range q.workspaceModules {
if module.JobID == jobID {
modules = append(modules, module)
}
}
return modules, nil
}
func (q *FakeQuerier) GetWorkspaceModulesCreatedAfter(_ context.Context, createdAt time.Time) ([]database.WorkspaceModule, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
modules := make([]database.WorkspaceModule, 0)
for _, module := range q.workspaceModules {
if module.CreatedAt.After(createdAt) {
modules = append(modules, module)
}
}
return modules, nil
}
func (q *FakeQuerier) GetWorkspaceProxies(_ context.Context) ([]database.WorkspaceProxy, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -8233,6 +8261,20 @@ func (q *FakeQuerier) InsertWorkspaceBuildParameters(_ context.Context, arg data
return nil
}
func (q *FakeQuerier) InsertWorkspaceModule(_ context.Context, arg database.InsertWorkspaceModuleParams) (database.WorkspaceModule, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.WorkspaceModule{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
workspaceModule := database.WorkspaceModule(arg)
q.workspaceModules = append(q.workspaceModules, workspaceModule)
return workspaceModule, nil
}
func (q *FakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -8283,6 +8325,7 @@ func (q *FakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.In
Hide: arg.Hide,
Icon: arg.Icon,
DailyCost: arg.DailyCost,
ModulePath: arg.ModulePath,
}
q.workspaceResources = append(q.workspaceResources, resource)
return resource, nil

View File

@ -1568,6 +1568,20 @@ func (m queryMetricsStore) GetWorkspaceByWorkspaceAppID(ctx context.Context, wor
return workspace, err
}
func (m queryMetricsStore) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]database.WorkspaceModule, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceModulesByJobID(ctx, jobID)
m.queryLatencies.WithLabelValues("GetWorkspaceModulesByJobID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]database.WorkspaceModule, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceModulesCreatedAfter(ctx, createdAt)
m.queryLatencies.WithLabelValues("GetWorkspaceModulesCreatedAfter").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetWorkspaceProxies(ctx context.Context) ([]database.WorkspaceProxy, error) {
start := time.Now()
proxies, err := m.s.GetWorkspaceProxies(ctx)
@ -1995,6 +2009,13 @@ func (m queryMetricsStore) InsertWorkspaceBuildParameters(ctx context.Context, a
return err
}
func (m queryMetricsStore) InsertWorkspaceModule(ctx context.Context, arg database.InsertWorkspaceModuleParams) (database.WorkspaceModule, error) {
start := time.Now()
r0, r1 := m.s.InsertWorkspaceModule(ctx, arg)
m.queryLatencies.WithLabelValues("InsertWorkspaceModule").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) InsertWorkspaceProxy(ctx context.Context, arg database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) {
start := time.Now()
proxy, err := m.s.InsertWorkspaceProxy(ctx, arg)

View File

@ -3307,6 +3307,36 @@ func (mr *MockStoreMockRecorder) GetWorkspaceByWorkspaceAppID(arg0, arg1 any) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceByWorkspaceAppID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceByWorkspaceAppID), arg0, arg1)
}
// GetWorkspaceModulesByJobID mocks base method.
func (m *MockStore) GetWorkspaceModulesByJobID(arg0 context.Context, arg1 uuid.UUID) ([]database.WorkspaceModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceModulesByJobID", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceModulesByJobID indicates an expected call of GetWorkspaceModulesByJobID.
func (mr *MockStoreMockRecorder) GetWorkspaceModulesByJobID(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceModulesByJobID", reflect.TypeOf((*MockStore)(nil).GetWorkspaceModulesByJobID), arg0, arg1)
}
// GetWorkspaceModulesCreatedAfter mocks base method.
func (m *MockStore) GetWorkspaceModulesCreatedAfter(arg0 context.Context, arg1 time.Time) ([]database.WorkspaceModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceModulesCreatedAfter", arg0, arg1)
ret0, _ := ret[0].([]database.WorkspaceModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceModulesCreatedAfter indicates an expected call of GetWorkspaceModulesCreatedAfter.
func (mr *MockStoreMockRecorder) GetWorkspaceModulesCreatedAfter(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceModulesCreatedAfter", reflect.TypeOf((*MockStore)(nil).GetWorkspaceModulesCreatedAfter), arg0, arg1)
}
// GetWorkspaceProxies mocks base method.
func (m *MockStore) GetWorkspaceProxies(arg0 context.Context) ([]database.WorkspaceProxy, error) {
m.ctrl.T.Helper()
@ -4224,6 +4254,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceBuildParameters(arg0, arg1 any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceBuildParameters", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceBuildParameters), arg0, arg1)
}
// InsertWorkspaceModule mocks base method.
func (m *MockStore) InsertWorkspaceModule(arg0 context.Context, arg1 database.InsertWorkspaceModuleParams) (database.WorkspaceModule, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertWorkspaceModule", arg0, arg1)
ret0, _ := ret[0].(database.WorkspaceModule)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertWorkspaceModule indicates an expected call of InsertWorkspaceModule.
func (mr *MockStoreMockRecorder) InsertWorkspaceModule(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceModule", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceModule), arg0, arg1)
}
// InsertWorkspaceProxy mocks base method.
func (m *MockStore) InsertWorkspaceProxy(arg0 context.Context, arg1 database.InsertWorkspaceProxyParams) (database.WorkspaceProxy, error) {
m.ctrl.T.Helper()

View File

@ -1634,6 +1634,16 @@ CREATE VIEW workspace_build_with_user AS
COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.';
CREATE TABLE workspace_modules (
id uuid NOT NULL,
job_id uuid NOT NULL,
transition workspace_transition NOT NULL,
source text NOT NULL,
version text NOT NULL,
key text NOT NULL,
created_at timestamp with time zone NOT NULL
);
CREATE TABLE workspace_proxies (
id uuid NOT NULL,
name text NOT NULL,
@ -1700,7 +1710,8 @@ CREATE TABLE workspace_resources (
hide boolean DEFAULT false NOT NULL,
icon character varying(256) DEFAULT ''::character varying NOT NULL,
instance_type character varying(256),
daily_cost integer DEFAULT 0 NOT NULL
daily_cost integer DEFAULT 0 NOT NULL,
module_path text
);
CREATE TABLE workspaces (
@ -2095,6 +2106,8 @@ CREATE INDEX workspace_agents_resource_id_idx ON workspace_agents USING btree (r
CREATE INDEX workspace_app_stats_workspace_id_idx ON workspace_app_stats USING btree (workspace_id);
CREATE INDEX workspace_modules_created_at_idx ON workspace_modules USING btree (created_at);
CREATE UNIQUE INDEX workspace_proxies_lower_name_idx ON workspace_proxies USING btree (lower(name)) WHERE (deleted = false);
CREATE INDEX workspace_resources_job_id_idx ON workspace_resources USING btree (job_id);
@ -2360,6 +2373,9 @@ ALTER TABLE ONLY workspace_builds
ALTER TABLE ONLY workspace_builds
ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_modules
ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
ALTER TABLE ONLY workspace_resource_metadata
ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;

View File

@ -65,6 +65,7 @@ const (
ForeignKeyWorkspaceBuildsJobID ForeignKeyConstraint = "workspace_builds_job_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildsTemplateVersionID ForeignKeyConstraint = "workspace_builds_template_version_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE;
ForeignKeyWorkspaceBuildsWorkspaceID ForeignKeyConstraint = "workspace_builds_workspace_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
ForeignKeyWorkspaceModulesJobID ForeignKeyConstraint = "workspace_modules_job_id_fkey" // ALTER TABLE ONLY workspace_modules ADD CONSTRAINT workspace_modules_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
ForeignKeyWorkspaceResourceMetadataWorkspaceResourceID ForeignKeyConstraint = "workspace_resource_metadata_workspace_resource_id_fkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_workspace_resource_id_fkey FOREIGN KEY (workspace_resource_id) REFERENCES workspace_resources(id) ON DELETE CASCADE;
ForeignKeyWorkspaceResourcesJobID ForeignKeyConstraint = "workspace_resources_job_id_fkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
ForeignKeyWorkspacesOrganizationID ForeignKeyConstraint = "workspaces_organization_id_fkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE RESTRICT;

View File

@ -0,0 +1,5 @@
DROP TABLE workspace_modules;
ALTER TABLE
workspace_resources
DROP COLUMN module_path;

View File

@ -0,0 +1,16 @@
ALTER TABLE
workspace_resources
ADD
COLUMN module_path TEXT;
CREATE TABLE workspace_modules (
id uuid NOT NULL,
job_id uuid NOT NULL REFERENCES provisioner_jobs (id) ON DELETE CASCADE,
transition workspace_transition NOT NULL,
source TEXT NOT NULL,
version TEXT NOT NULL,
key TEXT NOT NULL,
created_at timestamp with time zone NOT NULL
);
CREATE INDEX workspace_modules_created_at_idx ON workspace_modules (created_at);

View File

@ -0,0 +1,20 @@
INSERT INTO
public.workspace_modules (
id,
job_id,
transition,
source,
version,
key,
created_at
)
VALUES
(
'5b1a722c-b8a0-40b0-a3a0-d8078fff9f6c',
'424a58cb-61d6-4627-9907-613c396c4a38',
'start',
'test-source',
'v1.0.0',
'test-key',
'2024-11-08 10:00:00+00'
);

View File

@ -3152,6 +3152,16 @@ type WorkspaceBuildTable struct {
MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
}
type WorkspaceModule struct {
ID uuid.UUID `db:"id" json:"id"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Source string `db:"source" json:"source"`
Version string `db:"version" json:"version"`
Key string `db:"key" json:"key"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
type WorkspaceProxy struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
@ -3186,6 +3196,7 @@ type WorkspaceResource struct {
Icon string `db:"icon" json:"icon"`
InstanceType sql.NullString `db:"instance_type" json:"instance_type"`
DailyCost int32 `db:"daily_cost" json:"daily_cost"`
ModulePath sql.NullString `db:"module_path" json:"module_path"`
}
type WorkspaceResourceMetadatum struct {

View File

@ -323,6 +323,8 @@ type sqlcQuerier interface {
GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error)
GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error)
GetWorkspaceByWorkspaceAppID(ctx context.Context, workspaceAppID uuid.UUID) (Workspace, error)
GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceModule, error)
GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceModule, error)
GetWorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error)
// Finds a workspace proxy that has an access URL or app hostname that matches
// the provided hostname. This is to check if a hostname matches any workspace
@ -404,6 +406,7 @@ type sqlcQuerier interface {
InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error
InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error
InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error
InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error)
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)

View File

@ -14115,9 +14115,124 @@ func (q *sqlQuerier) UpdateWorkspaceBuildProvisionerStateByID(ctx context.Contex
return err
}
const getWorkspaceModulesByJobID = `-- name: GetWorkspaceModulesByJobID :many
SELECT
id, job_id, transition, source, version, key, created_at
FROM
workspace_modules
WHERE
job_id = $1
`
func (q *sqlQuerier) GetWorkspaceModulesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceModule, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceModulesByJobID, jobID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceModule
for rows.Next() {
var i WorkspaceModule
if err := rows.Scan(
&i.ID,
&i.JobID,
&i.Transition,
&i.Source,
&i.Version,
&i.Key,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWorkspaceModulesCreatedAfter = `-- name: GetWorkspaceModulesCreatedAfter :many
SELECT id, job_id, transition, source, version, key, created_at FROM workspace_modules WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceModulesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceModule, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceModulesCreatedAfter, createdAt)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceModule
for rows.Next() {
var i WorkspaceModule
if err := rows.Scan(
&i.ID,
&i.JobID,
&i.Transition,
&i.Source,
&i.Version,
&i.Key,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceModule = `-- name: InsertWorkspaceModule :one
INSERT INTO
workspace_modules (id, job_id, transition, source, version, key, created_at)
VALUES
($1, $2, $3, $4, $5, $6, $7) RETURNING id, job_id, transition, source, version, key, created_at
`
type InsertWorkspaceModuleParams struct {
ID uuid.UUID `db:"id" json:"id"`
JobID uuid.UUID `db:"job_id" json:"job_id"`
Transition WorkspaceTransition `db:"transition" json:"transition"`
Source string `db:"source" json:"source"`
Version string `db:"version" json:"version"`
Key string `db:"key" json:"key"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error) {
row := q.db.QueryRowContext(ctx, insertWorkspaceModule,
arg.ID,
arg.JobID,
arg.Transition,
arg.Source,
arg.Version,
arg.Key,
arg.CreatedAt,
)
var i WorkspaceModule
err := row.Scan(
&i.ID,
&i.JobID,
&i.Transition,
&i.Source,
&i.Version,
&i.Key,
&i.CreatedAt,
)
return i, err
}
const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one
SELECT
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
workspace_resources
WHERE
@ -14138,6 +14253,7 @@ func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID)
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
)
return i, err
}
@ -14217,7 +14333,7 @@ func (q *sqlQuerier) GetWorkspaceResourceMetadataCreatedAfter(ctx context.Contex
const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many
SELECT
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
workspace_resources
WHERE
@ -14244,6 +14360,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
); err != nil {
return nil, err
}
@ -14260,7 +14377,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui
const getWorkspaceResourcesByJobIDs = `-- name: GetWorkspaceResourcesByJobIDs :many
SELECT
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost
id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
FROM
workspace_resources
WHERE
@ -14287,6 +14404,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uu
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
); err != nil {
return nil, err
}
@ -14302,7 +14420,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uu
}
const getWorkspaceResourcesCreatedAfter = `-- name: GetWorkspaceResourcesCreatedAfter :many
SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost FROM workspace_resources WHERE created_at > $1
SELECT id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path FROM workspace_resources WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error) {
@ -14325,6 +14443,7 @@ func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, crea
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
); err != nil {
return nil, err
}
@ -14341,9 +14460,9 @@ func (q *sqlQuerier) GetWorkspaceResourcesCreatedAfter(ctx context.Context, crea
const insertWorkspaceResource = `-- name: InsertWorkspaceResource :one
INSERT INTO
workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost)
workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path
`
type InsertWorkspaceResourceParams struct {
@ -14357,6 +14476,7 @@ type InsertWorkspaceResourceParams struct {
Icon string `db:"icon" json:"icon"`
InstanceType sql.NullString `db:"instance_type" json:"instance_type"`
DailyCost int32 `db:"daily_cost" json:"daily_cost"`
ModulePath sql.NullString `db:"module_path" json:"module_path"`
}
func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) {
@ -14371,6 +14491,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork
arg.Icon,
arg.InstanceType,
arg.DailyCost,
arg.ModulePath,
)
var i WorkspaceResource
err := row.Scan(
@ -14384,6 +14505,7 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork
&i.Icon,
&i.InstanceType,
&i.DailyCost,
&i.ModulePath,
)
return i, err
}

View File

@ -0,0 +1,16 @@
-- name: InsertWorkspaceModule :one
INSERT INTO
workspace_modules (id, job_id, transition, source, version, key, created_at)
VALUES
($1, $2, $3, $4, $5, $6, $7) RETURNING *;
-- name: GetWorkspaceModulesByJobID :many
SELECT
*
FROM
workspace_modules
WHERE
job_id = $1;
-- name: GetWorkspaceModulesCreatedAfter :many
SELECT * FROM workspace_modules WHERE created_at > $1;

View File

@ -27,9 +27,9 @@ SELECT * FROM workspace_resources WHERE created_at > $1;
-- name: InsertWorkspaceResource :one
INSERT INTO
workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost)
workspace_resources (id, created_at, job_id, transition, type, name, hide, icon, instance_type, daily_cost, module_path)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *;
-- name: GetWorkspaceResourceMetadataByResourceIDs :many
SELECT

View File

@ -1261,12 +1261,28 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
slog.F("resource_type", resource.Type),
slog.F("transition", transition))
err = InsertWorkspaceResource(ctx, s.Database, jobID, transition, resource, telemetrySnapshot)
if err != nil {
if err := InsertWorkspaceResource(ctx, s.Database, jobID, transition, resource, telemetrySnapshot); err != nil {
return nil, xerrors.Errorf("insert resource: %w", err)
}
}
}
for transition, modules := range map[database.WorkspaceTransition][]*sdkproto.Module{
database.WorkspaceTransitionStart: jobType.TemplateImport.StartModules,
database.WorkspaceTransitionStop: jobType.TemplateImport.StopModules,
} {
for _, module := range modules {
s.Logger.Info(ctx, "inserting template import job module",
slog.F("job_id", job.ID.String()),
slog.F("module_source", module.Source),
slog.F("module_version", module.Version),
slog.F("module_key", module.Key),
slog.F("transition", transition))
if err := InsertWorkspaceModule(ctx, s.Database, jobID, transition, module, telemetrySnapshot); err != nil {
return nil, xerrors.Errorf("insert module: %w", err)
}
}
}
for _, richParameter := range jobType.TemplateImport.RichParameters {
s.Logger.Info(ctx, "inserting template import job parameter",
@ -1472,6 +1488,11 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
return xerrors.Errorf("insert provisioner job: %w", err)
}
}
for _, module := range jobType.WorkspaceBuild.Modules {
if err := InsertWorkspaceModule(ctx, db, job.ID, workspaceBuild.Transition, module, telemetrySnapshot); err != nil {
return xerrors.Errorf("insert provisioner job module: %w", err)
}
}
// On start, we want to ensure that workspace agents timeout statuses
// are propagated. This method is simple and does not protect against
@ -1653,6 +1674,16 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
return nil, xerrors.Errorf("insert resource: %w", err)
}
}
for _, module := range jobType.TemplateDryRun.Modules {
s.Logger.Info(ctx, "inserting template dry-run job module",
slog.F("job_id", job.ID.String()),
slog.F("module_source", module.Source),
)
if err := InsertWorkspaceModule(ctx, s.Database, jobID, database.WorkspaceTransitionStart, module, telemetrySnapshot); err != nil {
return nil, xerrors.Errorf("insert module: %w", err)
}
}
err = s.Database.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
ID: jobID,
@ -1734,6 +1765,23 @@ func (s *server) startTrace(ctx context.Context, name string, opts ...trace.Span
))...)
}
func InsertWorkspaceModule(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoModule *sdkproto.Module, snapshot *telemetry.Snapshot) error {
module, err := db.InsertWorkspaceModule(ctx, database.InsertWorkspaceModuleParams{
ID: uuid.New(),
CreatedAt: dbtime.Now(),
JobID: jobID,
Transition: transition,
Source: protoModule.Source,
Version: protoModule.Version,
Key: protoModule.Key,
})
if err != nil {
return xerrors.Errorf("insert provisioner job module %q: %w", protoModule.Source, err)
}
snapshot.WorkspaceModules = append(snapshot.WorkspaceModules, telemetry.ConvertWorkspaceModule(module))
return nil
}
func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource, snapshot *telemetry.Snapshot) error {
resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{
ID: uuid.New(),
@ -1749,6 +1797,11 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
String: protoResource.InstanceType,
Valid: protoResource.InstanceType != "",
},
ModulePath: sql.NullString{
String: protoResource.ModulePath,
// empty string is root module
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("insert provisioner job resource %q: %w", protoResource.Name, err)

View File

@ -1430,6 +1430,285 @@ func TestCompleteJob(t *testing.T) {
})
require.NoError(t, err)
})
t.Run("Modules", func(t *testing.T) {
t.Parallel()
templateVersionID := uuid.New()
workspaceBuildID := uuid.New()
cases := []struct {
name string
job *proto.CompletedJob
expectedResources []database.WorkspaceResource
expectedModules []database.WorkspaceModule
provisionerJobParams database.InsertProvisionerJobParams
}{
{
name: "TemplateDryRun",
job: &proto.CompletedJob{
Type: &proto.CompletedJob_TemplateDryRun_{
TemplateDryRun: &proto.CompletedJob_TemplateDryRun{
Resources: []*sdkproto.Resource{{
Name: "something",
Type: "aws_instance",
ModulePath: "module.test1",
}, {
Name: "something2",
Type: "aws_instance",
ModulePath: "",
}},
Modules: []*sdkproto.Module{
{
Key: "test1",
Version: "1.0.0",
Source: "github.com/example/example",
},
},
},
},
},
expectedResources: []database.WorkspaceResource{{
Name: "something",
Type: "aws_instance",
ModulePath: sql.NullString{
String: "module.test1",
Valid: true,
},
Transition: database.WorkspaceTransitionStart,
}, {
Name: "something2",
Type: "aws_instance",
ModulePath: sql.NullString{
String: "",
Valid: true,
},
Transition: database.WorkspaceTransitionStart,
}},
expectedModules: []database.WorkspaceModule{{
Key: "test1",
Version: "1.0.0",
Source: "github.com/example/example",
Transition: database.WorkspaceTransitionStart,
}},
provisionerJobParams: database.InsertProvisionerJobParams{
Type: database.ProvisionerJobTypeTemplateVersionDryRun,
},
},
{
name: "TemplateImport",
job: &proto.CompletedJob{
Type: &proto.CompletedJob_TemplateImport_{
TemplateImport: &proto.CompletedJob_TemplateImport{
StartResources: []*sdkproto.Resource{{
Name: "something",
Type: "aws_instance",
ModulePath: "module.test1",
}},
StartModules: []*sdkproto.Module{
{
Key: "test1",
Version: "1.0.0",
Source: "github.com/example/example",
},
},
StopResources: []*sdkproto.Resource{{
Name: "something2",
Type: "aws_instance",
ModulePath: "module.test2",
}},
StopModules: []*sdkproto.Module{
{
Key: "test2",
Version: "2.0.0",
Source: "github.com/example2/example",
},
},
},
},
},
provisionerJobParams: database.InsertProvisionerJobParams{
Type: database.ProvisionerJobTypeTemplateVersionImport,
Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{
TemplateVersionID: templateVersionID,
})),
},
expectedResources: []database.WorkspaceResource{{
Name: "something",
Type: "aws_instance",
ModulePath: sql.NullString{
String: "module.test1",
Valid: true,
},
Transition: database.WorkspaceTransitionStart,
}, {
Name: "something2",
Type: "aws_instance",
ModulePath: sql.NullString{
String: "module.test2",
Valid: true,
},
Transition: database.WorkspaceTransitionStop,
}},
expectedModules: []database.WorkspaceModule{{
Key: "test1",
Version: "1.0.0",
Source: "github.com/example/example",
Transition: database.WorkspaceTransitionStart,
}, {
Key: "test2",
Version: "2.0.0",
Source: "github.com/example2/example",
Transition: database.WorkspaceTransitionStop,
}},
},
{
name: "WorkspaceBuild",
job: &proto.CompletedJob{
Type: &proto.CompletedJob_WorkspaceBuild_{
WorkspaceBuild: &proto.CompletedJob_WorkspaceBuild{
Resources: []*sdkproto.Resource{{
Name: "something",
Type: "aws_instance",
ModulePath: "module.test1",
}, {
Name: "something2",
Type: "aws_instance",
ModulePath: "",
}},
Modules: []*sdkproto.Module{
{
Key: "test1",
Version: "1.0.0",
Source: "github.com/example/example",
},
},
},
},
},
expectedResources: []database.WorkspaceResource{{
Name: "something",
Type: "aws_instance",
ModulePath: sql.NullString{
String: "module.test1",
Valid: true,
},
Transition: database.WorkspaceTransitionStart,
}, {
Name: "something2",
Type: "aws_instance",
ModulePath: sql.NullString{
String: "",
Valid: true,
},
Transition: database.WorkspaceTransitionStart,
}},
expectedModules: []database.WorkspaceModule{{
Key: "test1",
Version: "1.0.0",
Source: "github.com/example/example",
Transition: database.WorkspaceTransitionStart,
}},
provisionerJobParams: database.InsertProvisionerJobParams{
Type: database.ProvisionerJobTypeWorkspaceBuild,
Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{
WorkspaceBuildID: workspaceBuildID,
})),
},
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
srv, db, _, pd := setup(t, false, &overrides{})
jobParams := c.provisionerJobParams
if jobParams.ID == uuid.Nil {
jobParams.ID = uuid.New()
}
if jobParams.Provisioner == "" {
jobParams.Provisioner = database.ProvisionerTypeEcho
}
if jobParams.StorageMethod == "" {
jobParams.StorageMethod = database.ProvisionerStorageMethodFile
}
job, err := db.InsertProvisionerJob(ctx, jobParams)
tpl := dbgen.Template(t, db, database.Template{
OrganizationID: pd.OrganizationID,
})
tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
JobID: job.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
TemplateID: tpl.ID,
})
_ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
ID: workspaceBuildID,
JobID: job.ID,
WorkspaceID: workspace.ID,
TemplateVersionID: tv.ID,
})
require.NoError(t, err)
_, err = db.AcquireProvisionerJob(ctx, database.AcquireProvisionerJobParams{
WorkerID: uuid.NullUUID{
UUID: pd.ID,
Valid: true,
},
Types: []database.ProvisionerType{jobParams.Provisioner},
})
require.NoError(t, err)
completedJob := c.job
completedJob.JobId = job.ID.String()
_, err = srv.CompleteJob(ctx, completedJob)
require.NoError(t, err)
resources, err := db.GetWorkspaceResourcesByJobID(ctx, job.ID)
require.NoError(t, err)
require.Len(t, resources, len(c.expectedResources))
for _, expectedResource := range c.expectedResources {
for i, resource := range resources {
if resource.Name == expectedResource.Name &&
resource.Type == expectedResource.Type &&
resource.ModulePath == expectedResource.ModulePath &&
resource.Transition == expectedResource.Transition {
resources[i] = database.WorkspaceResource{Name: "matched"}
}
}
}
// all resources should be matched
for _, resource := range resources {
require.Equal(t, "matched", resource.Name)
}
modules, err := db.GetWorkspaceModulesByJobID(ctx, job.ID)
require.NoError(t, err)
require.Len(t, modules, len(c.expectedModules))
for _, expectedModule := range c.expectedModules {
for i, module := range modules {
if module.Key == expectedModule.Key &&
module.Version == expectedModule.Version &&
module.Source == expectedModule.Source &&
module.Transition == expectedModule.Transition {
modules[i] = database.WorkspaceModule{Key: "matched"}
}
}
}
for _, module := range modules {
require.Equal(t, "matched", module.Key)
}
})
}
})
}
func TestInsertWorkspaceResource(t *testing.T) {

View File

@ -456,6 +456,17 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
}
return nil
})
eg.Go(func() error {
workspaceModules, err := r.options.Database.GetWorkspaceModulesCreatedAfter(ctx, createdAfter)
if err != nil {
return xerrors.Errorf("get workspace modules: %w", err)
}
snapshot.WorkspaceModules = make([]WorkspaceModule, 0, len(workspaceModules))
for _, module := range workspaceModules {
snapshot.WorkspaceModules = append(snapshot.WorkspaceModules, ConvertWorkspaceModule(module))
}
return nil
})
eg.Go(func() error {
licenses, err := r.options.Database.GetUnexpiredLicenses(ctx)
if err != nil {
@ -642,7 +653,7 @@ func ConvertWorkspaceApp(app database.WorkspaceApp) WorkspaceApp {
// ConvertWorkspaceResource anonymizes a workspace resource.
func ConvertWorkspaceResource(resource database.WorkspaceResource) WorkspaceResource {
return WorkspaceResource{
r := WorkspaceResource{
ID: resource.ID,
JobID: resource.JobID,
CreatedAt: resource.CreatedAt,
@ -650,6 +661,10 @@ func ConvertWorkspaceResource(resource database.WorkspaceResource) WorkspaceReso
Type: resource.Type,
InstanceType: resource.InstanceType.String,
}
if resource.ModulePath.Valid {
r.ModulePath = &resource.ModulePath.String
}
return r
}
// ConvertWorkspaceResourceMetadata anonymizes workspace metadata.
@ -661,6 +676,29 @@ func ConvertWorkspaceResourceMetadata(metadata database.WorkspaceResourceMetadat
}
}
func shouldSendRawModuleSource(source string) bool {
return strings.Contains(source, "registry.coder.com")
}
func ConvertWorkspaceModule(module database.WorkspaceModule) WorkspaceModule {
source := module.Source
version := module.Version
if !shouldSendRawModuleSource(source) {
source = fmt.Sprintf("%x", sha256.Sum256([]byte(source)))
version = fmt.Sprintf("%x", sha256.Sum256([]byte(version)))
}
return WorkspaceModule{
ID: module.ID,
JobID: module.JobID,
Transition: module.Transition,
Source: source,
Version: version,
Key: module.Key,
CreatedAt: module.CreatedAt,
}
}
// ConvertUser anonymizes a user.
func ConvertUser(dbUser database.User) User {
emailHashed := ""
@ -810,6 +848,7 @@ type Snapshot struct {
WorkspaceProxies []WorkspaceProxy `json:"workspace_proxies"`
WorkspaceResourceMetadata []WorkspaceResourceMetadata `json:"workspace_resource_metadata"`
WorkspaceResources []WorkspaceResource `json:"workspace_resources"`
WorkspaceModules []WorkspaceModule `json:"workspace_modules"`
Workspaces []Workspace `json:"workspaces"`
NetworkEvents []NetworkEvent `json:"network_events"`
}
@ -878,6 +917,11 @@ type WorkspaceResource struct {
Transition database.WorkspaceTransition `json:"transition"`
Type string `json:"type"`
InstanceType string `json:"instance_type"`
// ModulePath is nullable because it was added a long time after the
// original workspace resource telemetry was added. All new resources
// will have a module path, but deployments with older resources still
// in the database will not.
ModulePath *string `json:"module_path"`
}
type WorkspaceResourceMetadata struct {
@ -886,6 +930,16 @@ type WorkspaceResourceMetadata struct {
Sensitive bool `json:"sensitive"`
}
type WorkspaceModule struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
JobID uuid.UUID `json:"job_id"`
Transition database.WorkspaceTransition `json:"transition"`
Key string `json:"key"`
Version string `json:"version"`
Source string `json:"source"`
}
type WorkspaceAgent struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`

View File

@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"sort"
"testing"
"time"
@ -20,6 +21,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbmem"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/testutil"
@ -87,6 +89,8 @@ func TestTelemetry(t *testing.T) {
assert.NoError(t, err)
_, _ = dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{})
_ = dbgen.WorkspaceModule(t, db, database.WorkspaceModule{})
_, snapshot := collectSnapshot(t, db, nil)
require.Len(t, snapshot.ProvisionerJobs, 1)
require.Len(t, snapshot.Licenses, 1)
@ -103,6 +107,7 @@ func TestTelemetry(t *testing.T) {
require.Len(t, snapshot.WorkspaceResources, 1)
require.Len(t, snapshot.WorkspaceAgentStats, 1)
require.Len(t, snapshot.WorkspaceProxies, 1)
require.Len(t, snapshot.WorkspaceModules, 1)
wsa := snapshot.WorkspaceAgents[0]
require.Len(t, wsa.Subsystems, 2)
@ -119,6 +124,31 @@ func TestTelemetry(t *testing.T) {
require.Len(t, snapshot.Users, 1)
require.Equal(t, snapshot.Users[0].EmailHashed, "bb44bf07cf9a2db0554bba63a03d822c927deae77df101874496df5a6a3e896d@coder.com")
})
t.Run("HashedModule", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
pj := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{})
_ = dbgen.WorkspaceModule(t, db, database.WorkspaceModule{
JobID: pj.ID,
Source: "registry.coder.com/terraform/aws",
Version: "1.0.0",
})
_ = dbgen.WorkspaceModule(t, db, database.WorkspaceModule{
JobID: pj.ID,
Source: "internal-url.com/some-module",
Version: "1.0.0",
})
_, snapshot := collectSnapshot(t, db, nil)
require.Len(t, snapshot.WorkspaceModules, 2)
modules := snapshot.WorkspaceModules
sort.Slice(modules, func(i, j int) bool {
return modules[i].Source < modules[j].Source
})
require.Equal(t, modules[0].Source, "921c61d6f3eef5118f3cae658d1518b378c5b02a4955a766c791440894d989c5")
require.Equal(t, modules[0].Version, "92521fc3cbd964bdc9f584a991b89fddaa5754ed1cc96d6d42445338669c1305")
require.Equal(t, modules[1].Source, "registry.coder.com/terraform/aws")
require.Equal(t, modules[1].Version, "1.0.0")
})
}
// nolint:paralleltest