mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
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:
@ -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)
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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()),
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
18
coderd/database/dump.sql
generated
18
coderd/database/dump.sql
generated
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,5 @@
|
||||
DROP TABLE workspace_modules;
|
||||
|
||||
ALTER TABLE
|
||||
workspace_resources
|
||||
DROP COLUMN module_path;
|
16
coderd/database/migrations/000276_workspace_modules.up.sql
Normal file
16
coderd/database/migrations/000276_workspace_modules.up.sql
Normal 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);
|
20
coderd/database/migrations/testdata/fixtures/000276_workspace_modules.up.sql
vendored
Normal file
20
coderd/database/migrations/testdata/fixtures/000276_workspace_modules.up.sql
vendored
Normal 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'
|
||||
);
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
16
coderd/database/queries/workspacemodules.sql
Normal file
16
coderd/database/queries/workspacemodules.sql
Normal 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;
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user