Skip to content

Commit 8a17eff

Browse files
committed
feat: store coder_ai_task resource state
Signed-off-by: Danny Kopping <dannykopping@gmail.com>
1 parent e1ad75b commit 8a17eff

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

coderd/provisionerdserver/provisionerdserver.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
protobuf "google.golang.org/protobuf/proto"
2929

3030
"cdr.dev/slog"
31+
3132
"github.com/coder/coder/v2/coderd/util/slice"
3233

3334
"github.com/coder/coder/v2/codersdk/drpcsdk"
@@ -1654,6 +1655,17 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
16541655
if err != nil {
16551656
return xerrors.Errorf("update template version external auth providers: %w", err)
16561657
}
1658+
err = db.UpdateTemplateVersionAITaskByJobID(ctx, database.UpdateTemplateVersionAITaskByJobIDParams{
1659+
JobID: jobID,
1660+
HasAITask: sql.NullBool{
1661+
Bool: jobType.TemplateImport.HasAiTasks,
1662+
Valid: true,
1663+
},
1664+
UpdatedAt: now,
1665+
})
1666+
if err != nil {
1667+
return xerrors.Errorf("update template version external auth providers: %w", err)
1668+
}
16571669

16581670
// Process terraform values
16591671
plan := jobType.TemplateImport.Plan
@@ -1866,6 +1878,34 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
18661878
}
18671879
}
18681880

1881+
var sidebarAppID uuid.NullUUID
1882+
if len(jobType.WorkspaceBuild.AiTasks) == 1 {
1883+
task := jobType.WorkspaceBuild.AiTasks[0]
1884+
if task.SidebarApp == nil {
1885+
return xerrors.Errorf("update ai task: sidebar app is nil")
1886+
}
1887+
1888+
id, err := uuid.Parse(task.SidebarApp.Id)
1889+
if err != nil {
1890+
return xerrors.Errorf("parse sidebar app id: %w", err)
1891+
}
1892+
1893+
sidebarAppID = uuid.NullUUID{UUID: id, Valid: true}
1894+
}
1895+
1896+
err = db.UpdateWorkspaceBuildAITaskByID(ctx, database.UpdateWorkspaceBuildAITaskByIDParams{
1897+
ID: workspaceBuild.ID,
1898+
HasAITask: sql.NullBool{
1899+
Bool: len(jobType.WorkspaceBuild.AiTasks) > 0,
1900+
Valid: true,
1901+
},
1902+
SidebarAppID: sidebarAppID,
1903+
UpdatedAt: now,
1904+
})
1905+
if err != nil {
1906+
return xerrors.Errorf("update workspace build ai tasks flag: %w", err)
1907+
}
1908+
18691909
// Insert timings inside the transaction now
18701910
// nolint:exhaustruct // The other fields are set further down.
18711911
params := database.InsertProvisionerJobTimingsParams{
@@ -2570,8 +2610,13 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
25702610
openIn = database.WorkspaceAppOpenInSlimWindow
25712611
}
25722612

2613+
id, err := uuid.Parse(app.Id)
2614+
if err != nil {
2615+
return xerrors.Errorf("parse app uuid: %w", err)
2616+
}
2617+
25732618
dbApp, err := db.InsertWorkspaceApp(ctx, database.InsertWorkspaceAppParams{
2574-
ID: uuid.New(),
2619+
ID: id,
25752620
CreatedAt: dbtime.Now(),
25762621
AgentID: dbAgent.ID,
25772622
Slug: slug,

provisioner/terraform/executor.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,8 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
361361
Plan: planJSON,
362362
ResourceReplacements: resReps,
363363
ModuleFiles: moduleFiles,
364+
HasAiTasks: state.HasAITasks,
365+
AiTasks: state.AITasks,
364366
}
365367

366368
return msg, nil
@@ -577,6 +579,7 @@ func (e *executor) apply(
577579
ExternalAuthProviders: state.ExternalAuthProviders,
578580
State: stateContent,
579581
Timings: e.timings.aggregate(),
582+
AiTasks: state.AITasks,
580583
}, nil
581584
}
582585

provisioner/terraform/resources.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/awalterschulze/gographviz"
10+
"github.com/google/uuid"
1011
tfjson "github.com/hashicorp/terraform-json"
1112
"github.com/mitchellh/mapstructure"
1213
"golang.org/x/xerrors"
@@ -93,6 +94,7 @@ type agentDisplayAppsAttributes struct {
9394

9495
// A mapping of attributes on the "coder_app" resource.
9596
type agentAppAttributes struct {
97+
ID string `mapstructure:"id"`
9698
AgentID string `mapstructure:"agent_id"`
9799
// Slug is required in terraform, but to avoid breaking existing users we
98100
// will default to the resource name if it is not specified.
@@ -160,10 +162,29 @@ type State struct {
160162
Parameters []*proto.RichParameter
161163
Presets []*proto.Preset
162164
ExternalAuthProviders []*proto.ExternalAuthProviderResource
165+
AITasks []*proto.AITask
166+
HasAITasks bool
163167
}
164168

165169
var ErrInvalidTerraformAddr = xerrors.New("invalid terraform address")
166170

171+
// hasAITaskResources is used to determine if a template has *any* `coder_ai_task` resources defined. During template
172+
// import, it's possible that none of these have `count=1` since count may be dependent on the value of a `coder_parameter`
173+
// or something else.
174+
// We need to know at template import if these resources exist to inform the frontend of their existence.
175+
func hasAITaskResources(graph *gographviz.Graph) bool {
176+
for _, node := range graph.Nodes.Lookup {
177+
// Check if this node is a coder_ai_task resource
178+
if label, exists := node.Attrs["label"]; exists {
179+
labelValue := strings.Trim(label, `"`)
180+
if strings.Contains(labelValue, "coder_ai_task.") {
181+
return true
182+
}
183+
}
184+
}
185+
return false
186+
}
187+
167188
// ConvertState consumes Terraform state and a GraphViz representation
168189
// produced by `terraform graph` to produce resources consumable by Coder.
169190
// nolint:gocognit // This function makes more sense being large for now, until refactored.
@@ -187,6 +208,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
187208
// Extra array to preserve the order of rich parameters.
188209
tfResourcesRichParameters := make([]*tfjson.StateResource, 0)
189210
tfResourcesPresets := make([]*tfjson.StateResource, 0)
211+
tfResourcesAITasks := make([]*tfjson.StateResource, 0)
190212
var findTerraformResources func(mod *tfjson.StateModule)
191213
findTerraformResources = func(mod *tfjson.StateModule) {
192214
for _, module := range mod.ChildModules {
@@ -199,6 +221,9 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
199221
if resource.Type == "coder_workspace_preset" {
200222
tfResourcesPresets = append(tfResourcesPresets, resource)
201223
}
224+
if resource.Type == "coder_ai_task" {
225+
tfResourcesAITasks = append(tfResourcesAITasks, resource)
226+
}
202227

203228
label := convertAddressToLabel(resource.Address)
204229
if tfResourcesByLabel[label] == nil {
@@ -522,7 +547,17 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
522547
continue
523548
}
524549

550+
id := attrs.ID
551+
if id == "" {
552+
// This should never happen since the "id" attribute is set on creation:
553+
// https://github.com/coder/terraform-provider-coder/blob/cfa101df4635e405e66094fa7779f9a89d92f400/provider/app.go#L37
554+
logger.Warn(ctx, "coder_app's id was unexpectedly empty", slog.F("name", attrs.Name))
555+
556+
id = uuid.NewString()
557+
}
558+
525559
agent.Apps = append(agent.Apps, &proto.App{
560+
Id: id,
526561
Slug: attrs.Slug,
527562
DisplayName: attrs.DisplayName,
528563
Command: attrs.Command,
@@ -940,6 +975,27 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
940975
)
941976
}
942977

978+
// This will only pick up resources which will actually be created.
979+
aiTasks := make([]*proto.AITask, 0, len(tfResourcesAITasks))
980+
for _, resource := range tfResourcesAITasks {
981+
var task provider.AITask
982+
err = mapstructure.Decode(resource.AttributeValues, &task)
983+
if err != nil {
984+
return nil, xerrors.Errorf("decode coder_ai_task attributes: %w", err)
985+
}
986+
987+
if len(task.SidebarApp) < 1 {
988+
return nil, xerrors.Errorf("coder_ai_task has no sidebar_app defined")
989+
}
990+
991+
aiTasks = append(aiTasks, &proto.AITask{
992+
Id: task.ID,
993+
SidebarApp: &proto.AITaskSidebarApp{
994+
Id: task.SidebarApp[0].ID,
995+
},
996+
})
997+
}
998+
943999
// A map is used to ensure we don't have duplicates!
9441000
externalAuthProvidersMap := map[string]*proto.ExternalAuthProviderResource{}
9451001
for _, tfResources := range tfResourcesByLabel {
@@ -975,6 +1031,8 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
9751031
Parameters: parameters,
9761032
Presets: presets,
9771033
ExternalAuthProviders: externalAuthProviders,
1034+
HasAITasks: hasAITaskResources(graph),
1035+
AITasks: aiTasks,
9781036
}, nil
9791037
}
9801038

provisionerd/runner/runner.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import (
77
"errors"
88
"fmt"
99
"reflect"
10+
"slices"
1011
"strings"
1112
"sync"
1213
"sync/atomic"
1314
"time"
1415

16+
"github.com/coder/terraform-provider-coder/v2/provider"
1517
"github.com/google/uuid"
1618
"github.com/prometheus/client_golang/prometheus"
1719
"go.opentelemetry.io/otel/attribute"
@@ -584,8 +586,6 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
584586
externalAuthProviderNames = append(externalAuthProviderNames, it.Id)
585587
}
586588

587-
// fmt.Println("completed job: template import: graph:", startProvision.Graph)
588-
589589
return &proto.CompletedJob{
590590
JobId: r.job.JobId,
591591
Type: &proto.CompletedJob_TemplateImport_{
@@ -603,6 +603,7 @@ func (r *Runner) runTemplateImport(ctx context.Context) (*proto.CompletedJob, *p
603603
ModuleFiles: startProvision.ModuleFiles,
604604
// ModuleFileHash will be populated if the file is uploaded async
605605
ModuleFilesHash: []byte{},
606+
HasAiTasks: startProvision.HasAITasks,
606607
},
607608
},
608609
}, nil
@@ -666,6 +667,7 @@ type templateImportProvision struct {
666667
Presets []*sdkproto.Preset
667668
Plan json.RawMessage
668669
ModuleFiles []byte
670+
HasAITasks bool
669671
}
670672

671673
// Performs a dry-run provision when importing a template.
@@ -799,6 +801,15 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(
799801
}
800802
}
801803

804+
if c.HasAiTasks {
805+
hasPromptParam := slices.ContainsFunc(c.Parameters, func(param *sdkproto.RichParameter) bool {
806+
return param.Name == provider.TaskPromptParameterName
807+
})
808+
if !hasPromptParam {
809+
return nil, xerrors.Errorf("coder_parameter named '%s' is required when 'coder_ai_task' resource is defined", provider.TaskPromptParameterName)
810+
}
811+
}
812+
802813
return &templateImportProvision{
803814
Resources: c.Resources,
804815
Parameters: c.Parameters,
@@ -807,6 +818,7 @@ func (r *Runner) runTemplateImportProvisionWithRichParameters(
807818
Presets: c.Presets,
808819
Plan: c.Plan,
809820
ModuleFiles: moduleFilesData,
821+
HasAITasks: c.HasAiTasks,
810822
}, nil
811823
default:
812824
return nil, xerrors.Errorf("invalid message type %q received from provisioner",
@@ -1047,6 +1059,9 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
10471059
},
10481060
}
10491061
}
1062+
if len(planComplete.AiTasks) > 1 {
1063+
return nil, r.failedWorkspaceBuildf("only one 'coder_ai_task' resource can be provisioned per template")
1064+
}
10501065

10511066
r.logger.Info(context.Background(), "plan request successful",
10521067
slog.F("resource_count", len(planComplete.Resources)),
@@ -1124,6 +1139,7 @@ func (r *Runner) runWorkspaceBuild(ctx context.Context) (*proto.CompletedJob, *p
11241139
Modules: planComplete.Modules,
11251140
// Resource replacements are discovered at plan time, only.
11261141
ResourceReplacements: planComplete.ResourceReplacements,
1142+
AiTasks: applyComplete.AiTasks,
11271143
},
11281144
},
11291145
}, nil

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy