Skip to content

feat(metal): module for setting project custom data#1009

Open
brosenqvist wants to merge 1 commit intoequinix:mainfrom
brosenqvist:feat/metal_project_custom_data
Open

feat(metal): module for setting project custom data#1009
brosenqvist wants to merge 1 commit intoequinix:mainfrom
brosenqvist:feat/metal_project_custom_data

Conversation

@brosenqvist
Copy link
Copy Markdown

@brosenqvist brosenqvist commented Apr 16, 2026

module: equinix_metal_project_custom_data
description: allows the creation, modifications, deletion of a projects custom data

Creation

OpenTofu will perform the following actions:

  # module.metal[0].equinix_metal_project_custom_data.project_metadata will be created
  + resource "equinix_metal_project_custom_data" "project_metadata" {
      + custom_data = jsonencode(
            {
              + bios_config_url = "REDACTED"
            }
        )
      + id          = (known after apply)
      + project_id  = "REDACTED"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions in workspace "REDACTED"?
  OpenTofu will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.metal[0].equinix_metal_project_custom_data.project_metadata: Creating...
module.metal[0].equinix_metal_project_custom_data.project_metadata: Creation complete after 1s [id=REDACTED]

Modification

OpenTofu will perform the following actions:

  # module.metal[0].equinix_metal_project_custom_data.project_metadata will be updated in-place
  ~ resource "equinix_metal_project_custom_data" "project_metadata" {
      ~ custom_data = jsonencode(
          ~ {
              ~ bios_config_url = "REDACTED" -> "REDACTED"
            }
        )
        id          = "REDACTED"
        # (1 unchanged attribute hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions in workspace "REDACTED"?
  OpenTofu will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.metal[0].equinix_metal_project_custom_data.project_metadata: Modifying... [id=REDACTED]
module.metal[0].equinix_metal_project_custom_data.project_metadata: Modifications complete after 1s [id=REDACTED]

Deletion

OpenTofu will perform the following actions:

  # module.metal[0].equinix_metal_project_custom_data.project_metadata will be destroyed
  # (because equinix_metal_project_custom_data.project_metadata is not in configuration)
  - resource "equinix_metal_project_custom_data" "project_metadata" {
      - custom_data = jsonencode(
            {
              - bios_config_url = "REDACTEDn"
            }
        ) -> null
      - id          = "REDACTED" -> null
      - project_id  = "REDACTED" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you want to perform these actions in workspace "REDACTED"?
  OpenTofu will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.metal[0].equinix_metal_project_custom_data.project_metadata: Destroying... [id=REDACTED]
module.metal[0].equinix_metal_project_custom_data.project_metadata: Destruction complete after 2s

@brosenqvist brosenqvist requested a review from a team as a code owner April 16, 2026 12:52
Copilot AI review requested due to automatic review settings April 16, 2026 12:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Terraform (Framework) Metal resource for managing an Equinix Metal project’s customdata, along with docs, an example, and acceptance coverage, and wires it into the provider’s Metal service registration.

Changes:

  • Introduces equinix_metal_project_custom_data resource implementation (CRUD where Delete clears project custom data).
  • Adds acceptance test coverage plus example configuration.
  • Adds template + generated docs for the new resource and registers it in MetalResources().

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/resources/metal/project_custom_data/resource.go Implements create/read/update/delete (delete clears custom data with retries).
internal/resources/metal/project_custom_data/resource_schema.go Defines schema for project_id and custom_data.
internal/resources/metal/project_custom_data/models.go Maps API project customdata into Terraform state.
internal/resources/metal/project_custom_data/resource_test.go Acceptance test for create/update/remove + destroy verification.
internal/provider/services/metal.go Registers the new Metal resource with the framework provider.
templates/resources/metal_project_custom_data.md.tmpl Documentation template for the resource.
docs/resources/metal_project_custom_data.md Generated docs page for the resource.
examples/resources/equinix_metal_project_custom_data/example_1.tf Example usage showing custom_data = jsonencode({...}).

Comment on lines +23 to +26
"custom_data": schema.StringAttribute{
Description: "Project custom data as a JSON object string",
Required: true,
},
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom_data is a required string, but the resource persists a canonicalized JSON string from the API into state (see ResourceModel.parse). If a user supplies equivalent JSON with different whitespace/key ordering, Terraform will see perpetual diffs (config string != state string). Consider normalizing the planned value (e.g., via a custom string plan modifier that parses+re-marshals JSON) or modeling custom_data as a map/object type instead of a raw string to avoid endless updates.

Copilot uses AI. Check for mistakes.
customData := map[string]interface{}{}
if err := json.Unmarshal([]byte(raw), &customData); err != nil {
return nil, fmt.Errorf("custom_data must be valid JSON object: %w", err)
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseCustomData unmarshals into a map, which accepts JSON null without error and yields a nil map. Passing a nil map through to the API will serialize as null, which contradicts the “JSON object” expectation and may lead to unexpected behavior. After unmarshalling, explicitly reject null (nil map) and/or verify the decoded value is a JSON object.

Suggested change
}
}
if customData == nil {
return nil, fmt.Errorf("custom_data must be a JSON object, not null")
}

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +26
// Persist compact canonical JSON to keep state stable.
b, err := json.Marshal(project.GetCustomdata())
if err != nil {
return diag.Diagnostics{diag.NewErrorDiagnostic("Failed to marshal project custom data", err.Error())}
}
m.CustomData = types.StringValue(string(b))
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceModel.parse marshals project.GetCustomdata() directly; if the API returns null for custom data, this will persist the literal string "null" into state even though the schema/docs describe a JSON object. Consider coercing a nil/absent customdata value to {} before marshaling so state remains an object string and avoids surprising diffs.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +82
project, _, err := client.ProjectsApi.FindProjectById(context.Background(), rs.Primary.Attributes["project_id"]).Execute()
if err != nil {
continue
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testAccMetalProjectCustomDataCheckDestroyed ignores all API errors (if err != nil { continue }), which can produce false positives (e.g., transient 5xx/rate-limit errors) where the test passes without verifying custom data was cleared. Prefer only ignoring expected "not found" cases (e.g., 404 after project destroy) and returning other errors so the acceptance test reliably validates behavior.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +24 to +28
Steps: []resource.TestStep{
{
Config: testAccMetalProjectCustomDataConfig(rInt, `{"owner":"platform","env":"test"}`),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "custom_data", `{"env":"test","owner":"platform"}`),
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The acceptance test covers create/update/delete, but it doesn’t assert there is no post-apply drift once custom_data is canonicalized (JSON ordering/whitespace). Consider adding a ConfigPlanChecks step that re-applies the same config and expects an empty plan, to catch perpetual-diff regressions.

Copilot generated this review using guidance from repository custom instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants