Skip to content
Open
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
3 changes: 3 additions & 0 deletions .changes/unreleased/Fixed-20260429-190243.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Fixed
body: Fixed `opslevel_service` dropping `preferred_api_document_source` after apply when `api_document_path` was not set, which caused a "Provider produced inconsistent result after apply" error
time: 2026-04-29T19:02:43.236755+05:30
60 changes: 22 additions & 38 deletions opslevel/resource_opslevel_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,11 @@ func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest
return
}

if planModel.ApiDocumentPath.ValueString() != "" {
apiDocPath := planModel.ApiDocumentPath.ValueString()
if planModel.PreferredApiDocumentSource.IsNull() {
if _, err := r.client.ServiceApiDocSettingsUpdate(string(service.Id), apiDocPath, nil); err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to set provided 'api_document_path' %s for service. error: %s", apiDocPath, err))
return
}
} else {
sourceEnum := opslevel.ApiDocumentSourceEnum(planModel.PreferredApiDocumentSource.ValueString())
if _, err := r.client.ServiceApiDocSettingsUpdate(string(service.Id), apiDocPath, &sourceEnum); err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to set provided 'api_document_path' %s with doc source '%s' for service. error: %s", apiDocPath, sourceEnum, err))
return
}
if !planModel.ApiDocumentPath.IsNull() || !planModel.PreferredApiDocumentSource.IsNull() {
apiDocPath, sourceEnum := serviceApiDocSettingsUpdateInput(planModel)
if _, err := r.client.ServiceApiDocSettingsUpdate(string(service.Id), apiDocPath, sourceEnum); err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to update API document settings for service %s. error: %s", service.Name, err))
return
}
}

Expand Down Expand Up @@ -477,33 +469,13 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest
}
}

if planModel.ApiDocumentPath.IsNull() {
if _, err := r.client.ServiceApiDocSettingsUpdate(string(service.Id), "", nil); err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to unset 'api_document_path' for service %s. error: %s", service.Name, err))
if !planModel.ApiDocumentPath.Equal(stateModel.ApiDocumentPath) ||
!planModel.PreferredApiDocumentSource.Equal(stateModel.PreferredApiDocumentSource) {
apiDocPath, sourceEnum := serviceApiDocSettingsUpdateInput(planModel)
if _, err := r.client.ServiceApiDocSettingsUpdate(string(service.Id), apiDocPath, sourceEnum); err != nil {
resp.Diagnostics.AddError("opslevel client error", fmt.Sprintf("Unable to update API document settings for service %s. error: %s", service.Name, err))
return
}
} else {
apiDocPath := planModel.ApiDocumentPath.ValueString()
if planModel.PreferredApiDocumentSource.IsNull() {
if _, err := r.client.ServiceApiDocSettingsUpdate(string(service.Id), apiDocPath, nil); err != nil {
resp.Diagnostics.AddError("opslevel client error",
fmt.Sprintf(
"Unable to set provided 'api_document_path' %s for service. error: %s",
apiDocPath, err),
)
return
}
} else {
sourceEnum := opslevel.ApiDocumentSourceEnum(planModel.PreferredApiDocumentSource.ValueString())
if _, err := r.client.ServiceApiDocSettingsUpdate(string(service.Id), apiDocPath, &sourceEnum); err != nil {
resp.Diagnostics.AddError("opslevel client error",
fmt.Sprintf(
"Unable to set provided 'api_document_path' %s with doc source '%s' for service. error: %s",
apiDocPath, sourceEnum, err),
)
return
}
}
}

// fetch the service again, since other mutations are performed after the create/update step
Expand Down Expand Up @@ -561,3 +533,15 @@ func updateServiceNote(client opslevel.Client, service opslevel.Service, planMod

return client.UpdateServiceNote(serviceNoteUpdateInput)
}

func serviceApiDocSettingsUpdateInput(plan ServiceResourceModel) (string, *opslevel.ApiDocumentSourceEnum) {
apiDocPath := ""
if !plan.ApiDocumentPath.IsNull() {
apiDocPath = plan.ApiDocumentPath.ValueString()
}
if plan.PreferredApiDocumentSource.IsNull() {
return apiDocPath, nil
}
sourceEnum := opslevel.ApiDocumentSourceEnum(plan.PreferredApiDocumentSource.ValueString())
return apiDocPath, &sourceEnum
}
83 changes: 83 additions & 0 deletions opslevel/resource_opslevel_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package opslevel

import (
"testing"

"github.com/hashicorp/terraform-plugin-framework/types"
opslevelgo "github.com/opslevel/opslevel-go/v2026"
)

func TestServiceApiDocSettingsUpdateInput(t *testing.T) {
testCases := []struct {
name string
plan ServiceResourceModel
expectedDocPath string
expectedDocSource *opslevelgo.ApiDocumentSourceEnum
}{
{
name: "neither field set",
plan: ServiceResourceModel{
ApiDocumentPath: types.StringNull(),
PreferredApiDocumentSource: types.StringNull(),
},
expectedDocPath: "",
},
{
name: "push source without path",
plan: ServiceResourceModel{
ApiDocumentPath: types.StringNull(),
PreferredApiDocumentSource: types.StringValue(string(opslevelgo.ApiDocumentSourceEnumPush)),
},
expectedDocPath: "",
expectedDocSource: &opslevelgo.ApiDocumentSourceEnumPush,
},
{
name: "pull source without path",
plan: ServiceResourceModel{
ApiDocumentPath: types.StringNull(),
PreferredApiDocumentSource: types.StringValue(string(opslevelgo.ApiDocumentSourceEnumPull)),
},
expectedDocPath: "",
expectedDocSource: &opslevelgo.ApiDocumentSourceEnumPull,
},
{
name: "path without source",
plan: ServiceResourceModel{
ApiDocumentPath: types.StringValue("openapi.yaml"),
PreferredApiDocumentSource: types.StringNull(),
},
expectedDocPath: "openapi.yaml",
},
{
name: "path with source",
plan: ServiceResourceModel{
ApiDocumentPath: types.StringValue("openapi.yaml"),
PreferredApiDocumentSource: types.StringValue(string(opslevelgo.ApiDocumentSourceEnumPush)),
},
expectedDocPath: "openapi.yaml",
expectedDocSource: &opslevelgo.ApiDocumentSourceEnumPush,
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
docPath, docSource := serviceApiDocSettingsUpdateInput(testCase.plan)

if docPath != testCase.expectedDocPath {
t.Fatalf("expected doc path %q, got %q", testCase.expectedDocPath, docPath)
}
if testCase.expectedDocSource == nil {
if docSource != nil {
t.Fatalf("expected nil doc source, got %q", *docSource)
}
return
}
if docSource == nil {
t.Fatalf("expected doc source %q, got nil", *testCase.expectedDocSource)
}
if *docSource != *testCase.expectedDocSource {
t.Fatalf("expected doc source %q, got %q", *testCase.expectedDocSource, *docSource)
}
})
}
}
23 changes: 23 additions & 0 deletions tests/service.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,29 @@ run "resource_service_create_with_empty_optional_fields" {

}

run "resource_service_create_with_preferred_api_document_source_without_path" {

variables {
name = "New ${var.name} with API doc source only"
preferred_api_document_source = "PUSH"
}

module {
source = "./opslevel_modules/modules/service"
}

assert {
condition = opslevel_service.this.api_document_path == null
error_message = var.error_expected_null_field
}

assert {
condition = opslevel_service.this.preferred_api_document_source == "PUSH"
error_message = "expected preferred_api_document_source to be persisted without api_document_path"
}

}

run "resource_service_update_unset_optional_fields" {

variables {
Expand Down
Loading