Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add timeout block for subaccount subscription #869

Merged
merged 5 commits into from
Jul 25, 2024
Merged
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
22 changes: 22 additions & 0 deletions docs/resources/subaccount_subscription.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,24 @@ You must be assigned to the admin role of the subaccount.
## Example Usage

```terraform
# create a subscription to workzone
resource "btp_subaccount_subscription" "workzone" {
subaccount_id = "6aa64c2f-38c1-49a9-b2e8-cf9fea769b7f"
app_name = "SAPLaunchpad"
plan_name = "free"
}


# create a subscription to workzone with a timeout
resource "btp_subaccount_subscription" "workzone" {
subaccount_id = "6aa64c2f-38c1-49a9-b2e8-cf9fea769b7f"
app_name = "SAPLaunchpad"
plan_name = "free"
timeouts = {
create = "25m"
delete = "15m"
}
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -38,6 +51,7 @@ resource "btp_subaccount_subscription" "workzone" {
### Optional

- `parameters` (String) The parameters of the subscription as a valid JSON object.
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

### Read-Only

Expand Down Expand Up @@ -65,6 +79,14 @@ resource "btp_subaccount_subscription" "workzone" {
- `supports_plan_updates` (Boolean) Specifies whether a consumer, whose subaccount is subscribed to the application, can change the subscription to a different plan that is available for this application and subaccount.
- `tenant_id` (String) The tenant ID of the application provider.

<a id="nestedatt--timeouts"></a>
### Nested Schema for `timeouts`

Optional:

- `create` (String) Timeout for creating the subscription.
- `delete` (String) Timeout for deleting the subscription.

## Import

Import is supported using the following syntax:
Expand Down
13 changes: 13 additions & 0 deletions examples/resources/btp_subaccount_subscription/resource.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# create a subscription to workzone
resource "btp_subaccount_subscription" "workzone" {
subaccount_id = "6aa64c2f-38c1-49a9-b2e8-cf9fea769b7f"
app_name = "SAPLaunchpad"
plan_name = "free"
}


# create a subscription to workzone with a timeout
resource "btp_subaccount_subscription" "workzone" {
subaccount_id = "6aa64c2f-38c1-49a9-b2e8-cf9fea769b7f"
app_name = "SAPLaunchpad"
plan_name = "free"
timeouts = {
create = "25m"
delete = "15m"
}
}
4 changes: 2 additions & 2 deletions internal/provider/datasource_subaccount_subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ You must be assigned to the admin or viewer role of the subaccount.`,
}

func (ds *subaccountSubscriptionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data subaccountSubscriptionType
var data subaccountSubscriptionDataSourceType

diags := req.Config.Get(ctx, &data)

Expand All @@ -182,7 +182,7 @@ func (ds *subaccountSubscriptionDataSource) Read(ctx context.Context, req dataso
return
}

data, diags = subaccountSubscriptionValueFrom(ctx, cliRes)
data, diags = subaccountSubscriptionDataSourceValueFrom(ctx, cliRes)
resp.Diagnostics.Append(diags...)

diags = resp.State.Set(ctx, &data)
Expand Down

Large diffs are not rendered by default.

232 changes: 116 additions & 116 deletions internal/provider/fixtures/resource_subaccount_subscription.yaml

Large diffs are not rendered by default.

1,185 changes: 1,185 additions & 0 deletions internal/provider/fixtures/resource_subaccount_subscription_with_timeouts.yaml

Large diffs are not rendered by default.

135 changes: 85 additions & 50 deletions internal/provider/resource_subaccount_subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
"errors"
"fmt"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
Expand Down Expand Up @@ -43,7 +44,7 @@ func (rs *subaccountSubscriptionResource) Configure(_ context.Context, req resou
rs.cli = req.ProviderData.(*btpcli.ClientFacade)
}

func (rs *subaccountSubscriptionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
func (rs *subaccountSubscriptionResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: `Subscribes a subaccount to a multitenant application.
Custom or partner-developed applications are currently not supported.
Expand Down Expand Up @@ -79,6 +80,13 @@ You must be assigned to the admin role of the subaccount.`,
jsonvalidator.ValidJSON(),
},
},
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
CreateDescription: "Timeout for creating the subscription.",
Update: false,
Delete: true,
DeleteDescription: "Timeout for deleting the subscription.",
}),
"additional_plan_features": schema.SetAttribute{
ElementType: types.StringType,
MarkdownDescription: "The list of features specific to this plan.",
Expand Down Expand Up @@ -192,13 +200,16 @@ func (rs *subaccountSubscriptionResource) Read(ctx context.Context, req resource
return
}

timeoutsLocal := state.Timeouts

cliRes, rawRes, err := rs.cli.Accounts.Subscription.Get(ctx, state.SubaccountId.ValueString(), state.AppName.ValueString(), state.PlanName.ValueString())
if err != nil {
handleReadErrors(ctx, rawRes, resp, err, "Resource Subscription (Subaccount)")
return
}

newState, diags := subaccountSubscriptionValueFrom(ctx, cliRes)
newState.Timeouts = timeoutsLocal

if newState.Parameters.IsNull() && !state.Parameters.IsNull() {
// The parameters are not returned by the API so we transfer the existing state to the read result if not existing
Expand Down Expand Up @@ -228,30 +239,8 @@ func (rs *subaccountSubscriptionResource) Create(ctx context.Context, req resour
return
}

timeout := 60 * time.Minute
delay, minTimeout := tfutils.CalculateDelayAndMinTimeOut(timeout)

createStateConf := &tfutils.StateChangeConf{
Pending: []string{saas_manager_service.StateInProcess},
Target: []string{saas_manager_service.StateSubscribed},
Refresh: func() (interface{}, string, error) {
subRes, _, err := rs.cli.Accounts.Subscription.Get(ctx, plan.SubaccountId.ValueString(), plan.AppName.ValueString(), plan.PlanName.ValueString())

if err != nil {
return subRes, "", err
}

// No error returned even is subscription failed
if subRes.State == saas_manager_service.StateSubscribeFailed {
return subRes, subRes.State, errors.New("undefined API error during subscription")
}

return subRes, subRes.State, nil
},
Timeout: timeout,
Delay: delay,
MinTimeout: minTimeout,
}
createStateConf, diags := rs.CreateStateChange(ctx, plan, "create")
resp.Diagnostics.Append(diags...)

updatedRes, err := createStateConf.WaitForStateContext(ctx)
if err != nil {
Expand All @@ -260,6 +249,7 @@ func (rs *subaccountSubscriptionResource) Create(ctx context.Context, req resour

updatedPlan, diags := subaccountSubscriptionValueFrom(ctx, updatedRes.(saas_manager_service.EntitledApplicationsResponseObject))
updatedPlan.Parameters = plan.Parameters
updatedPlan.Timeouts = plan.Timeouts
resp.Diagnostics.Append(diags...)

diags = resp.State.Set(ctx, &updatedPlan)
Expand Down Expand Up @@ -294,30 +284,8 @@ func (rs *subaccountSubscriptionResource) Delete(ctx context.Context, req resour
return
}

timeout := 60 * time.Minute
delay, minTimeout := tfutils.CalculateDelayAndMinTimeOut(timeout)

deleteStateConf := &tfutils.StateChangeConf{
Pending: []string{saas_manager_service.StateInProcess},
Target: []string{saas_manager_service.StateNotSubscribed},
Refresh: func() (interface{}, string, error) {
subRes, _, err := rs.cli.Accounts.Subscription.Get(ctx, state.SubaccountId.ValueString(), state.AppName.ValueString(), state.PlanName.ValueString())

if err != nil {
return subRes, subRes.State, err
}

// No error returned even is unsubscribe failed
if subRes.State == saas_manager_service.StateUnsubscribeFailed {
return subRes, subRes.State, errors.New("undefined API error during unsubscription")
}

return subRes, subRes.State, nil
},
Timeout: timeout,
Delay: delay,
MinTimeout: minTimeout,
}
deleteStateConf, diags := rs.DeleteStateChange(ctx, state, "delete")
resp.Diagnostics.Append(diags...)

_, err = deleteStateConf.WaitForStateContext(ctx)
if err != nil {
Expand All @@ -341,3 +309,70 @@ func (rs *subaccountSubscriptionResource) ImportState(ctx context.Context, req r
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("app_name"), idParts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("plan_name"), idParts[2])...)
}

func (rs *subaccountSubscriptionResource) CreateStateChange(ctx context.Context, plan subaccountSubscriptionType, operation string) (tfutils.StateChangeConf, diag.Diagnostics) {
var summary diag.Diagnostics

timeoutsLocal := plan.Timeouts

createTimeout, diags := timeoutsLocal.Update(ctx, tfutils.DefaultTimeout)
summary.Append(diags...)
delay, minTimeout := tfutils.CalculateDelayAndMinTimeOut(createTimeout)

return tfutils.StateChangeConf{
Pending: []string{saas_manager_service.StateInProcess},
Target: []string{saas_manager_service.StateSubscribed},
Refresh: func() (interface{}, string, error) {
subRes, _, err := rs.cli.Accounts.Subscription.Get(ctx, plan.SubaccountId.ValueString(), plan.AppName.ValueString(), plan.PlanName.ValueString())

if err != nil {
return subRes, "", err
}

// No error returned even is subscription failed
if subRes.State == saas_manager_service.StateSubscribeFailed {
return subRes, subRes.State, errors.New("undefined API error during subscription")
}

return subRes, subRes.State, nil
},
Timeout: createTimeout,
Delay: delay,
MinTimeout: minTimeout,
},
summary
}

func (rs *subaccountSubscriptionResource) DeleteStateChange(ctx context.Context, state subaccountSubscriptionType, operation string) (tfutils.StateChangeConf, diag.Diagnostics) {

var summary diag.Diagnostics

timeoutsLocal := state.Timeouts

deleteTimeout, diags := timeoutsLocal.Delete(ctx, tfutils.DefaultTimeout)
summary.Append(diags...)
delay, minTimeout := tfutils.CalculateDelayAndMinTimeOut(deleteTimeout)

return tfutils.StateChangeConf{
Pending: []string{saas_manager_service.StateInProcess},
Target: []string{saas_manager_service.StateNotSubscribed},
Refresh: func() (interface{}, string, error) {
subRes, _, err := rs.cli.Accounts.Subscription.Get(ctx, state.SubaccountId.ValueString(), state.AppName.ValueString(), state.PlanName.ValueString())

if err != nil {
return subRes, subRes.State, err
}

// No error returned even is unsubscribe failed
if subRes.State == saas_manager_service.StateUnsubscribeFailed {
return subRes, subRes.State, errors.New("undefined API error during unsubscription")
}

return subRes, subRes.State, nil
},
Timeout: deleteTimeout,
Delay: delay,
MinTimeout: minTimeout,
},
summary
}
50 changes: 50 additions & 0 deletions internal/provider/resource_subaccount_subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ func TestResourceSubaccountSubscription(t *testing.T) {
},
})
})

t.Run("happy path - simple subscription with timeouts", func(t *testing.T) {
rec, user := setupVCR(t, "fixtures/resource_subaccount_subscription_with_timeouts")
defer stopQuietly(rec)

resource.Test(t, resource.TestCase{
IsUnitTest: true,
ProtoV6ProviderFactories: getProviders(rec.GetDefaultClient()),
Steps: []resource.TestStep{
{
Config: hclProviderFor(user) + hclResourceSubaccountSubscriptionBySubaccountWithTimeout("uut", "integration-test-services-static", "auditlog-viewer", "free"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr("btp_subaccount_subscription.uut", "id", regexpValidUUID),
resource.TestMatchResourceAttr("btp_subaccount_subscription.uut", "subaccount_id", regexpValidUUID),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "app_name", "auditlog-viewer"),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "plan_name", "free"),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "app_id", "auditlog-viewer!t49"),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "state", "SUBSCRIBED"),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "quota", "1"),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "customer_developed", "false"),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "authentication_provider", "XSUAA"),
resource.TestMatchResourceAttr("btp_subaccount_subscription.uut", "created_date", regexpValidRFC3999Format),
resource.TestMatchResourceAttr("btp_subaccount_subscription.uut", "last_modified", regexpValidRFC3999Format),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "timeouts.create", "25m"),
resource.TestCheckResourceAttr("btp_subaccount_subscription.uut", "timeouts.delete", "15m"),
),
},
{
ResourceName: "btp_subaccount_subscription.uut",
ImportStateIdFunc: getSubscriptionImportStateId("btp_subaccount_subscription.uut", "auditlog-viewer", "free"),
ImportState: true,
ImportStateVerify: true,
},
},
})
})
t.Run("error path - subacount_id mandatory", func(t *testing.T) {
resource.Test(t, resource.TestCase{
IsUnitTest: true,
Expand Down Expand Up @@ -116,6 +152,20 @@ func hclResourceSubaccountSubscriptionBySubaccount(resourceName string, subaccou
}`, resourceName, subaccountName, appName, planName)
}

func hclResourceSubaccountSubscriptionBySubaccountWithTimeout(resourceName string, subaccountName string, appName string, planName string) string {
return fmt.Sprintf(`
data "btp_subaccounts" "all" {}
resource "btp_subaccount_subscription" "%s"{
subaccount_id = [for sa in data.btp_subaccounts.all.values : sa.id if sa.name == "%s"][0]
app_name = "%s"
plan_name = "%s"
timeouts = {
create = "25m"
delete = "15m"
}
}`, resourceName, subaccountName, appName, planName)
}

func hclResourceSubaccountSubscriptionNoSubaccountId(resourceName string, appName string, planName string) string {

return fmt.Sprintf(`
Expand Down
Loading