mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: use actual ai_task instances for HasAITasks (#25197)
Previously, `hasAITaskResources()` scanned the Terraform graph for `coder_ai_task` node labels. The graph includes resource definitions regardless of `count`, so templates with `count = 0` were incorrectly marked as `HasAITasks = true`, causing them to appear on the `/tasks` page when no AI task resources would be created. Replace the graph-based check with `len(aiTasks) > 0`. The `aiTasks` slice is populated from state modules where Terraform has already evaluated `count`, so it correctly reflects actual resource instances. ref: https://linear.app/codercom/issue/ECO-39/make-coder-tasks-respect-count > Generated with [Coder Agents](https://coder.com/agents) --------- Signed-off-by: 35C4n0r <work.jaykumar@gmail.com> Signed-off-by: Jay Kumar <jay.kumar@coder.com>
This commit is contained in:
@@ -173,25 +173,6 @@ type State struct {
|
||||
|
||||
var ErrInvalidTerraformAddr = xerrors.New("invalid terraform address")
|
||||
|
||||
// hasAITaskResources is used to determine if a template has *any* `coder_ai_task` resources defined. During template
|
||||
// import, it's possible that none of these have `count=1` since count may be dependent on the value of a `coder_parameter`
|
||||
// or something else.
|
||||
// We need to know at template import if these resources exist to inform the frontend of their existence.
|
||||
func hasAITaskResources(graph *gographviz.Graph) bool {
|
||||
for _, node := range graph.Nodes.Lookup {
|
||||
// Check if this node is a coder_ai_task resource
|
||||
if label, exists := node.Attrs["label"]; exists {
|
||||
labelValue := strings.Trim(label, `"`)
|
||||
// The first condition is for the case where the resource is in the root module.
|
||||
// The second condition is for the case where the resource is in a child module.
|
||||
if strings.HasPrefix(labelValue, "coder_ai_task.") || strings.Contains(labelValue, ".coder_ai_task.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasExternalAgentResources(graph *gographviz.Graph) bool {
|
||||
for _, node := range graph.Nodes.Lookup {
|
||||
if label, exists := node.Attrs["label"]; exists {
|
||||
@@ -1083,14 +1064,12 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
|
||||
slices.SortFunc(externalAuthProviders, func(a, b *proto.ExternalAuthProviderResource) int {
|
||||
return cmp.Compare(a.Id, b.Id)
|
||||
})
|
||||
hasAITasks := hasAITaskResources(graph)
|
||||
|
||||
return &State{
|
||||
Resources: resources,
|
||||
Parameters: parameters,
|
||||
Presets: presets,
|
||||
ExternalAuthProviders: externalAuthProviders,
|
||||
HasAITasks: hasAITasks,
|
||||
HasAITasks: len(aiTasks) > 0,
|
||||
AITasks: aiTasks,
|
||||
HasExternalAgents: hasExternalAgentResources(graph),
|
||||
}, nil
|
||||
|
||||
@@ -1764,6 +1764,32 @@ func TestAITasks(t *testing.T) {
|
||||
require.Equal(t, "5ece4674-dd35-4f16-88c8-82e40e72e2fd", sidebarApp.GetId())
|
||||
require.Equal(t, "5ece4674-dd35-4f16-88c8-82e40e72e2fd", state.AITasks[0].AppId)
|
||||
})
|
||||
|
||||
t.Run("Disabled with count zero", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// nolint:dogsled
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
|
||||
// This fixture has coder_ai_task.a in the graph (resource is defined
|
||||
// in the .tf file) but NOT in PlannedValues (count = 0). The old
|
||||
// graph-based check returned true here; the new len(aiTasks) > 0
|
||||
// check should return false.
|
||||
dir := filepath.Join(filepath.Dir(filename), "testdata", "resources", "ai-tasks-disabled")
|
||||
tfPlanRaw, err := os.ReadFile(filepath.Join(dir, "ai-tasks-disabled.tfplan.json"))
|
||||
require.NoError(t, err)
|
||||
var tfPlan tfjson.Plan
|
||||
err = json.Unmarshal(tfPlanRaw, &tfPlan)
|
||||
require.NoError(t, err)
|
||||
tfPlanGraph, err := os.ReadFile(filepath.Join(dir, "ai-tasks-disabled.tfplan.dot"))
|
||||
require.NoError(t, err)
|
||||
|
||||
state, err := terraform.ConvertState(ctx, []*tfjson.StateModule{tfPlan.PlannedValues.RootModule, tfPlan.PriorState.Values.RootModule}, string(tfPlanGraph), logger)
|
||||
require.NotNil(t, state)
|
||||
require.NoError(t, err)
|
||||
require.False(t, state.HasAITasks)
|
||||
require.Empty(t, state.AITasks)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExternalAgents(t *testing.T) {
|
||||
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
digraph {
|
||||
compound = "true"
|
||||
newrank = "true"
|
||||
subgraph "root" {
|
||||
"[root] coder_ai_task.a (expand)" [label = "coder_ai_task.a", shape = "box"]
|
||||
"[root] data.coder_provisioner.me (expand)" [label = "data.coder_provisioner.me", shape = "box"]
|
||||
"[root] data.coder_workspace.me (expand)" [label = "data.coder_workspace.me", shape = "box"]
|
||||
"[root] data.coder_workspace_owner.me (expand)" [label = "data.coder_workspace_owner.me", shape = "box"]
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
|
||||
"[root] coder_ai_task.a (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] data.coder_provisioner.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] data.coder_workspace.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] data.coder_workspace_owner.me (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_ai_task.a (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_provisioner.me (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace.me (expand)"
|
||||
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] data.coder_workspace_owner.me (expand)"
|
||||
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+139
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"format_version": "1.2",
|
||||
"terraform_version": "1.15.2",
|
||||
"planned_values": {
|
||||
"root_module": {}
|
||||
},
|
||||
"prior_state": {
|
||||
"format_version": "1.0",
|
||||
"terraform_version": "1.15.2",
|
||||
"values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "data.coder_provisioner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_provisioner",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"arch": "amd64",
|
||||
"id": "4bd4900e-85ab-4f4e-9378-153f9630d2aa",
|
||||
"os": "linux"
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 1,
|
||||
"values": {
|
||||
"access_port": 443,
|
||||
"access_url": "https://dev.coder.com/",
|
||||
"id": "f8c4851f-dcbd-48bc-9a14-3fd506f8f015",
|
||||
"is_prebuild": false,
|
||||
"is_prebuild_claim": false,
|
||||
"name": "ai-task-plan-check",
|
||||
"prebuild_count": 0,
|
||||
"start_count": 1,
|
||||
"template_id": "",
|
||||
"template_name": "",
|
||||
"template_version": "",
|
||||
"transition": "start"
|
||||
},
|
||||
"sensitive_values": {}
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace_owner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace_owner",
|
||||
"name": "me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"email": "default@example.com",
|
||||
"full_name": "default",
|
||||
"groups": [],
|
||||
"id": "33769beb-1777-4d16-8774-2da632ca9611",
|
||||
"login_type": null,
|
||||
"name": "default",
|
||||
"oidc_access_token": "",
|
||||
"rbac_roles": [],
|
||||
"session_token": "",
|
||||
"ssh_private_key": "",
|
||||
"ssh_public_key": ""
|
||||
},
|
||||
"sensitive_values": {
|
||||
"groups": [],
|
||||
"oidc_access_token": true,
|
||||
"rbac_roles": [],
|
||||
"session_token": true,
|
||||
"ssh_private_key": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"configuration": {
|
||||
"provider_config": {
|
||||
"coder": {
|
||||
"name": "coder",
|
||||
"full_name": "registry.terraform.io/coder/coder",
|
||||
"version_constraint": ">= 2.0.0"
|
||||
}
|
||||
},
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"address": "coder_ai_task.a",
|
||||
"mode": "managed",
|
||||
"type": "coder_ai_task",
|
||||
"name": "a",
|
||||
"provider_config_key": "coder",
|
||||
"expressions": {
|
||||
"app_id": {
|
||||
"constant_value": "5ece4674-dd35-4f16-88c8-82e40e72e2fd"
|
||||
}
|
||||
},
|
||||
"schema_version": 1,
|
||||
"count_expression": {
|
||||
"constant_value": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"address": "data.coder_provisioner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_provisioner",
|
||||
"name": "me",
|
||||
"provider_config_key": "coder",
|
||||
"schema_version": 1
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace",
|
||||
"name": "me",
|
||||
"provider_config_key": "coder",
|
||||
"schema_version": 1
|
||||
},
|
||||
{
|
||||
"address": "data.coder_workspace_owner.me",
|
||||
"mode": "data",
|
||||
"type": "coder_workspace_owner",
|
||||
"name": "me",
|
||||
"provider_config_key": "coder",
|
||||
"schema_version": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"timestamp": "2026-05-13T11:32:56Z",
|
||||
"applyable": false,
|
||||
"complete": true,
|
||||
"errored": false
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Resources": [],
|
||||
"Parameters": [],
|
||||
"Presets": [],
|
||||
"ExternalAuthProviders": [],
|
||||
"AITasks": [],
|
||||
"HasAITasks": false,
|
||||
"HasExternalAgents": false
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_ai_task" "a" {
|
||||
count = 0
|
||||
app_id = "5ece4674-dd35-4f16-88c8-82e40e72e2fd"
|
||||
}
|
||||
Generated
+2
-5
@@ -4030,11 +4030,8 @@ type GraphComplete struct {
|
||||
Parameters []*RichParameter `protobuf:"bytes,4,rep,name=parameters,proto3" json:"parameters,omitempty"`
|
||||
ExternalAuthProviders []*ExternalAuthProviderResource `protobuf:"bytes,5,rep,name=external_auth_providers,json=externalAuthProviders,proto3" json:"external_auth_providers,omitempty"`
|
||||
Presets []*Preset `protobuf:"bytes,6,rep,name=presets,proto3" json:"presets,omitempty"`
|
||||
// Whether a template has any `coder_ai_task` resources defined, even if not planned for creation.
|
||||
// During a template import, a plan is run which may not yield in any `coder_ai_task` resources, but nonetheless we
|
||||
// still need to know that such resources are defined.
|
||||
//
|
||||
// See `hasAITaskResources` in provisioner/terraform/resources.go for more details.
|
||||
// Whether actual `coder_ai_task` resource instances exist.
|
||||
// Resources defined with count = 0 do not set this flag.
|
||||
HasAiTasks bool `protobuf:"varint,7,opt,name=has_ai_tasks,json=hasAiTasks,proto3" json:"has_ai_tasks,omitempty"`
|
||||
AiTasks []*AITask `protobuf:"bytes,8,rep,name=ai_tasks,json=aiTasks,proto3" json:"ai_tasks,omitempty"`
|
||||
HasExternalAgents bool `protobuf:"varint,9,opt,name=has_external_agents,json=hasExternalAgents,proto3" json:"has_external_agents,omitempty"`
|
||||
|
||||
@@ -504,11 +504,8 @@ message GraphComplete {
|
||||
repeated RichParameter parameters = 4;
|
||||
repeated ExternalAuthProviderResource external_auth_providers = 5;
|
||||
repeated Preset presets = 6;
|
||||
// Whether a template has any `coder_ai_task` resources defined, even if not planned for creation.
|
||||
// During a template import, a plan is run which may not yield in any `coder_ai_task` resources, but nonetheless we
|
||||
// still need to know that such resources are defined.
|
||||
//
|
||||
// See `hasAITaskResources` in provisioner/terraform/resources.go for more details.
|
||||
// Whether actual `coder_ai_task` resource instances exist.
|
||||
// Resources defined with count = 0 do not set this flag.
|
||||
bool has_ai_tasks = 7;
|
||||
repeated provisioner.AITask ai_tasks = 8;
|
||||
bool has_external_agents = 9;
|
||||
|
||||
Generated
+2
-5
@@ -562,11 +562,8 @@ export interface GraphComplete {
|
||||
externalAuthProviders: ExternalAuthProviderResource[];
|
||||
presets: Preset[];
|
||||
/**
|
||||
* Whether a template has any `coder_ai_task` resources defined, even if not planned for creation.
|
||||
* During a template import, a plan is run which may not yield in any `coder_ai_task` resources, but nonetheless we
|
||||
* still need to know that such resources are defined.
|
||||
*
|
||||
* See `hasAITaskResources` in provisioner/terraform/resources.go for more details.
|
||||
* Whether actual `coder_ai_task` resource instances exist.
|
||||
* Resources defined with count = 0 do not set this flag.
|
||||
*/
|
||||
hasAiTasks: boolean;
|
||||
aiTasks: AITask[];
|
||||
|
||||
Reference in New Issue
Block a user