Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions nullplatform/scope_definition/data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ data "external" "service_spec" {
}

# Process scope type template with service specification context
# `depends_on` deliberately does NOT list `nullplatform_service_specification`:
# the implicit reference to its `.id` via SERVICE_SPECIFICATION_ID already
# creates that edge. An explicit whole-resource depends_on here defers this
# entire data source to apply-time whenever service_specification has ANY
# pending change, marking provider_type unknown and forcing a phantom
# destroy+recreate of scope_type (provider_type is ForceNew). Confirmed by
# forcing a drift on service_specification with/without this depends_on.
data "external" "scope_type" {
depends_on = [
nullplatform_service_specification.from_template,
data.http.scope_type_template
]

Expand All @@ -52,10 +58,13 @@ data "external" "scope_type" {
}

# Process all action specification templates with full service context
# `depends_on` deliberately does NOT list `nullplatform_service_specification`
# — see the comment on `data.external.scope_type` above. Same cascade, but
# it forces a phantom destroy+recreate of EVERY action_specification
# instance at once (their `type` is ForceNew).
data "external" "action_specs" {
for_each = toset(var.action_spec_names)
depends_on = [
nullplatform_service_specification.from_template,
data.http.action_templates
]

Expand Down
36 changes: 8 additions & 28 deletions nullplatform/scope_definition/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ resource "nullplatform_service_specification" "from_template" {
depends_on = [
data.external.service_spec
]
name = var.service_spec_name
visible_to = concat(local.service_spec_parsed.visible_to, var.extra_visible_to_nrns)
assignable_to = local.service_spec_parsed.assignable_to
type = local.service_spec_parsed.type
attributes = jsonencode(local.service_spec_parsed.attributes)
name = var.service_spec_name
visible_to = concat(local.service_spec_parsed.visible_to, var.extra_visible_to_nrns)
assignable_to = local.service_spec_parsed.assignable_to
type = local.service_spec_parsed.type
# The API persists a `values = {}` key inside `attributes` that the
# template never sets, causing perpetual drift on every plan. Mirroring
# it here keeps config and state in sync instead of masking the diff.
attributes = jsonencode(merge({ values = {} }, local.service_spec_parsed.attributes))
use_default_actions = local.service_spec_parsed.use_default_actions

selectors {
Expand All @@ -19,13 +22,6 @@ resource "nullplatform_service_specification" "from_template" {
provider = local.service_spec_parsed.selectors.provider
sub_category = local.service_spec_parsed.selectors.sub_category
}

# `visible_to` deliberately NOT in ignore_changes so updates from
# `var.extra_visible_to_nrns` flow via `tofu apply`. The other ignored
# attributes (name, attributes, type) are server-enriched after create.
lifecycle {
ignore_changes = [name, attributes, type]
}
}

################################################################################
Expand All @@ -42,18 +38,6 @@ resource "nullplatform_scope_type" "from_template" {
description = var.service_spec_description
provider_id = local.service_specification_id
provider_type = local.scope_type_def.provider_type

# `provider_type` is read from a `data.external` (gomplate-rendered template)
# which Terraform plans as `(known after apply)`. Combined with the provider
# marking `provider_type` as ForceNew, every plan triggers a phantom replace
# even when the upstream template value is unchanged. `status` is server-
# managed (not user-set). Ignoring both is safe — neither field can be
# mutated after create in any meaningful way — and prevents the false
# `must be replaced` diff that would otherwise destroy and re-create the
# scope_type on every apply.
lifecycle {
ignore_changes = [provider_type, status]
}
}

################################################################################
Expand All @@ -74,10 +58,6 @@ resource "nullplatform_action_specification" "from_templates" {
retryable = try(jsondecode(base64decode(data.external.action_specs[each.key].result.json_b64)).retryable, false)
icon = try(jsondecode(base64decode(data.external.action_specs[each.key].result.json_b64)).icon, "")
annotations = jsonencode(try(jsondecode(base64decode(data.external.action_specs[each.key].result.json_b64)).annotations, {}))

lifecycle {
ignore_changes = [name, annotations, parameters, results, type, retryable, icon]
}
}

################################################################################
Expand Down
4 changes: 0 additions & 4 deletions nullplatform/scope_definition_agent_association/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ resource "nullplatform_notification_channel" "from_template" {
}
filters = local.merged_filters_json
lifecycle {
ignore_changes = [
source,
type,
]
replace_triggered_by = [terraform_data.api_key_trigger]
}
}