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
91 changes: 91 additions & 0 deletions .github/docs/contribution-guide/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package foo

import (
"fmt"
"net/http"
"regexp"
"testing"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
)

func TestFooSavesIDsOnError(t *testing.T) {
/* Setup code:
- define known values for attributes used in id
- create mock server
- define minimal tf config with custom endpoint pointing to mock server
*/
var (
projectId = uuid.NewString()
barId = uuid.NewString()
)
const region = "eu01"
s := testutil.NewMockServer(t)
defer s.Server.Close()
tfConfig := fmt.Sprintf(`
provider "stackit" {
foo_custom_endpoint = "%s"
service_account_token = "mock-server-needs-no-auth"
}

resource "stackit_foo" "foo" {
project_id = "%s"
}
`, s.Server.URL, projectId)

/* Test steps:
1. Create resource with mocked backend
2. Verify with a refresh, that IDs are saved to state
*/
resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
/* Setup mock responses for create and waiter.
The create response succeeds and returns the barId, but the waiter fails with an error.
We can't check the state in this step, because the create returns early due to the waiter error.
TF won't execute any Checks of the TestStep if there is an error.
*/
s.Reset(
testutil.MockResponse{
Description: "create foo",
ToJsonBody: &BarResponse{
BarId: barId,
},
},
testutil.MockResponse{Description: "failing waiter", StatusCode: http.StatusInternalServerError},
)
},
Config: tfConfig,
ExpectError: regexp.MustCompile("Error creating foo.*"),
},
{
PreConfig: func() {
/* Setup mock responses for refresh and delete.
The refresh response fails with an error, but we want to verify that the URL contains the correct IDs.
After the test TF will automatically destroy the resource. So we set up mocks to simulate a successful dlete.
*/
s.Reset(
testutil.MockResponse{
Description: "refresh",
Handler: func(w http.ResponseWriter, req *http.Request) {
expected := fmt.Sprintf("/v1/projects/%s/regions/%s/foo/%s", projectId, region, barId)
if req.URL.Path != expected {
t.Errorf("unexpected URL path: got %s, want %s", req.URL.Path, expected)
}
w.WriteHeader(http.StatusInternalServerError)
},
},
testutil.MockResponse{Description: "delete"},
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusGone},
)
},
RefreshState: true,
ExpectError: regexp.MustCompile("Error reading foo.*"),
},
},
})
}
17 changes: 16 additions & 1 deletion CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,22 @@ If you want to onboard resources of a STACKIT service `foo` that was not yet in
```
4. Create a utils package, for service `foo` it would be `stackit/internal/foo/utils`. Add a `ConfigureClient()` func and use it in your resource and datasource implementations.

https://github.com/stackitcloud/terraform-provider-stackit/blob/main/.github/docs/contribution-guide/utils/util.go
https://github.com/stackitcloud/terraform-provider-stackit/blob/main/.github/docs/contribution-guide/utils/util.go

5. If the service `foo` uses async creation (you have to use a waiter after creating the resource), we want to save the
IDs as soon as possible to the state. Should the waiter time out, we'll still have the IDs in the state and allow
further usage of that resource.

https://github.com/stackitcloud/terraform-provider-stackit/blob/main/.github/docs/contribution-guide/resource.go

The example in the contribution-guide linked above, does this by calling `utils.SetAndLogStateFields` before calling
the waiter.

To test this we use terraforms acceptance tests in unit test mode. These tests are named `Test<RESOURCE>SavesIDsOnError`.
You can find an annotated example of such tests in:

https://github.com/stackitcloud/terraform-provider-stackit/blob/main/.github/docs/contribution-guide/resource.go


### Local development

Expand Down
15 changes: 10 additions & 5 deletions stackit/internal/services/opensearch/credential/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"

"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"
Expand Down Expand Up @@ -190,7 +189,11 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ
return
}
credentialId := *credentialsResp.Id
ctx = tflog.SetField(ctx, "credential_id", credentialId)
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": projectId,
"instance_id": instanceId,
"credential_id": credentialId,
})

waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx)
if err != nil {
Expand Down Expand Up @@ -311,9 +314,11 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...)
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"instance_id": idParts[1],
"credential_id": idParts[2],
})
tflog.Info(ctx, "OpenSearch credential state imported")
}

Expand Down
18 changes: 13 additions & 5 deletions stackit/internal/services/opensearch/instance/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"

"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"
Expand Down Expand Up @@ -366,9 +365,16 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
}

ctx = core.LogResponse(ctx)

if createResp.InstanceId == nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "API response did not include instance ID")
}
instanceId := *createResp.InstanceId
ctx = tflog.SetField(ctx, "instance_id", instanceId)

ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": projectId,
"instance_id": instanceId,
})

waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err))
Expand Down Expand Up @@ -557,8 +563,10 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...)
ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"instance_id": idParts[1],
})
tflog.Info(ctx, "OpenSearch instance state imported")
}

Expand Down
161 changes: 161 additions & 0 deletions stackit/internal/services/opensearch/opensearch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package opensearch

import (
"fmt"
"net/http"
"regexp"
"testing"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/opensearch"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
)

func TestOpensearchInstanceSavesIDsOnError(t *testing.T) {
var (
projectId = uuid.NewString()
instanceId = uuid.NewString()
)
const (
name = "opensearch-instance-test"
version = "version"
planName = "plan-name"
)
s := testutil.NewMockServer(t)
defer s.Server.Close()
tfConfig := fmt.Sprintf(`
provider "stackit" {
opensearch_custom_endpoint = "%s"
service_account_token = "mock-server-needs-no-auth"
}

resource "stackit_opensearch_instance" "instance" {
project_id = "%s"
name = "%s"
version = "%s"
plan_name = "%s"
}
`, s.Server.URL, projectId, name, version, planName)

resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
s.Reset(
testutil.MockResponse{
Description: "offerings",
ToJsonBody: &opensearch.ListOfferingsResponse{
Offerings: &[]opensearch.Offering{
{
Name: utils.Ptr("offering-name"),
Version: utils.Ptr(version),
Plans: &[]opensearch.Plan{
{
Id: utils.Ptr("plan-id"),
Name: utils.Ptr(planName),
},
},
},
},
},
},
testutil.MockResponse{
Description: "create instance",
ToJsonBody: &opensearch.CreateInstanceResponse{
InstanceId: utils.Ptr(instanceId),
},
},
testutil.MockResponse{Description: "failing waiter", StatusCode: http.StatusInternalServerError},
)
},
Config: tfConfig,
ExpectError: regexp.MustCompile("Error creating instance.*"),
},
{
PreConfig: func() {
s.Reset(
testutil.MockResponse{
Description: "refresh",
Handler: func(w http.ResponseWriter, req *http.Request) {
expected := fmt.Sprintf("/v1/projects/%s/instances/%s", projectId, instanceId)
if req.URL.Path != expected {
t.Errorf(fmt.Sprintf("unexpected URL path: got %s, want %s", req.URL.Path, expected), http.StatusBadRequest)
}
w.WriteHeader(http.StatusInternalServerError)
},
},
testutil.MockResponse{Description: "delete"},
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusGone},
)
},
RefreshState: true,
ExpectError: regexp.MustCompile("Error reading instance.*"),
},
},
})
}

func TestOpensearchCredentialSavesIDsOnError(t *testing.T) {
var (
projectId = uuid.NewString()
instanceId = uuid.NewString()
credentialId = uuid.NewString()
)
s := testutil.NewMockServer(t)
defer s.Server.Close()
tfConfig := fmt.Sprintf(`
provider "stackit" {
opensearch_custom_endpoint = "%s"
service_account_token = "mock-server-needs-no-auth"
}

resource "stackit_opensearch_credential" "credential" {
project_id = "%s"
instance_id = "%s"
}
`, s.Server.URL, projectId, instanceId)

resource.UnitTest(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
PreConfig: func() {
s.Reset(
testutil.MockResponse{
Description: "create credential",
ToJsonBody: &opensearch.CredentialsResponse{
Id: utils.Ptr(credentialId),
},
},
testutil.MockResponse{Description: "create waiter", StatusCode: http.StatusInternalServerError},
)
},
Config: tfConfig,
ExpectError: regexp.MustCompile("Error creating credential.*"),
},
{
PreConfig: func() {
s.Reset(
testutil.MockResponse{
Description: "refresh",
Handler: func(w http.ResponseWriter, req *http.Request) {
expected := fmt.Sprintf("/v1/projects/%s/instances/%s/credentials/%s", projectId, instanceId, credentialId)
if req.URL.Path != expected {
t.Errorf(fmt.Sprintf("unexpected URL path: got %s, want %s", req.URL.Path, expected), http.StatusBadRequest)
}
w.WriteHeader(http.StatusInternalServerError)
},
},
testutil.MockResponse{Description: "delete"},
testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusGone},
)
},
RefreshState: true,
ExpectError: regexp.MustCompile("Error reading credential.*"),
},
},
})
}
Loading
Loading