Skip to content

Add opslevel_integration_kubernetes resource - #660

Merged
saditya370 merged 4 commits into
mainfrom
add-integration-kubernetes
Jul 23, 2026
Merged

Add opslevel_integration_kubernetes resource#660
saditya370 merged 4 commits into
mainfrom
add-integration-kubernetes

Conversation

@saditya370

Copy link
Copy Markdown
Collaborator

Add opslevel_integration_kubernetes resource

Lets users manage a Kubernetes integration's ETL config as code (GitLab issue 14148):

resource "opslevel_integration_kubernetes" "this" {
  name                 = "Kubernetes Integration"
  extract_definition   = file("extract.yaml")   # optional
  transform_definition = file("transform.yaml") # optional
}

Bumps opslevel-go to v2026.7.21 for the new client methods.

Notes for reviewers

  • The API parses and re-serializes the YAML definitions, so the returned string never matches
    the configured one byte-for-byte, and the server fills in default definitions when none are
    given. Both fields are therefore Optional + Computed, and state keeps the configured value
    (falling back to the API value on import or when unset). Trade-off: terraform plan won't
    catch ETL edits made through the UI. That's documented on the attributes — a semantic-YAML
    plan modifier could restore drift detection later if we want it.
  • The extractDefinition / transformDefinition arguments sit behind the k8s_etl_flow
    feature flag (default-on for new accounts); older accounts without it get a 402.

Testing

  • tests/remote/integration_kubernetes.tftest.hcl against a dev instance: create with both
    definitions, update both in place, destroy — all green.
  • go build, unit tests, golangci-lint, gofumpt, terraform fmt all clean; docs generated
    with tfplugindocs.

Manages a Kubernetes integration's ETL config (extract_definition /
transform_definition) as code. Requires opslevel-go v2026.7.21.
@saditya370 saditya370 self-assigned this Jul 22, 2026
@andrewstillv15

Copy link
Copy Markdown
Contributor

Can we update the opslevel-go submodule too?

Comment thread opslevel/resource_opslevel_integration_kubernetes.go
Comment thread opslevel/resource_opslevel_integration_kubernetes.go Outdated
Comment thread opslevel/resource_opslevel_integration_kubernetes.go Outdated
Review feedback: the API manages extract/transform as a unit, so both now
live under one optional etl_definition object. Definitions are compared by
unmarshalled value so formatting normalization is not drift but UI edits are.
@saditya370

Copy link
Copy Markdown
Collaborator Author

Addressed all three comments in d09e7ad:

  • extract/transform now live under a single optional etl_definition object with both members required, so the provider always sends the pair together and can't hit the clear-one-clears-both behavior.
  • Definitions are compared by unmarshalled value on refresh — formatting normalization no longer churns state, but edits made through the UI now show up as drift.

Re-ran the acceptance test against a dev instance: create with both definitions, in-place update, destroy — all green.

@saditya370

Copy link
Copy Markdown
Collaborator Author

Submodule bumped to v2026.7.21 as well.

Comment on lines +36 to +38
// IntegrationKubernetesEtlDefinitionModel describes the extract/transform definition pair.
// The API manages the two definitions as a unit (clearing one clears both), so they are
// modeled as a single object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// IntegrationKubernetesEtlDefinitionModel describes the extract/transform definition pair.
// The API manages the two definitions as a unit (clearing one clears both), so they are
// modeled as a single object.

Comment on lines +58 to +59
// yamlEquivalent reports whether two YAML documents carry the same data. The API parses and
// re-serializes stored definitions, so equivalent documents can differ in formatting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// yamlEquivalent reports whether two YAML documents carry the same data. The API parses and
// re-serializes stored definitions, so equivalent documents can differ in formatting.


// yamlEquivalent reports whether two YAML documents carry the same data. The API parses and
// re-serializes stored definitions, so equivalent documents can differ in formatting.
func yamlEquivalent(a, b string) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this should go in helpers.go

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"

Needed for the Default added below.

Comment on lines +109 to +123
"etl_definition": schema.SingleNestedAttribute{
Description: "The ETL definitions used to import data from the integration. If not set, OpsLevel's default definitions are used. The API manages the two definitions as a unit, so both must be set together.",
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"extract_definition": schema.StringAttribute{
Description: "The YAML definition for extracting data from inbound payloads.",
Required: true,
},
"transform_definition": schema.StringAttribute{
Description: "The YAML definition for transforming extracted data to OpsLevel resources.",
Required: true,
},
},
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"etl_definition": schema.SingleNestedAttribute{
Description: "The ETL definitions used to import data from the integration. If not set, OpsLevel's default definitions are used. The API manages the two definitions as a unit, so both must be set together.",
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"extract_definition": schema.StringAttribute{
Description: "The YAML definition for extracting data from inbound payloads.",
Required: true,
},
"transform_definition": schema.StringAttribute{
Description: "The YAML definition for transforming extracted data to OpsLevel resources.",
Required: true,
},
},
},
"etl_definition": schema.SingleNestedAttribute{
Description: "The ETL definitions used to import data from the integration. If not set (or removed), OpsLevel's default definitions are used. The API manages the two definitions as a unit, so both must be set together.",
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"extract_definition": schema.StringAttribute{
Description: "The YAML definition for extracting data from inbound payloads.",
Required: true,
},
"transform_definition": schema.StringAttribute{
Description: "The YAML definition for transforming extracted data to OpsLevel resources.",
Required: true,
},
},
Default: objectdefault.StaticValue(types.ObjectValueMust(
integrationKubernetesEtlDefinitionAttrs(),
map[string]attr.Value{
"extract_definition": types.StringValue(""),
"transform_definition": types.StringValue(""),
},
)),
},

Without this, an omitted etl_definition resolves to Unknown on every plan (no default, no plan modifier), so it never converges.

Comment on lines +139 to +151
// newKubernetesIntegrationInput only sets the definition fields when the etl_definition object
// has a known value. Unknown values (unset in config) must be omitted so the API keeps its
// defaults - sending an explicit null would clear both definitions on the server.
func newKubernetesIntegrationInput(ctx context.Context, planModel IntegrationKubernetesResourceModel, diags *diag.Diagnostics) opslevel.KubernetesIntegrationInput {
input := opslevel.KubernetesIntegrationInput{
Name: nullable(planModel.Name.ValueStringPointer()),
}
if !planModel.EtlDefinition.IsNull() && !planModel.EtlDefinition.IsUnknown() {
var etlModel IntegrationKubernetesEtlDefinitionModel
diags.Append(planModel.EtlDefinition.As(ctx, &etlModel, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
input.ExtractDefinition = refOf(opslevel.YAML(etlModel.ExtractDefinition.ValueString()))
input.TransformDefinition = refOf(opslevel.YAML(etlModel.TransformDefinition.ValueString()))
}

@andrewstillv15 andrewstillv15 Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// newKubernetesIntegrationInput only sets the definition fields when the etl_definition object
// has a known value. Unknown values (unset in config) must be omitted so the API keeps its
// defaults - sending an explicit null would clear both definitions on the server.
func newKubernetesIntegrationInput(ctx context.Context, planModel IntegrationKubernetesResourceModel, diags *diag.Diagnostics) opslevel.KubernetesIntegrationInput {
input := opslevel.KubernetesIntegrationInput{
Name: nullable(planModel.Name.ValueStringPointer()),
}
if !planModel.EtlDefinition.IsNull() && !planModel.EtlDefinition.IsUnknown() {
var etlModel IntegrationKubernetesEtlDefinitionModel
diags.Append(planModel.EtlDefinition.As(ctx, &etlModel, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
input.ExtractDefinition = refOf(opslevel.YAML(etlModel.ExtractDefinition.ValueString()))
input.TransformDefinition = refOf(opslevel.YAML(etlModel.TransformDefinition.ValueString()))
}
func newKubernetesIntegrationInput(ctx context.Context, planModel IntegrationKubernetesResourceModel, diags *diag.Diagnostics) opslevel.KubernetesIntegrationInput {
input := opslevel.KubernetesIntegrationInput{
Name: nullable(planModel.Name.ValueStringPointer()),
}
if !planModel.EtlDefinition.IsNull() && !planModel.EtlDefinition.IsUnknown() {
var etlModel IntegrationKubernetesEtlDefinitionModel
diags.Append(planModel.EtlDefinition.As(ctx, &etlModel, basetypes.ObjectAsOptions{UnhandledNullAsEmpty: true, UnhandledUnknownAsEmpty: true})...)
if etlModel.ExtractDefinition.ValueString() != "" || etlModel.TransformDefinition.ValueString() != "" {
input.ExtractDefinition = refOf(opslevel.YAML(etlModel.ExtractDefinition.ValueString()))
input.TransformDefinition = refOf(opslevel.YAML(etlModel.TransformDefinition.ValueString()))
}
}

Sending extractDefinition: "" / transformDefinition: "" explicitly is rejected with a GraphQL coercion error (Could not coerce value "" to YAML), so always sending both fields can't work - omitting both together is the only way to trigger the server's reset-to-default behavior.

Review feedback: omit both fields when the sentinel default is planned
(explicit "" fails YAML coercion), keep the sentinel in state, and move
yamlEquivalent to helpers.go. Adds an unset run to the acceptance test.
@saditya370

saditya370 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

all six suggestions are in.
The import, the Default on etl_definition, and the omit-both-when-empty guard went in as you wrote them. yamlEquivalent now lives in helpers.go, and I dropped both doc comments.
One thing I added on top: the model now keeps the planned {"", ""} sentinel on refresh instead of adopting whatever defaults the server sends back. Without that the unset apply blows up with "Provider produced inconsistent result after apply", since the plan already promised empty strings. Happy to move that logic elsewhere if you had somewhere better in mind.

I also added an unset run to the acceptance test - set the definitions, null them out, set them again. Confirms the id stays put and both fields land on "". 3/3 green against a dev instance.

@saditya370
saditya370 merged commit 9ee585c into main Jul 23, 2026
7 checks passed
@saditya370
saditya370 deleted the add-integration-kubernetes branch July 23, 2026 17:20
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