From 24ea16ef21065a723b3cccbf9ff19a97ba7c3d87 Mon Sep 17 00:00:00 2001 From: Weilue Luo Date: Mon, 2 Oct 2023 18:00:47 +0100 Subject: [PATCH] feat(LH-71074): SDC Onboarding Resource (#69) --- client/client.go | 17 ++ .../connector/connectoronboarding/create.go | 43 +++++ .../connectoronboarding/create_test.go | 104 +++++++++++ .../connector/connectoronboarding/delete.go | 23 +++ client/connector/connectoronboarding/read.go | 23 +++ client/connector/connectoronboarding/retry.go | 24 +++ .../connector/connectoronboarding/update.go | 23 +++ client/connector/read.go | 5 +- client/internal/url/url.go | 4 +- client/model/device/status/status.go | 14 ++ docs/resources/sdc_onboarding.md | 24 +++ .../connectoronboarding/operation.go | 48 ++++++ .../connector/connectoronboarding/resource.go | 162 ++++++++++++++++++ provider/internal/provider/provider.go | 2 + 14 files changed, 513 insertions(+), 3 deletions(-) create mode 100644 client/connector/connectoronboarding/create.go create mode 100644 client/connector/connectoronboarding/create_test.go create mode 100644 client/connector/connectoronboarding/delete.go create mode 100644 client/connector/connectoronboarding/read.go create mode 100644 client/connector/connectoronboarding/retry.go create mode 100644 client/connector/connectoronboarding/update.go create mode 100644 client/model/device/status/status.go create mode 100644 docs/resources/sdc_onboarding.md create mode 100644 provider/internal/connector/connectoronboarding/operation.go create mode 100644 provider/internal/connector/connectoronboarding/resource.go diff --git a/client/client.go b/client/client.go index 55acfb53..cd123730 100644 --- a/client/client.go +++ b/client/client.go @@ -4,6 +4,7 @@ package client import ( "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector/connectoronboarding" "net/http" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector" @@ -202,3 +203,19 @@ func (c *Client) ReadCloudFmcDevice(ctx context.Context) (*device.ReadOutput, er func (c *Client) ReadCloudFmcSpecificDevice(ctx context.Context, inp cloudfmc.ReadSpecificInput) (*cloudfmc.ReadSpecificOutput, error) { return cloudfmc.ReadSpecific(ctx, c.client, inp) } + +func (c *Client) CreateConnectorOnboarding(ctx context.Context, inp connectoronboarding.CreateInput) (*connectoronboarding.CreateOutput, error) { + return connectoronboarding.Create(ctx, c.client, inp) +} + +func (c *Client) UpdateConnectorOnboarding(ctx context.Context, inp connectoronboarding.UpdateInput) (*connectoronboarding.UpdateOutput, error) { + return connectoronboarding.Update(ctx, c.client, inp) +} + +func (c *Client) ReadConnectorOnboarding(ctx context.Context, inp connectoronboarding.ReadInput) (*connectoronboarding.ReadOutput, error) { + return connectoronboarding.Read(ctx, c.client, inp) +} + +func (c *Client) DeleteConnectorOnboarding(ctx context.Context, inp connectoronboarding.DeleteInput) (*connectoronboarding.DeleteOutput, error) { + return connectoronboarding.Delete(ctx, c.client, inp) +} diff --git a/client/connector/connectoronboarding/create.go b/client/connector/connectoronboarding/create.go new file mode 100644 index 00000000..4caa8154 --- /dev/null +++ b/client/connector/connectoronboarding/create.go @@ -0,0 +1,43 @@ +package connectoronboarding + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/retry" + "time" +) + +type CreateInput struct { + Name string +} + +func NewCreateInput(name string) CreateInput { + return CreateInput{ + Name: name, + } +} + +type CreateOutput = connector.ReadOutput + +func Create(ctx context.Context, client http.Client, createInp CreateInput) (*CreateOutput, error) { + + // wait for connector status to be "Active" + var readOutp connector.ReadOutput + err := retry.Do( + UntilConnectorStatusIsActive(ctx, client, *connector.NewReadByNameInput(createInp.Name), &readOutp), + retry.NewOptionsBuilder(). + Timeout(15*time.Minute). // usually takes ~3 minutes + Retries(-1). + Delay(2*time.Second). + Logger(client.Logger). + EarlyExitOnError(false). + Build(), + ) + + if err != nil { + return nil, err + } else { + return &readOutp, nil + } +} diff --git a/client/connector/connectoronboarding/create_test.go b/client/connector/connectoronboarding/create_test.go new file mode 100644 index 00000000..343e8796 --- /dev/null +++ b/client/connector/connectoronboarding/create_test.go @@ -0,0 +1,104 @@ +package connectoronboarding_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector/connectoronboarding" + internalHttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/device/status" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" +) + +func TestCreate(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + activeConnector := connector.ReadOutput{ + Uid: "", + Name: "test-sdc", + DefaultConnector: false, + Cdg: false, + TenantUid: "", + PublicKey: model.PublicKey{}, + ConnectorStatus: status.Active, + } + + onboardingConnector := connector.ReadOutput{ + Uid: "", + Name: "test-sdc", + DefaultConnector: false, + Cdg: false, + TenantUid: "", + PublicKey: model.PublicKey{}, + ConnectorStatus: status.Onboarding, + } + + baseUrl := "https://unittest.cdo.cisco.com" + + testCases := []struct { + testName string + input connectoronboarding.CreateInput + setupFunc func() + assertFunc func(output *connectoronboarding.CreateOutput, err error, t *testing.T) + }{ + { + testName: "should finish on connector Active status", + input: connectoronboarding.NewCreateInput(activeConnector.Name), + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadConnectorByName(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, []connector.ReadOutput{activeConnector}), + ) + }, + assertFunc: func(output *connectoronboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, output) + assert.Nil(t, err) + assert.Equal(t, *output, activeConnector) + }, + }, + { + testName: "should retry until SDC activation status is ACTIVE", + input: connectoronboarding.NewCreateInput(activeConnector.Name), + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadConnectorByName(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, []connector.ReadOutput{onboardingConnector}), + ) + httpmock.RegisterResponder( + http.MethodGet, + url.ReadConnectorByName(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, []connector.ReadOutput{activeConnector}), + ) + }, + assertFunc: func(output *connectoronboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, output) + assert.Nil(t, err) + assert.Equal(t, *output, activeConnector) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := connectoronboarding.Create( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + testCase.input, + ) + + testCase.assertFunc(output, err, t) + }) + } +} diff --git a/client/connector/connectoronboarding/delete.go b/client/connector/connectoronboarding/delete.go new file mode 100644 index 00000000..6b428dfd --- /dev/null +++ b/client/connector/connectoronboarding/delete.go @@ -0,0 +1,23 @@ +package connectoronboarding + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" +) + +type DeleteInput struct { +} + +func NewDeleteInput() DeleteInput { + return DeleteInput{} +} + +type DeleteOutput struct { +} + +func Delete(ctx context.Context, client http.Client, deleteInp DeleteInput) (*DeleteOutput, error) { + + // empty + + return nil, nil +} diff --git a/client/connector/connectoronboarding/read.go b/client/connector/connectoronboarding/read.go new file mode 100644 index 00000000..5b78f02e --- /dev/null +++ b/client/connector/connectoronboarding/read.go @@ -0,0 +1,23 @@ +package connectoronboarding + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" +) + +type ReadInput struct { +} + +func NewReadInput() ReadInput { + return ReadInput{} +} + +type ReadOutput struct { +} + +func Read(ctx context.Context, client http.Client, readInp ReadInput) (*ReadOutput, error) { + + // empty + + return nil, nil +} diff --git a/client/connector/connectoronboarding/retry.go b/client/connector/connectoronboarding/retry.go new file mode 100644 index 00000000..bdba93f5 --- /dev/null +++ b/client/connector/connectoronboarding/retry.go @@ -0,0 +1,24 @@ +package connectoronboarding + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/retry" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/device/status" +) + +func UntilConnectorStatusIsActive(ctx context.Context, client http.Client, readInp connector.ReadByNameInput, readOutp *connector.ReadOutput) retry.Func { + return func() (bool, error) { + readRes, err := connector.ReadByName(ctx, client, readInp) + *readOutp = *readRes + if err != nil { + return false, err + } + client.Logger.Printf("connector status: %v\n", readRes.ConnectorStatus) + if readRes.ConnectorStatus == status.Active { + return true, nil + } + return false, nil + } +} diff --git a/client/connector/connectoronboarding/update.go b/client/connector/connectoronboarding/update.go new file mode 100644 index 00000000..6dae4c82 --- /dev/null +++ b/client/connector/connectoronboarding/update.go @@ -0,0 +1,23 @@ +package connectoronboarding + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" +) + +type UpdateInput struct { +} + +func NewUpdateInput() UpdateInput { + return UpdateInput{} +} + +type UpdateOutput struct { +} + +func Update(ctx context.Context, client http.Client, updateInp UpdateInput) (*UpdateOutput, error) { + + // empty + + return nil, nil +} diff --git a/client/connector/read.go b/client/connector/read.go index b378c3ed..a90b3a48 100644 --- a/client/connector/read.go +++ b/client/connector/read.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/device/status" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" @@ -24,6 +25,7 @@ type ReadOutput struct { Cdg bool `json:"cdg"` TenantUid string `json:"tenantUid"` PublicKey model.PublicKey `json:"larPublicKey"` + ConnectorStatus status.Type `json:"larStatus"` } func NewReadByUidInput(connectorUid string) *ReadByUidInput { @@ -49,9 +51,10 @@ func newReadByUidRequest(ctx context.Context, client http.Client, readInp ReadBy func newReadByNameRequest(ctx context.Context, client http.Client, readInp ReadByNameInput) *http.Request { - url := url.ReadConnectorByName(client.BaseUrl(), readInp.ConnectorName) + url := url.ReadConnectorByName(client.BaseUrl()) req := client.NewGet(ctx, url) + req.QueryParams.Add("q", fmt.Sprintf("name:%s", readInp.ConnectorName)) return req } diff --git a/client/internal/url/url.go b/client/internal/url/url.go index 741fb451..777bb8c9 100644 --- a/client/internal/url/url.go +++ b/client/internal/url/url.go @@ -50,8 +50,8 @@ func ReadConnectorByUid(baseUrl string, connectorUid string) string { return fmt.Sprintf("%s/aegis/rest/v1/services/targets/proxies/%s", baseUrl, connectorUid) } -func ReadConnectorByName(baseUrl string, connectorName string) string { - return fmt.Sprintf("%s/aegis/rest/v1/services/targets/proxies?q=name:%s", baseUrl, connectorName) +func ReadConnectorByName(baseUrl string) string { + return fmt.Sprintf("%s/aegis/rest/v1/services/targets/proxies", baseUrl) } func CreateConnector(baseUrl string) string { diff --git a/client/model/device/status/status.go b/client/model/device/status/status.go new file mode 100644 index 00000000..021c0cc6 --- /dev/null +++ b/client/model/device/status/status.go @@ -0,0 +1,14 @@ +package status + +type Type string + +// see also: https://github.com/cisco-lockhart/lh-plugin-core/blob/master/lh-target/src/main/java/com/cisco/lockhart/target/data/ServiceActivationState.java +const ( + New Type = "NEW" + ReOnboard Type = "REONBOARD" + YoReOnboard Type = "YO_REONBOARD" + Onboarding Type = "ONBOARDING" + Active Type = "ACTIVE" + Inactive Type = "INACTIVE" + Disabled Type = "DISABLE" +) diff --git a/docs/resources/sdc_onboarding.md b/docs/resources/sdc_onboarding.md new file mode 100644 index 00000000..e24bae58 --- /dev/null +++ b/docs/resources/sdc_onboarding.md @@ -0,0 +1,24 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdo_sdc_onboarding Resource - cdo" +subcategory: "" +description: |- + Use this resource to wait for an SDC to finish onboarding. When an SDC is onboarded, either manually or using the CDO Terraform Modules for AWS https://github.com/CiscoDevNet/terraform-aws-cdo-sdc and vSphere https://github.com/CiscoDevNet/terraform-vsphere-cdo-sdc, it can take a few minutes before the SDC is active and capable of proxying communications between CDO and the device. This resource allows you to wait until this is done. +--- + +# cdo_sdc_onboarding (Resource) + +Use this resource to wait for an SDC to finish onboarding. When an SDC is onboarded, either manually or using the CDO Terraform Modules for [AWS](https://github.com/CiscoDevNet/terraform-aws-cdo-sdc) and [vSphere](https://github.com/CiscoDevNet/terraform-vsphere-cdo-sdc), it can take a few minutes before the SDC is active and capable of proxying communications between CDO and the device. This resource allows you to wait until this is done. + + + + +## Schema + +### Required + +- `name` (String) Specify the name of the SDC. + +### Read-Only + +- `id` (String) The unique identifier of this SDC onboarding resource. diff --git a/provider/internal/connector/connectoronboarding/operation.go b/provider/internal/connector/connectoronboarding/operation.go new file mode 100644 index 00000000..c2c44262 --- /dev/null +++ b/provider/internal/connector/connectoronboarding/operation.go @@ -0,0 +1,48 @@ +package connectoronboarding + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector/connectoronboarding" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func Read(ctx context.Context, resource *Resource, stateData *ResourceModel) error { + + // do read + + // map return struct to sdc model + + return nil +} + +func Create(ctx context.Context, resource *Resource, planData *ResourceModel) error { + + // do create + + readOutp, err := resource.client.CreateConnectorOnboarding(ctx, connectoronboarding.NewCreateInput(planData.Name.ValueString())) + if err != nil { + return err + } + + // map return struct to sdc model + planData.Id = types.StringValue(readOutp.Uid) + planData.Name = types.StringValue(readOutp.Name) + + return nil +} + +func Update(ctx context.Context, resource *Resource, planData *ResourceModel, stateData *ResourceModel) error { + + // do update + + // map return struct to sdc model + + return nil +} + +func Delete(ctx context.Context, resource *Resource, stateData *ResourceModel) error { + + // do delete + + return nil +} diff --git a/provider/internal/connector/connectoronboarding/resource.go b/provider/internal/connector/connectoronboarding/resource.go new file mode 100644 index 00000000..9d0c178c --- /dev/null +++ b/provider/internal/connector/connectoronboarding/resource.go @@ -0,0 +1,162 @@ +package connectoronboarding + +import ( + "context" + "fmt" + + cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client" + "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" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var _ resource.Resource = &Resource{} +var _ resource.ResourceWithImportState = &Resource{} + +func NewResource() resource.Resource { + return &Resource{} +} + +type Resource struct { + client *cdoClient.Client +} + +type ResourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} + +func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sdc_onboarding" +} + +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Use this resource to wait for an SDC to finish onboarding. When an SDC is onboarded, either manually or using the CDO Terraform Modules for [AWS](https://github.com/CiscoDevNet/terraform-aws-cdo-sdc) and [vSphere](https://github.com/CiscoDevNet/terraform-vsphere-cdo-sdc), it can take a few minutes before the SDC is active and capable of proxying communications between CDO and the device. This resource allows you to wait until this is done.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of this SDC onboarding resource.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Specify the name of the SDC.", + Required: true, + }, + }, + } +} + +func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*cdoClient.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *cdoClient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Trace(ctx, "read SDC resource") + + // 1. read terraform plan data into the model + var stateData ResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...) + if resp.Diagnostics.HasError() { + return + } + + // 2. do read + if err := Read(ctx, r, &stateData); err != nil { + resp.Diagnostics.AddError("failed to read SDC resource", err.Error()) + return + } + + // 3. save data into terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &stateData)...) + tflog.Trace(ctx, "read SDC resource done") +} + +func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, res *resource.CreateResponse) { + tflog.Trace(ctx, "create SDC resource") + + // 1. read terraform plan data into model + var planData ResourceModel + res.Diagnostics.Append(req.Plan.Get(ctx, &planData)...) + if res.Diagnostics.HasError() { + return + } + + // 2. create resource & fill model data + if err := Create(ctx, r, &planData); err != nil { + res.Diagnostics.AddError("failed to create SDC resource", err.Error()) + return + } + + // 3. fill terraform state using model data + res.Diagnostics.Append(res.State.Set(ctx, &planData)...) + tflog.Trace(ctx, "create SDC resource done") +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { + tflog.Trace(ctx, "update SDC resource") + + // 1. read plan and state data from terraform + var planData ResourceModel + res.Diagnostics.Append(req.Plan.Get(ctx, &planData)...) + if res.Diagnostics.HasError() { + return + } + var stateData ResourceModel + res.Diagnostics.Append(req.State.Get(ctx, &stateData)...) + if res.Diagnostics.HasError() { + return + } + + // 2. update resource & state data + if err := Update(ctx, r, &planData, &stateData); err != nil { + res.Diagnostics.AddError("failed to update SDC resource", err.Error()) + return + } + + // 3. update terraform state with updated state data + res.Diagnostics.Append(res.State.Set(ctx, &stateData)...) + tflog.Trace(ctx, "update SDC resource done") +} + +func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { + tflog.Trace(ctx, "delete SDC resource") + + // 1. read state data from terraform state + var stateData ResourceModel + res.Diagnostics.Append(req.State.Get(ctx, &stateData)...) + if res.Diagnostics.HasError() { + return + } + + // 2. delete the resource + if err := Delete(ctx, r, &stateData); err != nil { + res.Diagnostics.AddError("failed to delete SDC resource", err.Error()) + } +} + +func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, res *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, res) +} diff --git a/provider/internal/provider/provider.go b/provider/internal/provider/provider.go index 9e4cfd74..4c8e731f 100644 --- a/provider/internal/provider/provider.go +++ b/provider/internal/provider/provider.go @@ -6,6 +6,7 @@ package provider import ( "context" "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/internal/connector/connectoronboarding" "os" "github.com/CiscoDevnet/terraform-provider-cdo/internal/cdfmc" @@ -160,6 +161,7 @@ func (p *CdoProvider) Resources(ctx context.Context) []func() resource.Resource user.NewResource, user_api_token.NewResource, ftdonboarding.NewResource, + connectoronboarding.NewResource, } }