Add opslevel_integration_kubernetes resource - #660
Conversation
Manages a Kubernetes integration's ETL config (extract_definition / transform_definition) as code. Requires opslevel-go v2026.7.21.
|
Can we update the opslevel-go submodule too? |
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.
|
Addressed all three comments in d09e7ad:
Re-ran the acceptance test against a dev instance: create with both definitions, in-place update, destroy — all green. |
|
Submodule bumped to v2026.7.21 as well. |
| // 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. |
There was a problem hiding this comment.
| // 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. |
| // 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. |
There was a problem hiding this comment.
| // 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 { |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
| "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.
| "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, | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
| "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.
| // 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())) | ||
| } |
There was a problem hiding this comment.
| // 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.
|
all six suggestions are in. 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. |
Add
opslevel_integration_kubernetesresourceLets users manage a Kubernetes integration's ETL config as code (GitLab issue 14148):
Bumps opslevel-go to v2026.7.21 for the new client methods.
Notes for reviewers
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 planwon'tcatch 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.
extractDefinition/transformDefinitionarguments sit behind thek8s_etl_flowfeature flag (default-on for new accounts); older accounts without it get a 402.
Testing
tests/remote/integration_kubernetes.tftest.hclagainst a dev instance: create with bothdefinitions, update both in place, destroy — all green.
go build, unit tests, golangci-lint, gofumpt,terraform fmtall clean; docs generatedwith tfplugindocs.