diff --git a/client/client.go b/client/client.go index b4c68f77..b14b58e4 100644 --- a/client/client.go +++ b/client/client.go @@ -6,6 +6,7 @@ import ( "context" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/connector" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/asa/asaconfig" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/genericssh" "net/http" @@ -120,3 +121,23 @@ func (c *Client) UpdateGenericSSH(ctx context.Context, inp genericssh.UpdateInpu func (c *Client) DeleteGenericSSH(ctx context.Context, inp genericssh.DeleteInput) (*genericssh.DeleteOutput, error) { return genericssh.Delete(ctx, c.client, inp) } + +func (c *Client) ReadCloudFtdByUid(ctx context.Context, inp cloudftd.ReadByUidInput) (*cloudftd.ReadOutput, error) { + return cloudftd.ReadByUid(ctx, c.client, inp) +} + +func (c *Client) ReadCloudFtdByName(ctx context.Context, inp cloudftd.ReadByNameInput) (*cloudftd.ReadOutput, error) { + return cloudftd.ReadByName(ctx, c.client, inp) +} + +func (c *Client) CreateCloudFtd(ctx context.Context, inp cloudftd.CreateInput) (*cloudftd.CreateOutput, error) { + return cloudftd.Create(ctx, c.client, inp) +} + +func (c *Client) UpdateCloudFtd(ctx context.Context, inp cloudftd.UpdateInput) (*cloudftd.UpdateOutput, error) { + return cloudftd.Update(ctx, c.client, inp) +} + +func (c *Client) DeleteCloudFtd(ctx context.Context, inp cloudftd.DeleteInput) (*cloudftd.DeleteOutput, error) { + return cloudftd.Delete(ctx, c.client, inp) +} diff --git a/client/device/cdfmc/fixture_test.go b/client/device/cloudfmc/fixture_test.go similarity index 58% rename from client/device/cdfmc/fixture_test.go rename to client/device/cloudfmc/fixture_test.go index 65905f51..d2f1f24f 100644 --- a/client/device/cdfmc/fixture_test.go +++ b/client/device/cloudfmc/fixture_test.go @@ -1,4 +1,6 @@ -package cdfmc_test +package cloudfmc_test + +import "time" const ( smartLicenseEvalExpiresInDays = 123456 @@ -14,9 +16,21 @@ const ( smartLicenseLimit = 123456 smartLicensePages = 123456 - baseUrl = "https://unit-test.net" - domainUid = "unit-test-domain-uid" - limit = 123456 + baseUrl = "https://unit-test.net" + + fmcHostname = "https://fmc-hostname.unit-test.net" + fmcUid = "unit-test-fmc-uid" + domainUid = "unit-test-domain-uid" + limit = 123456 + status = "unit-test-status" + + deviceName = "unit-test-device-name" + deviceUid = "unit-test-uid" + deviceHost = "https://unit-test.com" + devicePort = 1234 + deviceCloudConnectorUId = "unit-test-uid" + + specificDeviceUid = "unit-test-specific-device-uid" accessPolicySelfLink = "https://unit-test.cdo.cisco.com/api/fmc_config/v1/domain/unit-test-domain-uid/policy/accesspolicies/unit-test-uid" accessPolicyName = "Unit Test Access Control Policy" @@ -27,3 +41,8 @@ const ( accessPolicyLimit = 123456 accessPolicyPages = 123456 ) + +var ( + deviceCreatedDate = time.Date(1999, 1, 1, 0, 0, 0, 0, time.Local) + deviceLastUpdatedDate = time.Date(1999, 1, 1, 0, 0, 0, 0, time.Local) +) diff --git a/client/device/cloudfmc/fmcappliance/fixture_test.go b/client/device/cloudfmc/fmcappliance/fixture_test.go new file mode 100644 index 00000000..24342005 --- /dev/null +++ b/client/device/cloudfmc/fmcappliance/fixture_test.go @@ -0,0 +1,16 @@ +package fmcappliance_test + +const ( + baseUrl = "https://unit-test.net" + FmcSpecificUid = "unit-test-cloudfmc-specific-uid" + queueTriggerState = "unit-test-queue-trigger-state" + uid = "unit-test-uid" + state = "unit-test-state" + domainUid = "unit-test-domainUid" +) + +var ( + stateMachineContext = map[string]string{ + "unit-test-sm-context-key": "unit-test-sm-context-value", + } +) diff --git a/client/device/cloudfmc/fmcappliance/update.go b/client/device/cloudfmc/fmcappliance/update.go new file mode 100644 index 00000000..a524a229 --- /dev/null +++ b/client/device/cloudfmc/fmcappliance/update.go @@ -0,0 +1,47 @@ +package fmcappliance + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" +) + +type UpdateInput struct { + FmcSpecificUid string + QueueTriggerState string + StateMachineContext map[string]string +} + +func NewUpdateInput(FmcSpecificUid, queueTriggerState string, stateMachineContext map[string]string) UpdateInput { + return UpdateInput{ + FmcSpecificUid: FmcSpecificUid, + QueueTriggerState: queueTriggerState, + StateMachineContext: stateMachineContext, + } +} + +type UpdateOutput struct { + Uid string `json:"uid"` + State string `json:"state"` + DomainUid string `json:"domainUid"` +} + +type updateRequestBody struct { + QueueTriggerState string `json:"queueTriggerState"` + StateMachineContext map[string]string `json:"stateMachineContext"` +} + +func Update(ctx context.Context, client http.Client, updateInp UpdateInput) (*UpdateOutput, error) { + updateUrl := url.UpdateFmcAppliance(client.BaseUrl(), updateInp.FmcSpecificUid) + updateBody := newUpdateRequestBodyBuilder(). + QueueTriggerState(updateInp.QueueTriggerState). + StateMachineContext(updateInp.StateMachineContext). + Build() + req := client.NewPut(ctx, updateUrl, updateBody) + var updateOup UpdateOutput + if err := req.Send(&updateOup); err != nil { + return nil, err + } + + return &updateOup, nil +} diff --git a/client/device/cloudfmc/fmcappliance/update_inputbuilder.go b/client/device/cloudfmc/fmcappliance/update_inputbuilder.go new file mode 100644 index 00000000..723614e6 --- /dev/null +++ b/client/device/cloudfmc/fmcappliance/update_inputbuilder.go @@ -0,0 +1,30 @@ +package fmcappliance + +type UpdateInputBuilder struct { + updateInput *UpdateInput +} + +func NewUpdateInputBuilder() *UpdateInputBuilder { + updateInput := &UpdateInput{} + b := &UpdateInputBuilder{updateInput: updateInput} + return b +} + +func (b *UpdateInputBuilder) FmcSpecificUid(fmcSpecificUid string) *UpdateInputBuilder { + b.updateInput.FmcSpecificUid = fmcSpecificUid + return b +} + +func (b *UpdateInputBuilder) QueueTriggerState(queueTriggerState string) *UpdateInputBuilder { + b.updateInput.QueueTriggerState = queueTriggerState + return b +} + +func (b *UpdateInputBuilder) StateMachineContext(stateMachineContext map[string]string) *UpdateInputBuilder { + b.updateInput.StateMachineContext = stateMachineContext + return b +} + +func (b *UpdateInputBuilder) Build() UpdateInput { + return *b.updateInput +} diff --git a/client/device/cloudfmc/fmcappliance/update_outputbuilder.go b/client/device/cloudfmc/fmcappliance/update_outputbuilder.go new file mode 100644 index 00000000..455a473e --- /dev/null +++ b/client/device/cloudfmc/fmcappliance/update_outputbuilder.go @@ -0,0 +1,54 @@ +package fmcappliance + +type UpdateOutputBuilder struct { + updateOutput *UpdateOutput +} + +func NewUpdateOutputBuilder() *UpdateOutputBuilder { + updateOutput := &UpdateOutput{} + b := &UpdateOutputBuilder{updateOutput: updateOutput} + return b +} + +func (b *UpdateOutputBuilder) Uid(uid string) *UpdateOutputBuilder { + b.updateOutput.Uid = uid + return b +} + +func (b *UpdateOutputBuilder) State(state string) *UpdateOutputBuilder { + b.updateOutput.State = state + return b +} + +func (b *UpdateOutputBuilder) DomainUid(domainUid string) *UpdateOutputBuilder { + b.updateOutput.DomainUid = domainUid + return b +} + +func (b *UpdateOutputBuilder) Build() UpdateOutput { + return *b.updateOutput +} + +type updateRequestBodyBuilder struct { + updateRequestBody *updateRequestBody +} + +func newUpdateRequestBodyBuilder() *updateRequestBodyBuilder { + updateRequestBody := &updateRequestBody{} + b := &updateRequestBodyBuilder{updateRequestBody: updateRequestBody} + return b +} + +func (b *updateRequestBodyBuilder) QueueTriggerState(queueTriggerState string) *updateRequestBodyBuilder { + b.updateRequestBody.QueueTriggerState = queueTriggerState + return b +} + +func (b *updateRequestBodyBuilder) StateMachineContext(stateMachineContext map[string]string) *updateRequestBodyBuilder { + b.updateRequestBody.StateMachineContext = stateMachineContext + return b +} + +func (b *updateRequestBodyBuilder) Build() updateRequestBody { + return *b.updateRequestBody +} diff --git a/client/device/cloudfmc/fmcappliance/update_test.go b/client/device/cloudfmc/fmcappliance/update_test.go new file mode 100644 index 00000000..420fc67c --- /dev/null +++ b/client/device/cloudfmc/fmcappliance/update_test.go @@ -0,0 +1,85 @@ +package fmcappliance_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc/fmcappliance" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" + + internalHttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/jarcoal/httpmock" +) + +func TestUpdate(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + validUpdateOutput := fmcappliance.NewUpdateOutputBuilder(). + Uid(uid). + State(state). + DomainUid(domainUid). + Build() + + testCases := []struct { + testName string + input fmcappliance.UpdateInput + setupFunc func() + assertFunc func(input fmcappliance.UpdateInput, output *fmcappliance.UpdateOutput, err error, t *testing.T) + }{ + { + testName: "successfully updates FMC Appliance name", + input: fmcappliance.NewUpdateInput(FmcSpecificUid, queueTriggerState, stateMachineContext), + + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateFmcAppliance(baseUrl, FmcSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validUpdateOutput), + ) + }, + + assertFunc: func(input fmcappliance.UpdateInput, output *fmcappliance.UpdateOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validUpdateOutput, *output) + }, + }, + + { + testName: "error when update FMC Appliance name error", + input: fmcappliance.NewUpdateInput(FmcSpecificUid, queueTriggerState, stateMachineContext), + + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateFmcAppliance(baseUrl, FmcSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + }, + + assertFunc: func(input fmcappliance.UpdateInput, output *fmcappliance.UpdateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := fmcappliance.Update( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + testCase.input, + ) + + testCase.assertFunc(testCase.input, output, err, t) + }) + } +} diff --git a/client/device/cloudfmc/fmcplatform/builder.go b/client/device/cloudfmc/fmcplatform/builder.go new file mode 100644 index 00000000..87a6b9f3 --- /dev/null +++ b/client/device/cloudfmc/fmcplatform/builder.go @@ -0,0 +1,20 @@ +package fmcplatform + +type ReadDomainInfoInputBuilder struct { + readDomainInfoInput *ReadDomainInfoInput +} + +func NewReadDomainInfoInputBuilder() *ReadDomainInfoInputBuilder { + readDomainInfoInput := &ReadDomainInfoInput{} + b := &ReadDomainInfoInputBuilder{readDomainInfoInput: readDomainInfoInput} + return b +} + +func (b *ReadDomainInfoInputBuilder) FmcHost(fmcHost string) *ReadDomainInfoInputBuilder { + b.readDomainInfoInput.FmcHost = fmcHost + return b +} + +func (b *ReadDomainInfoInputBuilder) Build() ReadDomainInfoInput { + return *b.readDomainInfoInput +} diff --git a/client/device/cloudfmc/fmcplatform/fixture_test.go b/client/device/cloudfmc/fmcplatform/fixture_test.go new file mode 100644 index 00000000..02f90073 --- /dev/null +++ b/client/device/cloudfmc/fmcplatform/fixture_test.go @@ -0,0 +1,16 @@ +package fmcplatform_test + +const ( + links = "unit-test-links" + count = 123 + offset = 234 + limit = 345 + pages = 456 + uuid = "unit-test-uuid" + name = "unit-test-name" + type_ = "unit-test-type" + + fmcHostname = "unit-test-fmc.com" + + baseUrl = "https://unit-test.cdo.cisco.com" +) diff --git a/client/device/cloudfmc/fmcplatform/readdomaininfo.go b/client/device/cloudfmc/fmcplatform/readdomaininfo.go new file mode 100644 index 00000000..5df3a7bf --- /dev/null +++ b/client/device/cloudfmc/fmcplatform/readdomaininfo.go @@ -0,0 +1,36 @@ +package fmcplatform + +import ( + "context" + "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/cloudfmc/fmcdomain" +) + +type ReadDomainInfoInput struct { + FmcHost string +} + +func NewReadDomainInfo(fmcHost string) ReadDomainInfoInput { + return ReadDomainInfoInput{ + FmcHost: fmcHost, + } +} + +type ReadDomainInfoOutput = fmcdomain.Info + +func ReadFmcDomainInfo(ctx context.Context, client http.Client, readInp ReadDomainInfoInput) (*ReadDomainInfoOutput, error) { + + client.Logger.Println("reading FMC domain info") + + readUrl := url.ReadFmcDomainInfo(readInp.FmcHost) + + req := client.NewGet(ctx, readUrl) + + var outp ReadDomainInfoOutput + if err := req.Send(&outp); err != nil { + return nil, err + } + + return &outp, nil +} diff --git a/client/device/cloudfmc/fmcplatform/readdomaininfo_test.go b/client/device/cloudfmc/fmcplatform/readdomaininfo_test.go new file mode 100644 index 00000000..dc02fa67 --- /dev/null +++ b/client/device/cloudfmc/fmcplatform/readdomaininfo_test.go @@ -0,0 +1,88 @@ +package fmcplatform_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc/fmcplatform" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/fmcdomain" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" + + internalHttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/jarcoal/httpmock" +) + +func TestReadDomainInfo(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + validOutput := &fmcplatform.ReadDomainInfoOutput{ + Links: fmcdomain.NewLinks(links), + Paging: fmcdomain.NewPaging(count, offset, limit, pages), + Items: []fmcdomain.Item{ + fmcdomain.NewItem(uuid, name, type_), + }, + } + + testCases := []struct { + testName string + fmcHostname string + setupFunc func() + assertFunc func(output *fmcplatform.ReadDomainInfoOutput, err error, t *testing.T) + }{ + { + testName: "successfully read FMC domain info", + fmcHostname: fmcHostname, + + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadFmcDomainInfo(fmcHostname), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validOutput), + ) + }, + + assertFunc: func(output *fmcplatform.ReadDomainInfoOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validOutput, output) + }, + }, + + { + testName: "error when read FMC domain info error", + fmcHostname: fmcHostname, + + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadFmcDomainInfo(fmcHostname), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + }, + + assertFunc: func(output *fmcplatform.ReadDomainInfoOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := fmcplatform.ReadFmcDomainInfo( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + fmcplatform.NewReadDomainInfo(fmcHostname), + ) + + testCase.assertFunc(output, err, t) + }) + } +} diff --git a/client/device/cdfmc/read.go b/client/device/cloudfmc/read.go similarity index 69% rename from client/device/cdfmc/read.go rename to client/device/cloudfmc/read.go index 19470b12..97cf7bc8 100644 --- a/client/device/cdfmc/read.go +++ b/client/device/cloudfmc/read.go @@ -1,4 +1,4 @@ -package cdfmc +package cloudfmc import ( "context" @@ -21,20 +21,21 @@ type ReadOutput = device.ReadOutput func Read(ctx context.Context, client http.Client, readInp ReadInput) (*ReadOutput, error) { - client.Logger.Println("reading cdFMC") + client.Logger.Println("reading cloud FMC") - cdfmcDevices, err := device.ReadAllByType(ctx, client, device.NewReadAllByTypeInput(devicetype.Cdfmc)) - if err != nil { + req := device.ReadAllByTypeRequest(ctx, client, device.NewReadAllByTypeInput(devicetype.CloudFmc)) + var cloudFmcDevices []ReadOutput + if err := req.Send(&cloudFmcDevices); err != nil { return nil, err } - if len(*cdfmcDevices) == 0 { + if len(cloudFmcDevices) == 0 { return nil, fmt.Errorf("firewall management center (FMC) not found") } - if len(*cdfmcDevices) > 1 { + if len(cloudFmcDevices) > 1 { return nil, fmt.Errorf("more than one firewall management center (FMC) found, please report this issue at: %s", cdo.TerraformProviderCDOIssuesUrl) } - return &(*cdfmcDevices)[0], nil + return &cloudFmcDevices[0], nil } diff --git a/client/device/cloudfmc/read_test.go b/client/device/cloudfmc/read_test.go new file mode 100644 index 00000000..859e30a0 --- /dev/null +++ b/client/device/cloudfmc/read_test.go @@ -0,0 +1,114 @@ +package cloudfmc_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" + internalHttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" +) + +func TestReadCloudFmc(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + validDevice := device.NewReadOutputBuilder(). + AsCloudFmc(). + WithName(deviceName). + WithUid(deviceUid). + WithLocation(deviceHost, devicePort). + WithCreatedDate(deviceCreatedDate). + WithLastUpdatedDate(deviceLastUpdatedDate). + OnboardedUsingCloudConnector(deviceCloudConnectorUId). + Build() + + validReadDeviceOutput := []device.ReadOutput{ + validDevice, + } + validReadFmcOutput := validDevice + + testCases := []struct { + testName string + setupFunc func() + assertFunc func(output *cloudfmc.ReadOutput, err error, t *testing.T) + }{ + { + testName: "successfully read Cloud FMC", + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAllDevicesByType(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadDeviceOutput), + ) + }, + assertFunc: func(output *cloudfmc.ReadOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validReadFmcOutput, *output) + }, + }, + { + testName: "error when read Cloud FMC error", + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAllDevicesByType(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + }, + assertFunc: func(output *cloudfmc.ReadOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when no Cloud FMC returned", + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAllDevicesByType(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, []device.ReadOutput{}), + ) + }, + assertFunc: func(output *cloudfmc.ReadOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when multiple Cloud FMCs returned", + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAllDevicesByType(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, []device.ReadOutput{validDevice, validDevice}), + ) + }, + assertFunc: func(output *cloudfmc.ReadOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := cloudfmc.Read( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + cloudfmc.NewReadInput(), + ) + + testCase.assertFunc(output, err, t) + }) + } +} diff --git a/client/device/cdfmc/readaccesspolicies.go b/client/device/cloudfmc/readaccesspolicies.go similarity index 59% rename from client/device/cdfmc/readaccesspolicies.go rename to client/device/cloudfmc/readaccesspolicies.go index 109d2a20..166ca29e 100644 --- a/client/device/cdfmc/readaccesspolicies.go +++ b/client/device/cloudfmc/readaccesspolicies.go @@ -1,21 +1,24 @@ -package cdfmc +package cloudfmc import ( "context" "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/accesspolicies" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/accesspolicies" + "strconv" ) type ReadAccessPoliciesInput struct { - DomainUid string - Limit int + FmcHostname string + DomainUid string + Limit int } -func NewReadAccessPoliciesInput(domainUid string, limit int) ReadAccessPoliciesInput { +func NewReadAccessPoliciesInput(fmcHostname, domainUid string, limit int) ReadAccessPoliciesInput { return ReadAccessPoliciesInput{ - DomainUid: domainUid, - Limit: limit, + FmcHostname: fmcHostname, + DomainUid: domainUid, + Limit: limit, } } @@ -23,9 +26,13 @@ type ReadAccessPoliciesOutput = accesspolicies.AccessPolicies func ReadAccessPolicies(ctx context.Context, client http.Client, inp ReadAccessPoliciesInput) (*ReadAccessPoliciesOutput, error) { - readUrl := url.ReadAccessPolicies(client.BaseUrl(), inp.DomainUid, inp.Limit) + client.Logger.Println("reading FMC Access Policies") + + readUrl := url.ReadAccessPolicies(client.BaseUrl(), inp.DomainUid) req := client.NewGet(ctx, readUrl) + req.Header.Add("Fmc-Hostname", inp.FmcHostname) // required, otherwise 500 internal server error + req.QueryParams.Add("limit", strconv.Itoa(inp.Limit)) var outp ReadAccessPoliciesOutput if err := req.Send(&outp); err != nil { diff --git a/client/device/cdfmc/readaccesspolicies_test.go b/client/device/cloudfmc/readaccesspolicies_test.go similarity index 78% rename from client/device/cdfmc/readaccesspolicies_test.go rename to client/device/cloudfmc/readaccesspolicies_test.go index 0ef8ee9d..6aef754e 100644 --- a/client/device/cdfmc/readaccesspolicies_test.go +++ b/client/device/cloudfmc/readaccesspolicies_test.go @@ -1,11 +1,11 @@ -package cdfmc_test +package cloudfmc_test import ( "context" - "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cdfmc" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" 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/accesspolicies" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/accesspolicies" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" "net/http" @@ -17,14 +17,14 @@ func TestAccessPoliciesRead(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - validAccessPolicesItems := accesspolicies.NewItems( + validAccessPolicesItems := []accesspolicies.Item{ accesspolicies.NewItem( accessPolicyId, accessPolicyName, accessPolicyType, accesspolicies.NewLinks(accessPolicySelfLink), ), - ) + } validAccessPoliciesPaging := accesspolicies.NewPaging( accessPolicyCount, accessPolicyOffset, @@ -44,7 +44,7 @@ func TestAccessPoliciesRead(t *testing.T) { domainUid string limit int setupFunc func() - assertFunc func(output *cdfmc.ReadAccessPoliciesOutput, err error, t *testing.T) + assertFunc func(output *cloudfmc.ReadAccessPoliciesOutput, err error, t *testing.T) }{ { testName: "successfully read Access Policies", @@ -53,11 +53,11 @@ func TestAccessPoliciesRead(t *testing.T) { setupFunc: func() { httpmock.RegisterResponder( http.MethodGet, - url.ReadAccessPolicies(baseUrl, domainUid, limit), + url.ReadAccessPolicies(baseUrl, domainUid), httpmock.NewJsonResponderOrPanic(http.StatusOK, validAccessPolicies), ) }, - assertFunc: func(output *cdfmc.ReadAccessPoliciesOutput, err error, t *testing.T) { + assertFunc: func(output *cloudfmc.ReadAccessPoliciesOutput, err error, t *testing.T) { assert.Nil(t, err) assert.NotNil(t, output) assert.Equal(t, validAccessPolicies, *output) @@ -70,11 +70,11 @@ func TestAccessPoliciesRead(t *testing.T) { setupFunc: func() { httpmock.RegisterResponder( http.MethodGet, - url.ReadAccessPolicies(baseUrl, domainUid, limit), + url.ReadAccessPolicies(baseUrl, domainUid), httpmock.NewStringResponder(http.StatusInternalServerError, "internal server error"), ) }, - assertFunc: func(output *cdfmc.ReadAccessPoliciesOutput, err error, t *testing.T) { + assertFunc: func(output *cloudfmc.ReadAccessPoliciesOutput, err error, t *testing.T) { assert.Nil(t, output) assert.NotNil(t, err) }, @@ -87,10 +87,10 @@ func TestAccessPoliciesRead(t *testing.T) { testCase.setupFunc() - output, err := cdfmc.ReadAccessPolicies( + output, err := cloudfmc.ReadAccessPolicies( context.Background(), *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), - cdfmc.NewReadAccessPoliciesInput(domainUid, limit), + cloudfmc.NewReadAccessPoliciesInput(fmcHostname, domainUid, limit), ) testCase.assertFunc(output, err, t) diff --git a/client/device/cdfmc/readsmartlicense.go b/client/device/cloudfmc/readsmartlicense.go similarity index 94% rename from client/device/cdfmc/readsmartlicense.go rename to client/device/cloudfmc/readsmartlicense.go index 29b3722b..502df010 100644 --- a/client/device/cdfmc/readsmartlicense.go +++ b/client/device/cloudfmc/readsmartlicense.go @@ -1,10 +1,10 @@ -package cdfmc +package cloudfmc import ( "context" "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/smartlicense" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/smartlicense" ) type ReadSmartLicenseInput struct{} diff --git a/client/device/cdfmc/readsmartlicense_test.go b/client/device/cloudfmc/readsmartlicense_test.go similarity index 83% rename from client/device/cdfmc/readsmartlicense_test.go rename to client/device/cloudfmc/readsmartlicense_test.go index 0dc8bb03..c65e8c5f 100644 --- a/client/device/cdfmc/readsmartlicense_test.go +++ b/client/device/cloudfmc/readsmartlicense_test.go @@ -1,11 +1,11 @@ -package cdfmc_test +package cloudfmc_test import ( "context" - "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cdfmc" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" 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/smartlicense" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/smartlicense" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" "net/http" @@ -24,13 +24,13 @@ func TestSmartLicenseRead(t *testing.T) { smartLicenseExportControl, smartLicenseVirtualAccount, ) - validSmartLicenseItems := smartlicense.NewItems( + validSmartLicenseItems := []smartlicense.Item{ smartlicense.NewItem( validSmartLicenseMetadata, smartLicenseRegStatus, smartLicenseType, ), - ) + } validSmartLicenseLinks := smartlicense.NewLinks(smartLicenseSelfLink) validSmartLicensePaging := smartlicense.NewPaging( smartLicenseCount, @@ -47,7 +47,7 @@ func TestSmartLicenseRead(t *testing.T) { testCases := []struct { testName string setupFunc func() - assertFunc func(output *cdfmc.ReadSmartLicenseOutput, err error, t *testing.T) + assertFunc func(output *cloudfmc.ReadSmartLicenseOutput, err error, t *testing.T) }{ { testName: "successfully read Smart License", @@ -58,7 +58,7 @@ func TestSmartLicenseRead(t *testing.T) { httpmock.NewJsonResponderOrPanic(http.StatusOK, validSmartLicense), ) }, - assertFunc: func(output *cdfmc.ReadSmartLicenseOutput, err error, t *testing.T) { + assertFunc: func(output *cloudfmc.ReadSmartLicenseOutput, err error, t *testing.T) { assert.Nil(t, err) assert.NotNil(t, output) assert.Equal(t, validSmartLicense, *output) @@ -73,7 +73,7 @@ func TestSmartLicenseRead(t *testing.T) { httpmock.NewStringResponder(http.StatusInternalServerError, "internal server error"), ) }, - assertFunc: func(output *cdfmc.ReadSmartLicenseOutput, err error, t *testing.T) { + assertFunc: func(output *cloudfmc.ReadSmartLicenseOutput, err error, t *testing.T) { assert.Nil(t, output) assert.NotNil(t, err) }, @@ -86,10 +86,10 @@ func TestSmartLicenseRead(t *testing.T) { testCase.setupFunc() - output, err := cdfmc.ReadSmartLicense( + output, err := cloudfmc.ReadSmartLicense( context.Background(), *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), - cdfmc.NewReadSmartLicenseInput(), + cloudfmc.NewReadSmartLicenseInput(), ) testCase.assertFunc(output, err, t) diff --git a/client/device/cloudfmc/readspecific.go b/client/device/cloudfmc/readspecific.go new file mode 100644 index 00000000..189b0a85 --- /dev/null +++ b/client/device/cloudfmc/readspecific.go @@ -0,0 +1,36 @@ +package cloudfmc + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" +) + +type ReadSpecificInput struct { + FmcId string +} + +type ReadSpecificOutput struct { + SpecificUid string `json:"uid"` + DomainUid string `json:"domainUid"` + State string `json:"state"` + Status string `json:"status"` +} + +func NewReadSpecificInput(fmcId string) ReadSpecificInput { + return ReadSpecificInput{ + FmcId: fmcId, + } +} + +func ReadSpecific(ctx context.Context, client http.Client, inp ReadSpecificInput) (*ReadSpecificOutput, error) { + + req := device.NewReadSpecificRequest(ctx, client, *device.NewReadSpecificInput(inp.FmcId)) + + var readSpecificOutp ReadSpecificOutput + if err := req.Send(&readSpecificOutp); err != nil { + return nil, err + } + + return &readSpecificOutp, nil +} diff --git a/client/device/cloudfmc/readspecific_outputbuilder.go b/client/device/cloudfmc/readspecific_outputbuilder.go new file mode 100644 index 00000000..d8cd6eea --- /dev/null +++ b/client/device/cloudfmc/readspecific_outputbuilder.go @@ -0,0 +1,35 @@ +package cloudfmc + +type ReadSpecificOutputBuilder struct { + readSpecificOutput *ReadSpecificOutput +} + +func NewReadSpecificOutputBuilder() *ReadSpecificOutputBuilder { + readSpecificOutput := &ReadSpecificOutput{} + b := &ReadSpecificOutputBuilder{readSpecificOutput: readSpecificOutput} + return b +} + +func (b *ReadSpecificOutputBuilder) SpecificUid(specificUid string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.SpecificUid = specificUid + return b +} + +func (b *ReadSpecificOutputBuilder) DomainUid(domainUid string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.DomainUid = domainUid + return b +} + +func (b *ReadSpecificOutputBuilder) State(state string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.State = state + return b +} + +func (b *ReadSpecificOutputBuilder) Status(status string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.Status = status + return b +} + +func (b *ReadSpecificOutputBuilder) Build() ReadSpecificOutput { + return *b.readSpecificOutput +} diff --git a/client/device/cloudfmc/readspecific_test.go b/client/device/cloudfmc/readspecific_test.go new file mode 100644 index 00000000..c51d00cc --- /dev/null +++ b/client/device/cloudfmc/readspecific_test.go @@ -0,0 +1,78 @@ +package cloudfmc_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" + 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/statemachine/state" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" +) + +func TestReadSpecificCloudFmc(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + validReadSpecificOutput := cloudfmc.ReadSpecificOutput{ + SpecificUid: specificDeviceUid, + DomainUid: domainUid, + State: state.DONE, + Status: status, + } + + testCases := []struct { + testName string + setupFunc func() + assertFunc func(output *cloudfmc.ReadSpecificOutput, err error, t *testing.T) + }{ + { + testName: "successfully read Cloud FMC specific device", + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, fmcUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadSpecificOutput), + ) + }, + assertFunc: func(output *cloudfmc.ReadSpecificOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validReadSpecificOutput, *output) + }, + }, + { + testName: "error when read Cloud FMC specific device error", + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, fmcUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + }, + assertFunc: func(output *cloudfmc.ReadSpecificOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := cloudfmc.ReadSpecific( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + cloudfmc.NewReadSpecificInput(fmcUid), + ) + + testCase.assertFunc(output, err, t) + }) + } +} diff --git a/client/device/cloudftd/create.go b/client/device/cloudftd/create.go new file mode 100644 index 00000000..11adedbd --- /dev/null +++ b/client/device/cloudftd/create.go @@ -0,0 +1,158 @@ +package cloudftd + +import ( + "context" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc/fmcplatform" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/cdo" + "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/internal/url" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/devicetype" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/license" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/tier" +) + +type CreateInput struct { + Name string + AccessPolicyName string + PerformanceTier *tier.Type // ignored if it is physical device + Virtual bool + Licenses []license.Type +} + +type CreateOutput struct { + Uid string `json:"uid"` + Name string `json:"name"` + Metadata *Metadata `json:"metadata"` +} + +func NewCreateInput( + name string, + accessPolicyName string, + performanceTier *tier.Type, + virtual bool, + licenses []license.Type, +) CreateInput { + return CreateInput{ + Name: name, + AccessPolicyName: accessPolicyName, + PerformanceTier: performanceTier, + Virtual: virtual, + Licenses: licenses, + } +} + +type createRequestBody struct { + FmcId string `json:"associatedDeviceUid"` + DeviceType devicetype.Type `json:"deviceType"` + Metadata *Metadata `json:"metadata"` + Name string `json:"name"` + State string `json:"state"` // TODO: use queueTriggerState? + Type string `json:"type"` + Model bool `json:"model"` +} + +func Create(ctx context.Context, client http.Client, createInp CreateInput) (*CreateOutput, error) { + + client.Logger.Println("creating cloud ftd") + + // 1. read Cloud FMC + fmcRes, err := cloudfmc.Read(ctx, client, cloudfmc.NewReadInput()) + if err != nil { + return nil, err + } + + // 2. get FMC domain uid by reading Cloud FMC domain info + readFmcDomainRes, err := fmcplatform.ReadFmcDomainInfo(ctx, client, fmcplatform.NewReadDomainInfo(fmcRes.Host)) + if err != nil { + return nil, err + } + if len(readFmcDomainRes.Items) == 0 { + return nil, fmt.Errorf("fmc domain info not found") + } + + // 3. get access policies using Cloud FMC domain uid + accessPoliciesRes, err := cloudfmc.ReadAccessPolicies( + ctx, + client, + cloudfmc.NewReadAccessPoliciesInput(fmcRes.Host, readFmcDomainRes.Items[0].Uuid, 1000), // 1000 is what CDO UI uses + ) + if err != nil { + return nil, err + } + selectedPolicy, ok := accessPoliciesRes.Find(createInp.AccessPolicyName) + if !ok { + return nil, fmt.Errorf( + `access policy: "%s" not found, available policies: %s. In rare cases where you have more than 1000 access policies, please raise an issue at: %s`, + createInp.AccessPolicyName, + accessPoliciesRes.Items, + cdo.TerraformProviderCDOIssuesUrl, + ) + } + + // handle performance tier + var performanceTier *tier.Type = nil // physical is nil + if createInp.Virtual { + performanceTier = createInp.PerformanceTier + } + + client.Logger.Println("posting FTD device") + + // 4. create the cloud ftd device + createUrl := url.CreateDevice(client.BaseUrl()) + createBody := createRequestBody{ + Name: createInp.Name, + FmcId: fmcRes.Uid, + DeviceType: devicetype.CloudFtd, + Metadata: &Metadata{ + AccessPolicyName: selectedPolicy.Name, + AccessPolicyUid: selectedPolicy.Id, + LicenseCaps: createInp.Licenses, + PerformanceTier: performanceTier, + }, + State: "NEW", + Type: "devices", + Model: false, + } + createReq := client.NewPost(ctx, createUrl, createBody) + var createOup CreateOutput + if err := createReq.Send(&createOup); err != nil { + return nil, err + } + + client.Logger.Println("reading FTD specific device") + + // 5. read created cloud ftd's specific device's uid + readSpecRes, err := device.ReadSpecific(ctx, client, *device.NewReadSpecificInput(createOup.Uid)) + if err != nil { + return nil, err + } + + // 6. initiate cloud ftd onboarding by triggering an endpoint at the specific device + _, err = UpdateSpecific(ctx, client, + NewUpdateSpecificFtdInput( + readSpecRes.SpecificUid, + "INITIATE_FTDC_ONBOARDING", + ), + ) + if err != nil { + return nil, err + } + + // 8. wait for generate command available + var metadata Metadata + err = retry.Do(UntilGeneratedCommandAvailable(ctx, client, createOup.Uid, &metadata), *retry.NewOptionsWithLoggerAndRetries(client.Logger, 3)) + if err != nil { + return nil, err + } + + // done! + return &CreateOutput{ + Uid: createOup.Uid, + Name: createOup.Name, + Metadata: &metadata, + }, nil +} diff --git a/client/device/cloudftd/create_metadatabuilder.go b/client/device/cloudftd/create_metadatabuilder.go new file mode 100644 index 00000000..34c0e986 --- /dev/null +++ b/client/device/cloudftd/create_metadatabuilder.go @@ -0,0 +1,60 @@ +package cloudftd + +import ( + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/license" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/tier" +) + +type MetadataBuilder struct { + metadata *Metadata +} + +func NewMetadataBuilder() *MetadataBuilder { + metadata := &Metadata{} + b := &MetadataBuilder{metadata: metadata} + return b +} + +func (b *MetadataBuilder) AccessPolicyName(accessPolicyName string) *MetadataBuilder { + b.metadata.AccessPolicyName = accessPolicyName + return b +} + +func (b *MetadataBuilder) AccessPolicyUuid(accessPolicyUuid string) *MetadataBuilder { + b.metadata.AccessPolicyUid = accessPolicyUuid + return b +} + +func (b *MetadataBuilder) CloudManagerDomain(cloudManagerDomain string) *MetadataBuilder { + b.metadata.CloudManagerDomain = cloudManagerDomain + return b +} + +func (b *MetadataBuilder) GeneratedCommand(generatedCommand string) *MetadataBuilder { + b.metadata.GeneratedCommand = generatedCommand + return b +} + +func (b *MetadataBuilder) LicenseCaps(licenseCaps []license.Type) *MetadataBuilder { + b.metadata.LicenseCaps = licenseCaps + return b +} + +func (b *MetadataBuilder) NatID(natID string) *MetadataBuilder { + b.metadata.NatID = natID + return b +} + +func (b *MetadataBuilder) PerformanceTier(performanceTier *tier.Type) *MetadataBuilder { + b.metadata.PerformanceTier = performanceTier + return b +} + +func (b *MetadataBuilder) RegKey(regKey string) *MetadataBuilder { + b.metadata.RegKey = regKey + return b +} + +func (b *MetadataBuilder) Build() Metadata { + return *b.metadata +} diff --git a/client/device/cloudftd/create_outputbuilder.go b/client/device/cloudftd/create_outputbuilder.go new file mode 100644 index 00000000..c22a9c3d --- /dev/null +++ b/client/device/cloudftd/create_outputbuilder.go @@ -0,0 +1,30 @@ +package cloudftd + +type CreateOutputBuilder struct { + createOutput *CreateOutput +} + +func NewCreateOutputBuilder() *CreateOutputBuilder { + createOutput := &CreateOutput{} + b := &CreateOutputBuilder{createOutput: createOutput} + return b +} + +func (b *CreateOutputBuilder) Uid(uid string) *CreateOutputBuilder { + b.createOutput.Uid = uid + return b +} + +func (b *CreateOutputBuilder) Name(name string) *CreateOutputBuilder { + b.createOutput.Name = name + return b +} + +func (b *CreateOutputBuilder) Metadata(metadata Metadata) *CreateOutputBuilder { + b.createOutput.Metadata = &metadata + return b +} + +func (b *CreateOutputBuilder) Build() CreateOutput { + return *b.createOutput +} diff --git a/client/device/cloudftd/create_test.go b/client/device/cloudftd/create_test.go new file mode 100644 index 00000000..7ceec933 --- /dev/null +++ b/client/device/cloudftd/create_test.go @@ -0,0 +1,285 @@ +package cloudftd_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" + internalHttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" +) + +func TestCreateCloudFtd(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + testCases := []struct { + testName string + input cloudftd.CreateInput + setupFunc func() + assertFunc func(output *cloudftd.CreateOutput, err error, t *testing.T) + }{ + { + testName: "successfully create Cloud FTD", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcDomainInfoIsSuccessful(true) + readFmcAccessPoliciesIsSuccessful(true) + createFtdIsSuccessful(true) + readFtdSpecificDeviceIsSuccessful(true) + triggerFtdOnboardingIsSuccessful(true) + generateFtdConfigureManagerCommandIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validReadFtdGeneratedCommandOutput, *output) + }, + }, + { + testName: "error when failed to read FMC", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(false) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to read FMC domain info", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcDomainInfoIsSuccessful(false) + readFmcAccessPoliciesIsSuccessful(true) + createFtdIsSuccessful(true) + readFtdSpecificDeviceIsSuccessful(true) + triggerFtdOnboardingIsSuccessful(true) + generateFtdConfigureManagerCommandIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to read FMC Access Policy", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcDomainInfoIsSuccessful(true) + readFmcAccessPoliciesIsSuccessful(false) + createFtdIsSuccessful(true) + readFtdSpecificDeviceIsSuccessful(true) + triggerFtdOnboardingIsSuccessful(true) + generateFtdConfigureManagerCommandIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to create FTD", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcDomainInfoIsSuccessful(true) + readFmcAccessPoliciesIsSuccessful(true) + createFtdIsSuccessful(false) + readFtdSpecificDeviceIsSuccessful(true) + triggerFtdOnboardingIsSuccessful(true) + generateFtdConfigureManagerCommandIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to read FTD specific device", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcDomainInfoIsSuccessful(true) + readFmcAccessPoliciesIsSuccessful(true) + createFtdIsSuccessful(true) + readFtdSpecificDeviceIsSuccessful(false) + triggerFtdOnboardingIsSuccessful(true) + generateFtdConfigureManagerCommandIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to read trigger FTD onboarding", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcDomainInfoIsSuccessful(true) + readFmcAccessPoliciesIsSuccessful(true) + createFtdIsSuccessful(true) + readFtdSpecificDeviceIsSuccessful(true) + triggerFtdOnboardingIsSuccessful(false) + generateFtdConfigureManagerCommandIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to read retrieve FTD configure manager command", + input: cloudftd.NewCreateInput(ftdName, ftdAccessPolicyName, &ftdPerformanceTier, ftdVirtual, ftdLicenseCaps), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcDomainInfoIsSuccessful(true) + readFmcAccessPoliciesIsSuccessful(true) + createFtdIsSuccessful(true) + readFtdSpecificDeviceIsSuccessful(true) + triggerFtdOnboardingIsSuccessful(true) + generateFtdConfigureManagerCommandIsSuccessful(false) + }, + assertFunc: func(output *cloudftd.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := cloudftd.Create( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + testCase.input, + ) + + testCase.assertFunc(output, err, t) + }) + } +} + +func readFmcIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAllDevicesByType(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadFmcOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAllDevicesByType(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func readFmcDomainInfoIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadFmcDomainInfo(fmcHost), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadFmcDomainInfoOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadFmcDomainInfo(fmcHost), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func readFmcAccessPoliciesIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAccessPolicies(baseUrl, fmcDomainUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadAccessPoliciesOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadAccessPolicies(baseUrl, fmcDomainUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func createFtdIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodPost, + url.CreateDevice(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validCreateFtdOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodPost, + url.CreateDevice(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func readFtdSpecificDeviceIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, ftdUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadFtdSpecificDeviceOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, ftdUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func triggerFtdOnboardingIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateSpecificCloudFtd(baseUrl, ftdSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validUpdateSpecificFtdOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateSpecificCloudFtd(baseUrl, ftdSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func generateFtdConfigureManagerCommandIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadDevice(baseUrl, ftdUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadFtdGeneratedCommandOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadDevice(baseUrl, ftdUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} diff --git a/client/device/cloudftd/delete.go b/client/device/cloudftd/delete.go new file mode 100644 index 00000000..18177b86 --- /dev/null +++ b/client/device/cloudftd/delete.go @@ -0,0 +1,64 @@ +package cloudftd + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc/fmcappliance" + "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/internal/statemachine" +) + +type DeleteInput struct { + Uid string +} + +func NewDeleteInput(uid string) DeleteInput { + return DeleteInput{ + Uid: uid, + } +} + +type DeleteOutput struct { +} + +func Delete(ctx context.Context, client http.Client, deleteInp DeleteInput) (*DeleteOutput, error) { + + // 1. read FMC that manages this cloud FTD + fmcReadRes, err := cloudfmc.Read(ctx, client, cloudfmc.NewReadInput()) + if err != nil { + return nil, err + } + + // 2. read FMC specific device, i.e. the actual FMC + fmcReadSpecificRes, err := cloudfmc.ReadSpecific(ctx, client, cloudfmc.NewReadSpecificInput(fmcReadRes.Uid)) + if err != nil { + return nil, err + } + + // 3. schedule a state machine for cloud fmc to delete the cloud FTD + _, err = fmcappliance.Update( + ctx, + client, + fmcappliance.NewUpdateInputBuilder(). + FmcSpecificUid(fmcReadSpecificRes.SpecificUid). + QueueTriggerState("PENDING_DELETE_FTDC"). + StateMachineContext(map[string]string{"ftdCDeviceIDs": deleteInp.Uid}). + Build(), + ) + if err != nil { + return nil, err + } + + // 4. wait until the delete cloud FTD state machine has started + err = retry.Do(statemachine.UntilStarted(ctx, client, fmcReadSpecificRes.SpecificUid, "fmceDeleteFtdcStateMachine"), retry.DefaultOpts) + if err != nil { + return nil, err + } + + // 5. we are not waiting for it to finish, like the CDO UI + + // done! + return &DeleteOutput{}, nil + +} diff --git a/client/device/cloudftd/delete_test.go b/client/device/cloudftd/delete_test.go new file mode 100644 index 00000000..f201299f --- /dev/null +++ b/client/device/cloudftd/delete_test.go @@ -0,0 +1,207 @@ +package cloudftd_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" + internalHttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/statemachine" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" +) + +func TestDeleteCloudFtd(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + testCases := []struct { + testName string + input cloudftd.DeleteInput + setupFunc func() + assertFunc func(output *cloudftd.DeleteOutput, err error, t *testing.T) + }{ + { + testName: "successfully create Cloud FTD", + input: cloudftd.NewDeleteInput(ftdUid), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcSpecificIsSuccessful(true) + triggerFtdDeleteOnFmcIsSuccessful(true) + waitForFtdDeleteStateMachineTriggeredIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.DeleteOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validDeleteOutput, *output) + }, + }, + { + testName: "successfully create Cloud FTD, and waited for delete state machine", + input: cloudftd.NewDeleteInput(ftdUid), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcSpecificIsSuccessful(true) + triggerFtdDeleteOnFmcIsSuccessful(true) + waitForFtdDeleteStateMachineTriggeredReturnedNotFound() + waitForFtdDeleteStateMachineTriggeredReturnedNotFound() + waitForFtdDeleteStateMachineTriggeredReturnedNotFound() + waitForFtdDeleteStateMachineTriggeredIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.DeleteOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validDeleteOutput, *output) + }, + }, + { + testName: "error when failed to read FMC", + input: cloudftd.NewDeleteInput(ftdUid), + setupFunc: func() { + readFmcIsSuccessful(false) + readFmcSpecificIsSuccessful(true) + triggerFtdDeleteOnFmcIsSuccessful(true) + waitForFtdDeleteStateMachineTriggeredIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.DeleteOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to read FMC specific", + input: cloudftd.NewDeleteInput(ftdUid), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcSpecificIsSuccessful(false) + triggerFtdDeleteOnFmcIsSuccessful(true) + waitForFtdDeleteStateMachineTriggeredIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.DeleteOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to trigger delete FTD state machine", + input: cloudftd.NewDeleteInput(ftdUid), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcSpecificIsSuccessful(true) + triggerFtdDeleteOnFmcIsSuccessful(false) + waitForFtdDeleteStateMachineTriggeredIsSuccessful(true) + }, + assertFunc: func(output *cloudftd.DeleteOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to wait for FTD delete state machine starts", + input: cloudftd.NewDeleteInput(ftdUid), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcSpecificIsSuccessful(true) + triggerFtdDeleteOnFmcIsSuccessful(true) + waitForFtdDeleteStateMachineTriggeredIsSuccessful(false) + }, + assertFunc: func(output *cloudftd.DeleteOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + { + testName: "error when failed to wait for FTD delete state machine starts, and waited before error", + input: cloudftd.NewDeleteInput(ftdUid), + setupFunc: func() { + readFmcIsSuccessful(true) + readFmcSpecificIsSuccessful(true) + triggerFtdDeleteOnFmcIsSuccessful(true) + waitForFtdDeleteStateMachineTriggeredReturnedNotFound() + waitForFtdDeleteStateMachineTriggeredReturnedNotFound() + waitForFtdDeleteStateMachineTriggeredReturnedNotFound() + waitForFtdDeleteStateMachineTriggeredIsSuccessful(false) + }, + assertFunc: func(output *cloudftd.DeleteOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := cloudftd.Delete( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + testCase.input, + ) + + testCase.assertFunc(output, err, t) + }) + } +} + +func waitForFtdDeleteStateMachineTriggeredIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadStateMachineInstance(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, []statemachine.ReadInstanceByDeviceUidOutput{ + validReadStateMachineOutput, + }), + ) + } else { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateFmcAppliance(baseUrl, fmcSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func waitForFtdDeleteStateMachineTriggeredReturnedNotFound() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadStateMachineInstance(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusNotFound, statemachine.StateMachineNotFoundError), + ) +} + +func triggerFtdDeleteOnFmcIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateFmcAppliance(baseUrl, fmcSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validUpdateFmcSpecificOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateFmcAppliance(baseUrl, fmcSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func readFmcSpecificIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, fmcUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadSpecificOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, fmcUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} diff --git a/client/device/cloudftd/fixture_test.go b/client/device/cloudftd/fixture_test.go new file mode 100644 index 00000000..b3770562 --- /dev/null +++ b/client/device/cloudftd/fixture_test.go @@ -0,0 +1,159 @@ +package cloudftd_test + +import ( + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc/fmcappliance" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/statemachine" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/accesspolicies" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/fmcdomain" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/license" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/tier" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/statemachine/state" +) + +const ( + baseUrl = "https://unit-test.net" + + fmcName = "unit-test-device-name" + fmcUid = "unit-test-uid" + fmcDomainUid = "unit-test-domain-uid" + fmcHost = "unit-test-fmc-host.com" + fmcPort = 1234 + fmcLink = "unit-test-fmc-link" + + fmcDomainPages = 123 + fmcDomainCount = 123 + fmcDomainOffset = 123 + fmcDomainLimit = 123 + fmcDomainItemName = "unit-test-fmcDomainItemName" + fmcDomainItemType = "unit-test-fmcDomainItemType" + fmcDomainItemUid = fmcDomainUid + + fmcAccessPolicyPages = 123 + fmcAccessPolicyCount = 123 + fmcAccessPolicyOffset = 123 + fmcAccessPolicyLimit = 123 + fmcAccessPolicyItemName = "unit-test-access-policy-item-name" + fmcAccessPolicyItemType = "unit-test-access-policy-item-type" + fmcAccessPolicyItemUid = "unit-test-access-policy-item-uid" + + fmcSpecificUid = "unit-test-fmc-specific-uid" + fmcSpecificStatus = "unit-test-fmc-specific-status" + fmcSpecificState = "unit-test-fmc-specific-state" + + ftdName = "unit-test-ftdName" + ftdUid = "unit-test-ftdUid" + ftdVirtual = true + + ftdGeneratedCommand = "unit-test-ftdGeneratedCommand" + ftdAccessPolicyName = fmcAccessPolicyItemName + ftdNatID = "unit-test-ftdNatID" + ftdCloudManagerDomain = "unit-test-ftdCloudManagerDomain.com" + ftdRegKey = "unit-test-ftdRegKey" + + ftdSpecificUid = "unit-test-ftd-specific-uid" + ftdSpecificType = "unit-test-ftd-specific-type" + ftdSpecificState = "unit-test-ftd-specific-state" + ftdSpecificNamespace = "unit-test-ftd-specific-namespace" +) + +var ( + ftdLicenseCaps = []license.Type{license.Base, license.Carrier} + ftdPerformanceTier = tier.FTDv5 +) + +var ( + validReadFmcOutput = []device.ReadOutput{ + device.NewReadOutputBuilder(). + AsCloudFmc(). + WithName(fmcName). + WithUid(fmcUid). + WithLocation(fmcHost, fmcPort). + Build(), + } + + validReadFmcDomainInfoOutput = fmcdomain.NewInfoBuilder(). + Links(fmcdomain.NewLinks(fmcLink)). + Paging(fmcdomain.NewPaging(fmcDomainCount, fmcDomainOffset, fmcDomainLimit, fmcDomainPages)). + Items([]fmcdomain.Item{ + fmcdomain.NewItem( + fmcDomainItemUid, + fmcDomainItemName, + fmcDomainItemType, + ), + }). + Build() + + validReadAccessPoliciesOutput = accesspolicies.NewAccessPoliciesBuilder(). + Links(accesspolicies.NewLinks(fmcLink)). + Paging(accesspolicies.NewPaging( + fmcAccessPolicyPages, + fmcAccessPolicyCount, + fmcAccessPolicyOffset, + fmcAccessPolicyLimit, + )). + Items([]accesspolicies.Item{accesspolicies.NewItem( + fmcAccessPolicyItemUid, + fmcAccessPolicyItemName, + fmcAccessPolicyItemType, + accesspolicies.NewLinks(fmcLink), + )}). + Build() + + validCreateFtdOutput = cloudftd.NewCreateOutputBuilder(). + Name(ftdName). + Uid(ftdUid). + Metadata(cloudftd.NewMetadataBuilder(). + LicenseCaps(ftdLicenseCaps). + AccessPolicyName(ftdAccessPolicyName). + PerformanceTier(&ftdPerformanceTier). + CloudManagerDomain(ftdCloudManagerDomain). + Build()). + Build() + + validUpdateSpecificFtdOutput = cloudftd.NewUpdateSpecificFtdOutputBuilder(). + SpecificUid(ftdSpecificUid). + Build() + + validReadFtdSpecificDeviceOutput = cloudftd.NewReadSpecificOutputBuilder(). + SpecificUid(ftdSpecificUid). + Type(ftdSpecificType). + State(ftdSpecificState). + Namespace(ftdSpecificNamespace). + Build() + + validReadFtdGeneratedCommandOutput = cloudftd.NewCreateOutputBuilder(). + Name(ftdName). + Uid(ftdUid). + Metadata(cloudftd.NewMetadataBuilder(). + LicenseCaps(ftdLicenseCaps). + GeneratedCommand(ftdGeneratedCommand). + AccessPolicyName(ftdAccessPolicyName). + PerformanceTier(&ftdPerformanceTier). + NatID(ftdNatID). + CloudManagerDomain(ftdCloudManagerDomain). + RegKey(ftdRegKey). + Build()). + Build() + + validReadSpecificOutput = cloudfmc.ReadSpecificOutput{ + SpecificUid: fmcSpecificUid, + DomainUid: fmcDomainUid, + State: state.DONE, + Status: fmcSpecificStatus, + } + + validUpdateFmcSpecificOutput = fmcappliance.NewUpdateOutputBuilder(). + Uid(fmcSpecificUid). + State(fmcSpecificState). + DomainUid(fmcDomainUid). + Build() + + validReadStateMachineOutput = statemachine.NewReadInstanceByDeviceUidOutputBuilder(). + StateMachineIdentifier("fmceDeleteFtdcStateMachine"). + Build() + + validDeleteOutput = cloudftd.DeleteOutput{} +) diff --git a/client/device/cloudftd/metadata.go b/client/device/cloudftd/metadata.go new file mode 100644 index 00000000..a0eb313e --- /dev/null +++ b/client/device/cloudftd/metadata.go @@ -0,0 +1,75 @@ +package cloudftd + +import ( + "encoding/json" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/license" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/tier" +) + +type Metadata struct { + AccessPolicyName string `json:"accessPolicyName,omitempty"` + AccessPolicyUid string `json:"accessPolicyUuid,omitempty"` + CloudManagerDomain string `json:"cloudManagerDomain,omitempty"` + GeneratedCommand string `json:"generatedCommand,omitempty"` + LicenseCaps []license.Type `json:"license_caps,omitempty"` + NatID string `json:"natID,omitempty"` + PerformanceTier *tier.Type `json:"performanceTier,omitempty"` + RegKey string `json:"regKey,omitempty"` +} + +type internalMetadata struct { + AccessPolicyName string `json:"accessPolicyName,omitempty"` + AccessPolicyUuid string `json:"accessPolicyUuid,omitempty"` + CloudManagerDomain string `json:"cloudManagerDomain,omitempty"` + GeneratedCommand string `json:"generatedCommand,omitempty"` + LicenseCaps string `json:"license_caps,omitempty"` // first, unmarshal it into string + NatID string `json:"natID,omitempty"` + PerformanceTier *tier.Type `json:"performanceTier,omitempty"` + RegKey string `json:"regKey,omitempty"` +} + +// UnmarshalJSON defines custom unmarshal json for metadata, because we need to handle license caps differently, +// it is a string containing command separated values, instead of a json list where it can be parsed directly. +// Note that this method is defined on the *Metadata type, so if you unmarshal or marshal a Metadata without pointer, +// it will not be called. +func (metadata *Metadata) UnmarshalJSON(data []byte) error { + fmt.Printf("\nunmarshalling Metadata: %s\n", string(data)) + var internalMeta internalMetadata + err := json.Unmarshal(data, &internalMeta) + if err != nil { + return err + } + + licenseCaps, err := license.DeserializeAll(internalMeta.LicenseCaps) // now parse it into golang type + if err != nil { + return err + } + + (*metadata).AccessPolicyName = internalMeta.AccessPolicyName + (*metadata).AccessPolicyUid = internalMeta.AccessPolicyUuid + (*metadata).CloudManagerDomain = internalMeta.CloudManagerDomain + (*metadata).GeneratedCommand = internalMeta.GeneratedCommand + (*metadata).NatID = internalMeta.NatID + (*metadata).PerformanceTier = internalMeta.PerformanceTier + (*metadata).RegKey = internalMeta.RegKey + + (*metadata).LicenseCaps = licenseCaps // set it as usual + + return nil +} + +func (metadata *Metadata) MarshalJSON() ([]byte, error) { + fmt.Printf("\nmarshalling Metadata: %+v\n", metadata) + var internalMeta internalMetadata + internalMeta.AccessPolicyName = metadata.AccessPolicyName + internalMeta.AccessPolicyUuid = metadata.AccessPolicyUid + internalMeta.CloudManagerDomain = metadata.CloudManagerDomain + internalMeta.GeneratedCommand = metadata.GeneratedCommand + internalMeta.LicenseCaps = license.SerializeAll(metadata.LicenseCaps) + internalMeta.NatID = metadata.NatID + internalMeta.PerformanceTier = metadata.PerformanceTier + internalMeta.RegKey = metadata.RegKey + + return json.Marshal(internalMeta) +} diff --git a/client/device/cloudftd/read_by_name.go b/client/device/cloudftd/read_by_name.go new file mode 100644 index 00000000..bca67ead --- /dev/null +++ b/client/device/cloudftd/read_by_name.go @@ -0,0 +1,41 @@ +package cloudftd + +import ( + "context" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/cdo" + "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/devicetype" +) + +type ReadByNameInput struct { + Name string +} + +func NewReadByNameInput(name string) ReadByNameInput { + return ReadByNameInput{ + Name: name, + } +} +g +func ReadByName(ctx context.Context, client http.Client, readInp ReadByNameInput) (*ReadOutput, error) { + + readUrl := url.ReadDeviceByNameAndType(client.BaseUrl(), readInp.Name, devicetype.CloudFtd) + req := client.NewGet(ctx, readUrl) + + var readOutp []ReadOutput + if err := req.Send(&readOutp); err != nil { + return nil, err + } + + if len(readOutp) == 0 { + return nil, fmt.Errorf("cloudftd with name: \"%s\" not found", readInp.Name) + } + + if len(readOutp) > 1 { + return nil, fmt.Errorf("multiple ftds with name: \"%s\" found, this is unexpected, please report this error at: %s", readInp.Name, cdo.TerraformProviderCDOIssuesUrl) + } + + return &readOutp[0], nil +} diff --git a/client/device/cloudftd/read_by_uid.go b/client/device/cloudftd/read_by_uid.go new file mode 100644 index 00000000..bd002a0f --- /dev/null +++ b/client/device/cloudftd/read_by_uid.go @@ -0,0 +1,36 @@ +package cloudftd + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" +) + +type ReadByUidInput struct { + Uid string `json:"uid"` +} + +func NewReadByUidInput(uid string) ReadByUidInput { + return ReadByUidInput{ + Uid: uid, + } +} + +type ReadOutput struct { + Uid string `json:"uid"` + Name string `json:"name"` + Metadata Metadata `json:"metadata,omitempty"` +} + +func ReadByUid(ctx context.Context, client http.Client, readInp ReadByUidInput) (*ReadOutput, error) { + + readUrl := url.ReadDevice(client.BaseUrl(), readInp.Uid) + req := client.NewGet(ctx, readUrl) + + var readOutp ReadOutput + if err := req.Send(&readOutp); err != nil { + return nil, err + } + + return &readOutp, nil +} diff --git a/client/device/cloudftd/read_outputbuilder.go b/client/device/cloudftd/read_outputbuilder.go new file mode 100644 index 00000000..ee752b56 --- /dev/null +++ b/client/device/cloudftd/read_outputbuilder.go @@ -0,0 +1,30 @@ +package cloudftd + +type ReadOutputBuilder struct { + readOutput *ReadOutput +} + +func NewReadOutputBuilder() *ReadOutputBuilder { + readOutput := &ReadOutput{} + b := &ReadOutputBuilder{readOutput: readOutput} + return b +} + +func (b *ReadOutputBuilder) Uid(uid string) *ReadOutputBuilder { + b.readOutput.Uid = uid + return b +} + +func (b *ReadOutputBuilder) Name(name string) *ReadOutputBuilder { + b.readOutput.Name = name + return b +} + +func (b *ReadOutputBuilder) Metadata(metadata Metadata) *ReadOutputBuilder { + b.readOutput.Metadata = metadata + return b +} + +func (b *ReadOutputBuilder) Build() ReadOutput { + return *b.readOutput +} diff --git a/client/device/cloudftd/readspecific.go b/client/device/cloudftd/readspecific.go new file mode 100644 index 00000000..2134c241 --- /dev/null +++ b/client/device/cloudftd/readspecific.go @@ -0,0 +1,13 @@ +package cloudftd + +import "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + +var ReadSpecific = device.ReadSpecific + +type ReadSpecificInput = device.ReadSpecificInput + +type ReadSpecificOutput = device.ReadSpecificOutput + +var NewReadSpecificInputBuilder = device.NewReadSpecificInputBuilder + +var NewReadSpecificOutputBuilder = device.NewReadSpecificOutputBuilder diff --git a/client/device/cloudftd/retry.go b/client/device/cloudftd/retry.go new file mode 100644 index 00000000..75878b22 --- /dev/null +++ b/client/device/cloudftd/retry.go @@ -0,0 +1,29 @@ +package cloudftd + +import ( + "context" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/retry" +) + +func UntilGeneratedCommandAvailable(ctx context.Context, client http.Client, uid string, metadata *Metadata) retry.Func { + + return func() (bool, error) { + client.Logger.Println("checking if FTD generated command is available") + + readOutp, err := ReadByUid(ctx, client, NewReadByUidInput(uid)) + if err != nil { + return false, err + } + + client.Logger.Printf("device metadata=%v\n", readOutp.Metadata) + + if readOutp.Metadata.GeneratedCommand != "" { + *metadata = readOutp.Metadata + return true, nil + } else { + return false, fmt.Errorf("generated command not found in metadata: %+v", readOutp.Metadata) + } + } +} diff --git a/client/device/cloudftd/update.go b/client/device/cloudftd/update.go new file mode 100644 index 00000000..6f324fbd --- /dev/null +++ b/client/device/cloudftd/update.go @@ -0,0 +1,41 @@ +package cloudftd + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" +) + +type UpdateInput struct { + Uid string + Name string +} + +func NewUpdateInput(uid, name string) UpdateInput { + return UpdateInput{ + Uid: uid, + Name: name, + } +} + +type updateRequestBody struct { + Name string `json:"name"` +} + +type UpdateOutput = ReadOutput + +func Update(ctx context.Context, client http.Client, updateInp UpdateInput) (*UpdateOutput, error) { + + updateUrl := url.UpdateDevice(client.BaseUrl(), updateInp.Uid) + updateBody := updateRequestBody{ + Name: updateInp.Name, + } + req := client.NewPut(ctx, updateUrl, updateBody) + var updateOutp UpdateOutput + if err := req.Send(&updateOutp); err != nil { + return nil, err + } + + return &updateOutp, nil + +} diff --git a/client/device/cloudftd/updatespecific.go b/client/device/cloudftd/updatespecific.go new file mode 100644 index 00000000..9c1d9b23 --- /dev/null +++ b/client/device/cloudftd/updatespecific.go @@ -0,0 +1,45 @@ +package cloudftd + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" +) + +type UpdateSpecificFtdInput struct { + SpecificUid string + QueueTriggerState string +} + +func NewUpdateSpecificFtdInput(specificUid string, queueTriggerState string) UpdateSpecificFtdInput { + return UpdateSpecificFtdInput{ + SpecificUid: specificUid, + QueueTriggerState: queueTriggerState, + } +} + +type UpdateSpecificFtdOutput struct { + SpecificUid string `json:"uid"` +} + +type updateSpecificRequestBody struct { + QueueTriggerState string `json:"queueTriggerState"` +} + +func UpdateSpecific(ctx context.Context, client http.Client, updateInp UpdateSpecificFtdInput) (*UpdateSpecificFtdOutput, error) { + + client.Logger.Println("updating FTD specific device") + + updateUrl := url.UpdateSpecificCloudFtd(client.BaseUrl(), updateInp.SpecificUid) + + updateBody := updateSpecificRequestBody{ + QueueTriggerState: updateInp.QueueTriggerState, + } + req := client.NewPut(ctx, updateUrl, updateBody) + var updateOutp UpdateSpecificFtdOutput + if err := req.Send(&updateOutp); err != nil { + return nil, err + } + + return &updateOutp, nil +} diff --git a/client/device/cloudftd/updatespecific_outputbuilder.go b/client/device/cloudftd/updatespecific_outputbuilder.go new file mode 100644 index 00000000..4efe2573 --- /dev/null +++ b/client/device/cloudftd/updatespecific_outputbuilder.go @@ -0,0 +1,20 @@ +package cloudftd + +type UpdateSpecificFtdOutputBuilder struct { + updateSpecificFtdOutput *UpdateSpecificFtdOutput +} + +func NewUpdateSpecificFtdOutputBuilder() *UpdateSpecificFtdOutputBuilder { + updateSpecificFtdOutput := &UpdateSpecificFtdOutput{} + b := &UpdateSpecificFtdOutputBuilder{updateSpecificFtdOutput: updateSpecificFtdOutput} + return b +} + +func (b *UpdateSpecificFtdOutputBuilder) SpecificUid(specificUid string) *UpdateSpecificFtdOutputBuilder { + b.updateSpecificFtdOutput.SpecificUid = specificUid + return b +} + +func (b *UpdateSpecificFtdOutputBuilder) Build() UpdateSpecificFtdOutput { + return *b.updateSpecificFtdOutput +} diff --git a/client/device/create_outputbuilder.go b/client/device/create_outputbuilder.go new file mode 100644 index 00000000..6ea7e9bc --- /dev/null +++ b/client/device/create_outputbuilder.go @@ -0,0 +1,3 @@ +package device + +var NewCreateOutputBuilder = NewReadOutputBuilder diff --git a/client/device/read_by_nameandtype.go b/client/device/read_by_nameandtype.go index bc564d52..aa33118c 100644 --- a/client/device/read_by_nameandtype.go +++ b/client/device/read_by_nameandtype.go @@ -5,14 +5,15 @@ import ( "fmt" "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/devicetype" ) type ReadByNameAndTypeInput struct { - Name string `json:"name"` - DeviceType string `json:"deviceType"` + Name string `json:"name"` + DeviceType devicetype.Type `json:"deviceType"` } -func NewReadByNameAndTypeInput(name, deviceType string) ReadByNameAndTypeInput { +func NewReadByNameAndTypeInput(name string, deviceType devicetype.Type) ReadByNameAndTypeInput { return ReadByNameAndTypeInput{ Name: name, DeviceType: deviceType, @@ -23,7 +24,7 @@ func ReadByNameAndType(ctx context.Context, client http.Client, readInp ReadByNa client.Logger.Println("reading Device by name and device type") - readUrl := url.ReadDeviceByNameAndDeviceType(client.BaseUrl(), readInp.Name, readInp.DeviceType) + readUrl := url.ReadDeviceByNameAndType(client.BaseUrl(), readInp.Name, readInp.DeviceType) req := client.NewGet(ctx, readUrl) diff --git a/client/device/read_outputbuilder.go b/client/device/read_outputbuilder.go index 0192afc5..af9f1c8d 100644 --- a/client/device/read_outputbuilder.go +++ b/client/device/read_outputbuilder.go @@ -33,7 +33,7 @@ func (builder *readOutputBuilder) AsIos() *readOutputBuilder { return builder } -func (builder *readOutputBuilder) AsCdfmc() *readOutputBuilder { +func (builder *readOutputBuilder) AsCloudFmc() *readOutputBuilder { builder.readOutput.DeviceType = "FMCE" return builder } diff --git a/client/device/readall_by_type.go b/client/device/readall_by_type.go index 81dd439c..dc486bf6 100644 --- a/client/device/readall_by_type.go +++ b/client/device/readall_by_type.go @@ -2,13 +2,14 @@ package device import ( "context" + "fmt" "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/devicetype" ) type ReadAllByTypeInput struct { - DeviceType devicetype.Type `json:"deviceType"` + DeviceType devicetype.Type } type ReadAllByTypeOutput = []ReadOutput @@ -19,13 +20,21 @@ func NewReadAllByTypeInput(deviceType devicetype.Type) ReadAllByTypeInput { } } +func ReadAllByTypeRequest(ctx context.Context, client http.Client, readInp ReadAllByTypeInput) *http.Request { + readAllUrl := url.ReadAllDevicesByType(client.BaseUrl()) + + req := client.NewGet(ctx, readAllUrl) + + req.QueryParams.Add("q", fmt.Sprintf("deviceType:%s", readInp.DeviceType)) + + return req +} + func ReadAllByType(ctx context.Context, client http.Client, readInp ReadAllByTypeInput) (*ReadAllByTypeOutput, error) { client.Logger.Println("reading all Devices by device type") - readAllUrl := url.ReadAllDevicesByType(client.BaseUrl(), readInp.DeviceType) - - req := client.NewGet(ctx, readAllUrl) + req := ReadAllByTypeRequest(ctx, client, readInp) var outp []ReadOutput if err := req.Send(&outp); err != nil { diff --git a/client/device/readall_by_type_test.go b/client/device/readall_by_type_test.go index f27f7569..cd4ef8ea 100644 --- a/client/device/readall_by_type_test.go +++ b/client/device/readall_by_type_test.go @@ -19,14 +19,14 @@ func TestDeviceReadAllByType(t *testing.T) { validDevice1 := device. NewReadOutputBuilder(). - AsCdfmc(). + AsCloudFmc(). WithUid(deviceUid1). WithName(deviceName1). Build() validDevice2 := device. NewReadOutputBuilder(). - AsCdfmc(). + AsCloudFmc(). WithUid(deviceUid2). WithName(deviceName2). Build() @@ -44,11 +44,11 @@ func TestDeviceReadAllByType(t *testing.T) { }{ { testName: "successfully read devices by type", - targetType: devicetype.Cdfmc, + targetType: devicetype.CloudFmc, setupFunc: func() { httpmock.RegisterResponder( http.MethodGet, - url.ReadAllDevicesByType(baseUrl, devicetype.Cdfmc), + url.ReadAllDevicesByType(baseUrl), httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadAllOutput), ) }, @@ -60,7 +60,7 @@ func TestDeviceReadAllByType(t *testing.T) { }, { testName: "return error when read devices by type error", - targetType: devicetype.Cdfmc, + targetType: devicetype.CloudFmc, setupFunc: func() { httpmock.RegisterResponder( http.MethodGet, diff --git a/client/device/readspecific_inputbuilder.go b/client/device/readspecific_inputbuilder.go new file mode 100644 index 00000000..86f304ec --- /dev/null +++ b/client/device/readspecific_inputbuilder.go @@ -0,0 +1,20 @@ +package device + +type ReadSpecificInputBuilder struct { + readSpecificInput *ReadSpecificInput +} + +func NewReadSpecificInputBuilder() *ReadSpecificInputBuilder { + readSpecificInput := &ReadSpecificInput{} + b := &ReadSpecificInputBuilder{readSpecificInput: readSpecificInput} + return b +} + +func (b *ReadSpecificInputBuilder) Uid(uid string) *ReadSpecificInputBuilder { + b.readSpecificInput.Uid = uid + return b +} + +func (b *ReadSpecificInputBuilder) Build() ReadSpecificInput { + return *b.readSpecificInput +} diff --git a/client/device/readspecific_outputbuilder.go b/client/device/readspecific_outputbuilder.go new file mode 100644 index 00000000..a46cbfe8 --- /dev/null +++ b/client/device/readspecific_outputbuilder.go @@ -0,0 +1,35 @@ +package device + +type ReadSpecificOutputBuilder struct { + readSpecificOutput *ReadSpecificOutput +} + +func NewReadSpecificOutputBuilder() *ReadSpecificOutputBuilder { + readSpecificOutput := &ReadSpecificOutput{} + b := &ReadSpecificOutputBuilder{readSpecificOutput: readSpecificOutput} + return b +} + +func (b *ReadSpecificOutputBuilder) SpecificUid(specificUid string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.SpecificUid = specificUid + return b +} + +func (b *ReadSpecificOutputBuilder) State(state string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.State = state + return b +} + +func (b *ReadSpecificOutputBuilder) Namespace(namespace string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.Namespace = namespace + return b +} + +func (b *ReadSpecificOutputBuilder) Type(type_ string) *ReadSpecificOutputBuilder { + b.readSpecificOutput.Type = type_ + return b +} + +func (b *ReadSpecificOutputBuilder) Build() ReadSpecificOutput { + return *b.readSpecificOutput +} diff --git a/client/examples/main.go b/client/examples/main.go new file mode 100644 index 00000000..71aadb33 --- /dev/null +++ b/client/examples/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" +) + +func main() { + client, err := client.New("https://ci.dev.lockhart.io", "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOiIwIiwic2NvcGUiOlsidHJ1c3QiLCJhZTk4ZDI1Zi0xMDg5LTQyODYtYTNjNS01MDVkY2I0NDMxYTIiLCJyZWFkIiwid3JpdGUiXSwiYW1yIjoicHdkIiwicm9sZXMiOlsiUk9MRV9BRE1JTiJdLCJpc3MiOiJpdGQiLCJjbHVzdGVySWQiOiIxIiwiaWQiOiI2NzJjMGE0MS1kMjAzLTQ2YzEtYmE5ZS0wNDVmYWUwYTc5ZGQiLCJzdWJqZWN0VHlwZSI6InVzZXIiLCJqdGkiOiI0MzAxNTMxMS1mYTcyLTQ1NDEtYTc4OS0yNGM2M2JmZTU1ZjYiLCJwYXJlbnRJZCI6ImFlOThkMjVmLTEwODktNDI4Ni1hM2M1LTUwNWRjYjQ0MzFhMiIsImNsaWVudF9pZCI6ImFwaS1jbGllbnQifQ.psPRQHG4UKxYxS-xEjlo40_vTnwBkEmKc-7LSoeGxjXWywFNc1cMUCtE7aENIi-HfDertAKfatmr6ZiJE-9F9Xc1etDqv7LAhFNlKtpYiVzSGPkPbfUINuDWt59Ymy3rRA25SJIuesROVx19eXjJF9IxyGMm5sYRS4H24wd50YoMRjuget_92NXeY-XjcmaL9TSGOmO-tfzMaPs2hE7IjXBcTJaI-btA8UJLczQbkmdADnLB9OFJfHArnkgDXF5hNp8JXg3rAM8UWmJrjSnClx7XLruWISaHWGbzWBE5ydGL9egxA-r2SFmoNWyPODkDRHrivL2oEVPyj46nveWjrQ") + if err != nil { + panic(err) + } + ctx := context.Background() + _, err = client.UpdateCloudFtd(ctx, cloudftd.NewUpdateInput("66827980-584b-4ba1-9ee6-a0a1692c640f", "my-new-name")) + if err != nil { + panic(err) + } +} diff --git a/client/internal/http/request.go b/client/internal/http/request.go index db9f1397..913066bc 100644 --- a/client/internal/http/request.go +++ b/client/internal/http/request.go @@ -11,6 +11,7 @@ import ( "io" "log" "net/http" + netUrl "net/url" "strings" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/cdo" @@ -29,12 +30,13 @@ type Request struct { url string body any + Header http.Header + QueryParams netUrl.Values + Response *Response Error error } -const () - func NewRequest(config cdo.Config, httpClient *http.Client, logger *log.Logger, ctx context.Context, method string, url string, body any) *Request { return &Request{ config: config, @@ -46,6 +48,9 @@ func NewRequest(config cdo.Config, httpClient *http.Client, logger *log.Logger, method: method, url: url, body: body, + + Header: make(http.Header), + QueryParams: make(netUrl.Values), } } @@ -95,13 +100,14 @@ func (r *Request) send(output any) error { // check status if res.StatusCode >= 400 { body, err := io.ReadAll(res.Body) - err = fmt.Errorf("failed: code=%d, status=%s, body=%s, readBodyErr=%s, url=%s, method=%s", res.StatusCode, res.Status, string(body), err, r.url, r.method) + err = fmt.Errorf("failed: url=%s, code=%d, status=%s, body=%s, readBodyErr=%s, method=%s, header=%s", r.url, res.StatusCode, res.Status, string(body), err, r.method, r.Header) r.Error = err return err } // request is all good, now parse body resBody, err := io.ReadAll(res.Body) + fmt.Printf("\n\nsuccess: url=%s, code=%d, status=%s, body=%s, readBodyErr=%s, method=%s, header=%s, queryParams=%s\n", r.url, res.StatusCode, res.Status, string(resBody), err, r.method, r.Header, r.QueryParams) if err != nil { r.Error = err return err @@ -131,6 +137,21 @@ func (r *Request) build() (*http.Request, error) { return nil, err } + // TODO: remove these debug lines + //if r.method != "GET" && r.method != "DELETE" { + // bodyReader2, err := toReader(r.body) + // if err != nil { + // return nil, err + // } + // bs, err := io.ReadAll(bodyReader2) + // if err != nil { + // return nil, err + // } + // fmt.Println("request_check") + // fmt.Printf("Request: %+v\n", r) + // fmt.Printf("Request: %s, %s, %s\n", r.url, r.method, string(bs)) + //} + req, err := http.NewRequest(r.method, r.url, bodyReader) if err != nil { return nil, err @@ -138,16 +159,41 @@ func (r *Request) build() (*http.Request, error) { if r.ctx != nil { req = req.WithContext(r.ctx) } - r.addAuthHeader(req) + + r.addHeaders(req) + r.addQueryParams(req) return req, nil } +func (r *Request) addQueryParams(req *http.Request) { + q := req.URL.Query() + for k, vs := range r.QueryParams { + for _, v := range vs { + q.Add(k, v) + } + } + req.URL.RawQuery = q.Encode() +} + +func (r *Request) addHeaders(req *http.Request) { + r.addAuthHeader(req) + r.addOtherHeader(req) +} + func (r *Request) addAuthHeader(req *http.Request) { req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.config.ApiToken)) req.Header.Add("Content-Type", "application/json") } -// toReader try to convert anything to io.Reader. +func (r *Request) addOtherHeader(req *http.Request) { + for k, vs := range r.Header { + for _, v := range vs { + req.Header.Add(k, v) + } + } +} + +// toReader tries to convert anything to io.Reader. // Can return nil, which means empty, i.e. empty request body func toReader(v any) (io.Reader, error) { var reader io.Reader diff --git a/client/internal/retry/do.go b/client/internal/retry/do.go index 33b80f46..c8c0b8d5 100644 --- a/client/internal/retry/do.go +++ b/client/internal/retry/do.go @@ -23,12 +23,12 @@ type Options struct { Logger *log.Logger - EarlyExitOnError bool // EarlyExitOnError will cause Retry to return immediately if error is returned from Func; if false, it will only return when max retries exceeded, which errors are combined the returned together. + EarlyExitOnError bool // EarlyExitOnError will cause Retry to return immediately if error is returned from Func; if false, it will only return when max retries exceeded, which errors are combined and returned together. } // Func is the retryable function for retrying. -// ok: whether to to stop -// err: if not nil, stop retrying +// ok: whether ok to stop +// err: if not nil, stop retrying and return that error type Func func() (ok bool, err error) const ( @@ -60,6 +60,16 @@ func NewOptionsWithLogger(logger *log.Logger) *Options { ) } +func NewOptionsWithLoggerAndRetries(logger *log.Logger, retries int) *Options { + return NewOptions( + logger, + DefaultTimeout, + DefaultDelay, + retries, + DefaultEarlyExitOnError, + ) +} + func NewOptions(logger *log.Logger, timeout time.Duration, delay time.Duration, retries int, earlyExitOnError bool) *Options { return &Options{ Timeout: timeout, diff --git a/client/internal/sliceutil/map.go b/client/internal/sliceutil/map.go new file mode 100644 index 00000000..9ae9c6d7 --- /dev/null +++ b/client/internal/sliceutil/map.go @@ -0,0 +1,25 @@ +package sliceutil + +// MapWithError takes a slice and a function, apply the function to each element in the slice, and return the result. +// It takes a function that can return error, when it returns error, no subsequent element is mapped +// and the slice mapped so far is returned together with the error. +func MapWithError[T any, V any](sliceT []T, mapFunc func(T) (V, error)) ([]V, error) { + sliceV := make([]V, len(sliceT)) + for i, item := range sliceT { + mapped, err := mapFunc(item) + if err != nil { + return sliceV, err + } + sliceV[i] = mapped + } + return sliceV, nil +} + +// Map takes a slice and a function, apply the function to each element in the slice, and return the mapped slice. +func Map[T any, V any](sliceT []T, mapFunc func(T) V) []V { + sliceV := make([]V, len(sliceT)) + for i, item := range sliceT { + sliceV[i] = mapFunc(item) + } + return sliceV +} diff --git a/client/internal/statemachine/error.go b/client/internal/statemachine/error.go new file mode 100644 index 00000000..5f694559 --- /dev/null +++ b/client/internal/statemachine/error.go @@ -0,0 +1,11 @@ +package statemachine + +import ( + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/cdo" +) + +var ( + StateMachineNotFoundError = fmt.Errorf("statemachine instance not found") + MoreThanOneStateMachineRunningError = fmt.Errorf("multiple running instance found, this is an expected error, please report this issue at: %s", cdo.TerraformProviderCDOIssuesUrl) +) diff --git a/client/internal/statemachine/fixture_test.go b/client/internal/statemachine/fixture_test.go new file mode 100644 index 00000000..62c4bca7 --- /dev/null +++ b/client/internal/statemachine/fixture_test.go @@ -0,0 +1,12 @@ +package statemachine_test + +import "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/statemachine" + +const ( + baseUrl = "https://unit-test.cdo.cisco.com" + deviceUid = "unit-test-device-uid" +) + +var ( + validReadStateMachineOutput = statemachine.NewReadInstanceByDeviceUidOutputBuilder().Build() +) diff --git a/client/internal/statemachine/readinstance_by_deviceuid.go b/client/internal/statemachine/readinstance_by_deviceuid.go new file mode 100644 index 00000000..53bb7c93 --- /dev/null +++ b/client/internal/statemachine/readinstance_by_deviceuid.go @@ -0,0 +1,47 @@ +package statemachine + +import ( + "context" + "fmt" + "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/statemachine" +) + +type ReadInstanceByDeviceUidInput struct { + Uid string // Uid of the device that runs the state machine +} + +func NewReadInstanceByDeviceUidInput(deviceUid string) ReadInstanceByDeviceUidInput { + return ReadInstanceByDeviceUidInput{ + Uid: deviceUid, + } +} + +type ReadInstanceByDeviceUidOutput = statemachine.Instance + +var NewReadInstanceByDeviceUidOutputBuilder = statemachine.NewInstanceBuilder + +func ReadInstanceByDeviceUid(ctx context.Context, client http.Client, readInp ReadInstanceByDeviceUidInput) (*ReadInstanceByDeviceUidOutput, error) { + + readUrl := url.ReadStateMachineInstance(client.BaseUrl()) + req := client.NewGet(ctx, readUrl) + req.QueryParams.Add("limit", "1") + req.QueryParams.Add("q", fmt.Sprintf("objectReference.uid:%s", readInp.Uid)) + req.QueryParams.Add("sort", "lastActiveDate:desc") + + var readRes []ReadInstanceByDeviceUidOutput + if err := req.Send(&readRes); err != nil { + return nil, err + } + if len(readRes) == 0 { + return nil, StateMachineNotFoundError + } + + // TODO: this can happen, no idea why, limit 1 does not seems to work + //if len(readRes) > 1 { + // return nil, MoreThanOneStateMachineRunningError + //} + + return &readRes[0], nil +} diff --git a/client/internal/statemachine/readinstance_by_deviceuid_test.go b/client/internal/statemachine/readinstance_by_deviceuid_test.go new file mode 100644 index 00000000..ce59e3b6 --- /dev/null +++ b/client/internal/statemachine/readinstance_by_deviceuid_test.go @@ -0,0 +1,75 @@ +package statemachine_test + +import ( + "context" + internalHttp "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/statemachine" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "net/http" + "testing" + "time" +) + +func TestReadInstanceByDeviceUid(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + testCases := []struct { + testName string + input statemachine.ReadInstanceByDeviceUidInput + setupFunc func() + assertFunc func(output *statemachine.ReadInstanceByDeviceUidOutput, err error, t *testing.T) + }{ + { + testName: "successfully read state machine instance by uid", + input: statemachine.NewReadInstanceByDeviceUidInput(deviceUid), + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadStateMachineInstance(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, []statemachine.ReadInstanceByDeviceUidOutput{ + validReadStateMachineOutput, + }), + ) + }, + assertFunc: func(output *statemachine.ReadInstanceByDeviceUidOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, validReadStateMachineOutput, *output) + }, + }, + { + testName: "error when read state machine instance by uid error", + input: statemachine.NewReadInstanceByDeviceUidInput(deviceUid), + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadStateMachineInstance(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + }, + assertFunc: func(output *statemachine.ReadInstanceByDeviceUidOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := statemachine.ReadInstanceByDeviceUid( + context.Background(), + *internalHttp.MustNewWithConfig(baseUrl, "a_valid_token", 0, 0, time.Minute), + testCase.input, + ) + + testCase.assertFunc(output, err, t) + }) + } +} diff --git a/client/internal/statemachine/retry.go b/client/internal/statemachine/retry.go new file mode 100644 index 00000000..8f375dce --- /dev/null +++ b/client/internal/statemachine/retry.go @@ -0,0 +1,30 @@ +package statemachine + +import ( + "context" + "errors" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/retry" +) + +// UntilStarted keeps polling until it finds a state machine error +func UntilStarted(ctx context.Context, client http.Client, deviceUid string, stateMachineIdentifier string) retry.Func { + return func() (ok bool, err error) { + res, err := ReadInstanceByDeviceUid(ctx, client, NewReadInstanceByDeviceUidInput(deviceUid)) + if err != nil { + if errors.Is(err, StateMachineNotFoundError) { + // state machine not found, probably because we are calling too early, and it has not started yet, continue polling + return false, nil + } + // other error, not valid + return false, err + } + if res.StateMachineIdentifier == stateMachineIdentifier { + // found it, done! + // we do not need to check it is running properly, because, as the function name suggest, as long as it has started, we do not care + return true, nil + } + // other state machine is running, continue polling + return false, nil + } +} diff --git a/client/internal/url/url.go b/client/internal/url/url.go index 87e5a61b..a86928e0 100644 --- a/client/internal/url/url.go +++ b/client/internal/url/url.go @@ -10,11 +10,11 @@ func ReadDevice(baseUrl string, uid string) string { return fmt.Sprintf("%s/aegis/rest/v1/services/targets/devices/%s", baseUrl, uid) } -func ReadDeviceByNameAndDeviceType(baseUrl string, deviceName string, deviceType string) string { +func ReadDeviceByNameAndType(baseUrl string, deviceName string, deviceType devicetype.Type) string { return fmt.Sprintf("%s/aegis/rest/v1/services/targets/devices?q=name:%s+AND+deviceType:%s", baseUrl, deviceName, deviceType) } -func ReadAllDevicesByType(baseUrl string, deviceType devicetype.Type) string { - return fmt.Sprintf("%s/aegis/rest/v1/services/targets/devices?q=deviceType:%s", baseUrl, deviceType) +func ReadAllDevicesByType(baseUrl string) string { + return fmt.Sprintf("%s/aegis/rest/v1/services/targets/devices", baseUrl) } func CreateDevice(baseUrl string) string { @@ -73,6 +73,22 @@ func ReadSmartLicense(baseUrl string) string { return fmt.Sprintf("%s/fmc/api/fmc_platform/v1/license/smartlicenses", baseUrl) } -func ReadAccessPolicies(baseUrl string, domainUid string, limit int) string { - return fmt.Sprintf("%s/fmc/api/fmc_config/v1/domain/%s/policy/accesspolicies?limit=%d", baseUrl, domainUid, limit) +func ReadAccessPolicies(baseUrl string, domainUid string) string { + return fmt.Sprintf("%s/fmc/api/fmc_config/v1/domain/%s/policy/accesspolicies", baseUrl, domainUid) +} + +func UpdateSpecificCloudFtd(baseUrl string, ftdSpecificUid string) string { + return fmt.Sprintf("%s/aegis/rest/v1/services/firepower/ftds/%s", baseUrl, ftdSpecificUid) +} + +func UpdateFmcAppliance(baseUrl string, fmcSpecificUid string) string { + return fmt.Sprintf("%s/aegis/rest/v1/services/fmc/appliance/%s", baseUrl, fmcSpecificUid) +} + +func ReadStateMachineInstance(baseUrl string) string { + return fmt.Sprintf("%s/aegis/rest/v1/services/state-machines/instances", baseUrl) +} + +func ReadFmcDomainInfo(fmcHost string) string { + return fmt.Sprintf("https://%s/api/fmc_platform/v1/info/domain", fmcHost) } diff --git a/client/model/accesspolicies/accesspolicies.go b/client/model/accesspolicies/accesspolicies.go deleted file mode 100644 index 4f03e0eb..00000000 --- a/client/model/accesspolicies/accesspolicies.go +++ /dev/null @@ -1,67 +0,0 @@ -package accesspolicies - -type AccessPolicies struct { - Items Items `json:"items"` - Links Links `json:"links"` - Paging Paging `json:"paging"` -} - -func New(items Items, links Links, paging Paging) AccessPolicies { - return AccessPolicies{ - Items: items, - Links: links, - Paging: paging, - } -} - -type Items struct { - Items []Item `json:"items"` -} - -func NewItems(items ...Item) Items { - return Items{ - Items: items, - } -} - -type Item struct { - Links Links `json:"links"` - Id string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` -} - -func NewItem(id, name, type_ string, links Links) Item { - return Item{ - Id: id, - Name: name, - Type: type_, - Links: links, - } -} - -type Paging struct { - Count int `json:"count"` - Offset int `json:"offset"` - Limit int `json:"limit"` - Pages int `json:"pages"` -} - -func NewPaging(count, offset, limit, pages int) Paging { - return Paging{ - Count: count, - Offset: offset, - Limit: limit, - Pages: pages, - } -} - -type Links struct { - Self string `json:"self"` -} - -func NewLinks(self string) Links { - return Links{ - Self: self, - } -} diff --git a/client/model/cloudfmc/accesspolicies/accesspolicies.go b/client/model/cloudfmc/accesspolicies/accesspolicies.go new file mode 100644 index 00000000..d385b56c --- /dev/null +++ b/client/model/cloudfmc/accesspolicies/accesspolicies.go @@ -0,0 +1,49 @@ +package accesspolicies + +import "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/internal" + +type AccessPolicies struct { + Items []Item `json:"items"` + Links Links `json:"links"` + Paging Paging `json:"paging"` +} + +func New(items []Item, links Links, paging Paging) AccessPolicies { + return AccessPolicies{ + Items: items, + Links: links, + Paging: paging, + } +} + +// Find return the access policy item with the given name, second return value ok indicate whether the item is found. +func (policies *AccessPolicies) Find(name string) (item Item, ok bool) { + for _, policy := range policies.Items { + if policy.Name == name { + return policy, true + } + } + return Item{}, false +} + +type Item struct { + Links Links `json:"links"` + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +func NewItem(id, name, type_ string, links Links) Item { + return Item{ + Id: id, + Name: name, + Type: type_, + Links: links, + } +} + +type Links = internal.Links +type Paging = internal.Paging + +var NewLinks = internal.NewLinks +var NewPaging = internal.NewPaging diff --git a/client/model/cloudfmc/accesspolicies/builder.go b/client/model/cloudfmc/accesspolicies/builder.go new file mode 100644 index 00000000..d9064644 --- /dev/null +++ b/client/model/cloudfmc/accesspolicies/builder.go @@ -0,0 +1,30 @@ +package accesspolicies + +type Builder struct { + accessPolicies *AccessPolicies +} + +func NewAccessPoliciesBuilder() *Builder { + accessPolicies := &AccessPolicies{} + b := &Builder{accessPolicies: accessPolicies} + return b +} + +func (b *Builder) Items(items []Item) *Builder { + b.accessPolicies.Items = items + return b +} + +func (b *Builder) Links(links Links) *Builder { + b.accessPolicies.Links = links + return b +} + +func (b *Builder) Paging(paging Paging) *Builder { + b.accessPolicies.Paging = paging + return b +} + +func (b *Builder) Build() AccessPolicies { + return *b.accessPolicies +} diff --git a/client/model/cloudfmc/fmcdomain/builder.go b/client/model/cloudfmc/fmcdomain/builder.go new file mode 100644 index 00000000..d64f2cbb --- /dev/null +++ b/client/model/cloudfmc/fmcdomain/builder.go @@ -0,0 +1,30 @@ +package fmcdomain + +type InfoBuilder struct { + info *Info +} + +func NewInfoBuilder() *InfoBuilder { + info := &Info{} + b := &InfoBuilder{info: info} + return b +} + +func (b *InfoBuilder) Links(links Links) *InfoBuilder { + b.info.Links = links + return b +} + +func (b *InfoBuilder) Paging(paging Paging) *InfoBuilder { + b.info.Paging = paging + return b +} + +func (b *InfoBuilder) Items(items []Item) *InfoBuilder { + b.info.Items = items + return b +} + +func (b *InfoBuilder) Build() Info { + return *b.info +} diff --git a/client/model/cloudfmc/fmcdomain/info.go b/client/model/cloudfmc/fmcdomain/info.go new file mode 100644 index 00000000..96236b92 --- /dev/null +++ b/client/model/cloudfmc/fmcdomain/info.go @@ -0,0 +1,39 @@ +package fmcdomain + +import ( + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/internal" +) + +type Info struct { + Links Links `json:"links"` + Paging Paging `json:"paging"` + Items []Item `json:"items"` +} + +func NewInfo(links Links, paging Paging, items []Item) Info { + return Info{ + Links: links, + Paging: paging, + Items: items, + } +} + +type Item struct { + Uuid string `json:"uuid"` + Name string `json:"name"` + Type string `json:"type"` +} + +func NewItem(uuid, name, type_ string) Item { + return Item{ + Uuid: uuid, + Name: name, + Type: type_, + } +} + +type Links = internal.Links +type Paging = internal.Paging + +var NewLinks = internal.NewLinks +var NewPaging = internal.NewPaging diff --git a/client/model/cloudfmc/internal/common.go b/client/model/cloudfmc/internal/common.go new file mode 100644 index 00000000..8a5bfd80 --- /dev/null +++ b/client/model/cloudfmc/internal/common.go @@ -0,0 +1,32 @@ +package internal + +type Paging struct { + Count int `json:"count"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Pages int `json:"pages"` +} + +func NewPaging( + count int, + offset int, + limit int, + pages int, +) Paging { + return Paging{ + Count: count, + Offset: offset, + Limit: limit, + Pages: pages, + } +} + +type Links struct { + Self string `json:"self"` +} + +func NewLinks(self string) Links { + return Links{ + Self: self, + } +} diff --git a/client/model/smartlicense/smartlicense.go b/client/model/cloudfmc/smartlicense/smartlicense.go similarity index 62% rename from client/model/smartlicense/smartlicense.go rename to client/model/cloudfmc/smartlicense/smartlicense.go index 40829d36..8127f725 100644 --- a/client/model/smartlicense/smartlicense.go +++ b/client/model/cloudfmc/smartlicense/smartlicense.go @@ -1,12 +1,14 @@ package smartlicense +import "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/internal" + type SmartLicense struct { - Items Items `json:"items"` + Items []Item `json:"items"` Links Links `json:"links"` Paging Paging `json:"paging"` } -func NewSmartLicense(items Items, links Links, paging Paging) SmartLicense { +func NewSmartLicense(items []Item, links Links, paging Paging) SmartLicense { return SmartLicense{ Items: items, Links: links, @@ -14,16 +16,6 @@ func NewSmartLicense(items Items, links Links, paging Paging) SmartLicense { } } -type Items struct { - Items []Item `json:"items"` -} - -func NewItems(items ...Item) Items { - return Items{ - Items: items, - } -} - type Item struct { Metadata Metadata `json:"metadata"` RegStatus string `json:"regStatus"` @@ -62,33 +54,8 @@ func NewMetadata( } } -type Paging struct { - Count int `json:"count"` - Offset int `json:"offset"` - Limit int `json:"limit"` - Pages int `json:"pages"` -} - -func NewPaging( - count int, - offset int, - limit int, - pages int, -) Paging { - return Paging{ - Count: count, - Offset: offset, - Limit: limit, - Pages: pages, - } -} - -type Links struct { - Self string `json:"self"` -} +type Links = internal.Links +type Paging = internal.Paging -func NewLinks(self string) Links { - return Links{ - Self: self, - } -} +var NewLinks = internal.NewLinks +var NewPaging = internal.NewPaging diff --git a/client/model/devicetype/devicetype.go b/client/model/devicetype/devicetype.go index 07da2787..53ee6221 100644 --- a/client/model/devicetype/devicetype.go +++ b/client/model/devicetype/devicetype.go @@ -3,7 +3,8 @@ package devicetype type Type string const ( - Asa Type = "ASA" - Ios Type = "IOS" - Cdfmc Type = "FMCE" + Asa Type = "ASA" + Ios Type = "IOS" + CloudFmc Type = "FMCE" + CloudFtd Type = "FTDC" ) diff --git a/client/model/ftd/license/license.go b/client/model/ftd/license/license.go new file mode 100644 index 00000000..71a9e13d --- /dev/null +++ b/client/model/ftd/license/license.go @@ -0,0 +1,63 @@ +package license + +import ( + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/sliceutil" + "strings" +) + +type Type string + +// https://www.cisco.com/c/en/us/td/docs/security/firepower/70/fdm/fptd-fdm-config-guide-700/fptd-fdm-license.html +const ( + Base Type = "BASE" + Carrier Type = "CARRIER" + Threat Type = "THREAT" + Malware Type = "MALWARE" + URLFilter Type = "URLFilter" +) + +var licenseMap = map[string]Type{ + "BASE": Base, + "CARRIER": Carrier, + "THREAT": Threat, + "MALWARE": Malware, + "URLFilter": URLFilter, +} + +func MustParse(name string) Type { + l, ok := licenseMap[name] + if !ok { + panic(fmt.Errorf("FTD License of name: \"%s\" not found", name)) + } + return l +} + +func Deserialize(name string) (Type, error) { + l, ok := licenseMap[name] + if !ok { + return "", fmt.Errorf("FTD License of name: \"%s\" not found, should be one of: %+v", name, licenseMap) + } + return l, nil +} + +func DeserializeAll(names string) ([]Type, error) { + licenseStrs := strings.Split(names, ",") + licenses := make([]Type, len(licenseStrs)) + for i, name := range licenseStrs { + t, err := Deserialize(name) + if err != nil { + return nil, err + } + licenses[i] = t + } + return licenses, nil +} + +func SerializeAll(licenses []Type) string { + return strings.Join(sliceutil.Map(licenses, func(l Type) string { return Serialize(l) }), ",") +} + +func Serialize(license Type) string { + return string(license) +} diff --git a/client/model/ftd/tier/tier.go b/client/model/ftd/tier/tier.go new file mode 100644 index 00000000..7afa6132 --- /dev/null +++ b/client/model/ftd/tier/tier.go @@ -0,0 +1,35 @@ +package tier + +import "fmt" + +type Type string + +// https://www.cisco.com/c/en/us/td/docs/security/firepower/70/fdm/fptd-fdm-config-guide-700/fptd-fdm-license.html +const ( + FTDv5 Type = "FTDv5" + FTDv10 Type = "FTDv10" + FTDv20 Type = "FTDv20" + FTDv30 Type = "FTDv30" + FTDv50 Type = "FTDv50" + FTDv100 Type = "FTDv100" + FTDv Type = "FTDv" +) + +var AllTiers = []Type{ + FTDv5, + FTDv10, + FTDv20, + FTDv30, + FTDv50, + FTDv100, + FTDv, +} + +func Parse(name string) (Type, error) { + for _, tier := range AllTiers { + if string(tier) == name { + return tier, nil + } + } + return "", fmt.Errorf("FTD Performance Tier of name: \"%s\" not found", name) +} diff --git a/client/model/statemachine/action.go b/client/model/statemachine/action.go new file mode 100644 index 00000000..ca1629fc --- /dev/null +++ b/client/model/statemachine/action.go @@ -0,0 +1,17 @@ +package statemachine + +type Action struct { + ActionIdentifier string `json:"actionIdentifier"` + EndDate int `json:"endDate"` + EndMessage string `json:"endMessage"` + EndMessageString string `json:"endMessageString"` + EndState string `json:"endState"` + Identifier string `json:"identifier"` + StartDate int `json:"startDate"` + StartState string `json:"startState"` + + CompleteStackTrace *string `json:"completeStackTrace,omitempty"` + ErrorCode *string `json:"errorCode,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` + FurtherDetails *string `json:"furtherDetails,omitempty"` +} diff --git a/client/model/statemachine/details.go b/client/model/statemachine/details.go new file mode 100644 index 00000000..b39e1b9a --- /dev/null +++ b/client/model/statemachine/details.go @@ -0,0 +1,12 @@ +package statemachine + +type Details struct { + CurrentDataRequirements *string `json:"currentDataRequirements,omitempty"` + Identifier string `json:"identifier"` + LastActiveDate int `json:"lastActiveDate"` + LastStep string `json:"lastStep"` + Priority string `json:"priority"` + StateMachineInstanceCondition string `json:"stateMachineInstanceCondition"` + StateMachineType string `json:"stateMachineType"` + Uid string `json:"uid"` +} diff --git a/client/model/statemachine/hook.go b/client/model/statemachine/hook.go new file mode 100644 index 00000000..d4cd634f --- /dev/null +++ b/client/model/statemachine/hook.go @@ -0,0 +1,12 @@ +package statemachine + +type Hook struct { + EndDate int `json:"endDate"` + EndMessage string `json:"endMessage"` + HookIdentifier string `json:"hookIdentifier"` + Identifier string `json:"identifier"` + StartDate int `json:"startDate"` + + CompleteStackTrace *string `json:"completeStackTrace,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` +} diff --git a/client/model/statemachine/instance.go b/client/model/statemachine/instance.go new file mode 100644 index 00000000..1a53b3f7 --- /dev/null +++ b/client/model/statemachine/instance.go @@ -0,0 +1,114 @@ +package statemachine + +type Instance struct { + Actions []Action `json:"actions"` + ActiveStateMachineContext map[string]string `json:"activeStateMachineContext"` + AfterHooks []Hook `json:"afterHooks"` + BeforeHooks []Hook `json:"beforeHooks"` + CreatedDate int `json:"createdDate"` + CurrentState string `json:"currentState"` + EndDate int `json:"endDate"` + HasErrors bool `json:"hasErrors"` + ObjectReference ObjectReference `json:"objectReference"` + StateMachineDetails Details `json:"stateMachineDetails"` + StateMachineIdentifier string `json:"stateMachineIdentifier"` + StateMachineInstanceCondition string `json:"stateMachineInstanceCondition"` + StateMachinePriority string `json:"stateMachinePriority"` + StateMachineType string `json:"stateMachineType"` + Status string `json:"status"` + Uid string `json:"uid"` +} + +type InstanceBuilder struct { + instance *Instance +} + +func NewInstanceBuilder() *InstanceBuilder { + instance := &Instance{} + b := &InstanceBuilder{instance: instance} + return b +} + +func (b *InstanceBuilder) Actions(actions []Action) *InstanceBuilder { + b.instance.Actions = actions + return b +} + +func (b *InstanceBuilder) ActiveStateMachineContext(activeStateMachineContext map[string]string) *InstanceBuilder { + b.instance.ActiveStateMachineContext = activeStateMachineContext + return b +} + +func (b *InstanceBuilder) AfterHooks(afterHooks []Hook) *InstanceBuilder { + b.instance.AfterHooks = afterHooks + return b +} + +func (b *InstanceBuilder) BeforeHooks(beforeHooks []Hook) *InstanceBuilder { + b.instance.BeforeHooks = beforeHooks + return b +} + +func (b *InstanceBuilder) CreatedDate(createdDate int) *InstanceBuilder { + b.instance.CreatedDate = createdDate + return b +} + +func (b *InstanceBuilder) CurrentState(currentState string) *InstanceBuilder { + b.instance.CurrentState = currentState + return b +} + +func (b *InstanceBuilder) EndDate(endDate int) *InstanceBuilder { + b.instance.EndDate = endDate + return b +} + +func (b *InstanceBuilder) HasErrors(hasErrors bool) *InstanceBuilder { + b.instance.HasErrors = hasErrors + return b +} + +func (b *InstanceBuilder) ObjectReference(objectReference ObjectReference) *InstanceBuilder { + b.instance.ObjectReference = objectReference + return b +} + +func (b *InstanceBuilder) StateMachineDetails(stateMachineDetails Details) *InstanceBuilder { + b.instance.StateMachineDetails = stateMachineDetails + return b +} + +func (b *InstanceBuilder) StateMachineIdentifier(stateMachineIdentifier string) *InstanceBuilder { + b.instance.StateMachineIdentifier = stateMachineIdentifier + return b +} + +func (b *InstanceBuilder) StateMachineInstanceCondition(stateMachineInstanceCondition string) *InstanceBuilder { + b.instance.StateMachineInstanceCondition = stateMachineInstanceCondition + return b +} + +func (b *InstanceBuilder) StateMachinePriority(stateMachinePriority string) *InstanceBuilder { + b.instance.StateMachinePriority = stateMachinePriority + return b +} + +func (b *InstanceBuilder) StateMachineType(stateMachineType string) *InstanceBuilder { + b.instance.StateMachineType = stateMachineType + return b +} + +func (b *InstanceBuilder) Status(status string) *InstanceBuilder { + b.instance.Status = status + return b +} + +func (b *InstanceBuilder) Uid(uid string) *InstanceBuilder { + b.instance.Uid = uid + return b +} + +func (b *InstanceBuilder) Build() Instance { + return *b.instance +} diff --git a/client/model/statemachine/objectreference.go b/client/model/statemachine/objectreference.go new file mode 100644 index 00000000..b52ddea5 --- /dev/null +++ b/client/model/statemachine/objectreference.go @@ -0,0 +1,7 @@ +package statemachine + +type ObjectReference struct { + Namespace string `json:"namespace"` + Type string `json:"type"` + Uid string `json:"uid"` +} diff --git a/provider/examples/resources/ftd/ftd_example.tf b/provider/examples/resources/ftd/ftd_example.tf new file mode 100644 index 00000000..68bfc355 --- /dev/null +++ b/provider/examples/resources/ftd/ftd_example.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + cdo = { + source = "hashicorp.com/CiscoDevnet/cdo" + } + } +} + +provider "cdo" { + base_url = "" + api_token = "" +} + +resource "cdo_ftd_device" "test" { + name = "test-weilue-ftd-9" + access_policy_name = "Default Access Control Policy" + performance_tier = "FTDv5" + virtual = true + licenses = ["BASE"] +} \ No newline at end of file diff --git a/provider/internal/device/ftd/operation.go b/provider/internal/device/ftd/operation.go new file mode 100644 index 00000000..4474f4e9 --- /dev/null +++ b/provider/internal/device/ftd/operation.go @@ -0,0 +1,103 @@ +package ftd + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/license" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/tier" + "github.com/CiscoDevnet/terraform-provider-cdo/internal/util" + "github.com/CiscoDevnet/terraform-provider-cdo/internal/util/sliceutil" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func Read(ctx context.Context, resource *Resource, stateData *ResourceModel) error { + + // do read + inp := cloudftd.NewReadByNameInput(stateData.Name.ValueString()) + res, err := resource.client.ReadCloudFtdByName(ctx, inp) + if err != nil { + return err + } + + // map return struct to model + stateData.ID = types.StringValue(res.Uid) + stateData.Name = types.StringValue(res.Name) + stateData.AccessPolicyName = types.StringValue(res.Metadata.AccessPolicyName) + stateData.AccessPolicyUid = types.StringValue(res.Metadata.AccessPolicyUid) + stateData.Virtual = types.BoolValue(res.Metadata.PerformanceTier != nil) + stateData.Licenses = util.GoStringSliceToTFStringList(sliceutil.Map(res.Metadata.LicenseCaps, func(l license.Type) string { return string(l) })) + if res.Metadata.PerformanceTier != nil { // nil means physical cloudftd + stateData.PerformanceTier = types.StringValue(string(*res.Metadata.PerformanceTier)) + } + stateData.GeneratedCommand = types.StringValue(res.Metadata.GeneratedCommand) + + return nil +} + +func Create(ctx context.Context, resource *Resource, planData *ResourceModel) error { + + // do create + var performanceTier *tier.Type = nil + if planData.PerformanceTier.ValueString() != "" { + t, err := tier.Parse(planData.PerformanceTier.ValueString()) + performanceTier = &t + if err != nil { + return err + } + } + + licensesGoList := util.TFStringListToGoStringList(planData.Licenses) + licenses, err := sliceutil.MapWithError(licensesGoList, func(s string) (license.Type, error) { return license.Deserialize(s) }) + if err != nil { + return err + } + createInp := cloudftd.NewCreateInput( + planData.Name.ValueString(), + planData.AccessPolicyName.ValueString(), + performanceTier, + planData.Virtual.ValueBool(), + licenses, + ) + res, err := resource.client.CreateCloudFtd(ctx, createInp) + if err != nil { + return err + } + + // map return struct to model + planData.ID = types.StringValue(res.Uid) + planData.Name = types.StringValue(res.Name) + planData.AccessPolicyName = types.StringValue(res.Metadata.AccessPolicyName) + planData.AccessPolicyUid = types.StringValue(res.Metadata.AccessPolicyUid) + planData.Virtual = types.BoolValue(res.Metadata.PerformanceTier != nil) + planData.Licenses = util.GoStringSliceToTFStringList(sliceutil.Map(res.Metadata.LicenseCaps, func(l license.Type) string { return string(l) })) + if res.Metadata.PerformanceTier != nil { // nil means physical cloud ftd + planData.PerformanceTier = types.StringValue(string(*res.Metadata.PerformanceTier)) + } + planData.GeneratedCommand = types.StringValue(res.Metadata.GeneratedCommand) + + return nil +} + +func Update(ctx context.Context, resource *Resource, planData *ResourceModel, stateData *ResourceModel) error { + + // do update + inp := cloudftd.NewUpdateInput(planData.ID.ValueString(), planData.Name.ValueString()) + res, err := resource.client.UpdateCloudFtd(ctx, inp) + if err != nil { + return err + } + + // map return struct to model + stateData.Name = types.StringValue(res.Name) + + return nil +} + +func Delete(ctx context.Context, resource *Resource, stateData *ResourceModel) error { + + // do delete + inp := cloudftd.NewDeleteInput(stateData.ID.ValueString()) + _, err := resource.client.DeleteCloudFtd(ctx, inp) + + return err +} diff --git a/provider/internal/device/ftd/resource.go b/provider/internal/device/ftd/resource.go new file mode 100644 index 00000000..fbdcce4b --- /dev/null +++ b/provider/internal/device/ftd/resource.go @@ -0,0 +1,215 @@ +package ftd + +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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "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"` + AccessPolicyName types.String `tfsdk:"access_policy_name"` + PerformanceTier types.String `tfsdk:"performance_tier"` + Virtual types.Bool `tfsdk:"virtual"` + Licenses []types.String `tfsdk:"licenses"` + + AccessPolicyUid types.String `tfsdk:"access_policy_id"` + GeneratedCommand types.String `tfsdk:"generated_command"` +} + +func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_ftd_device" // TODO: _cloud_ftd_device ? +} + +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Provides an Firepower Threat Defense device resource. This allows FTD to be onboarded, updated, and deleted on CDO.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Unique identifier of the device. This is a UUID and will be automatically generated when the device is created.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "A human-readable name for the Firewall Threat Defense (FTD). This should be unique among FTDs", + Required: true, + }, + "access_policy_name": schema.StringAttribute{ + MarkdownDescription: "The name of the Cloud FMC access policy that will be used by the FTD", + Required: true, + // TODO: make this optional, and use default access policy when not given + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), // TODO: can we change access policy after it is created? + }, + }, + "performance_tier": schema.StringAttribute{ + MarkdownDescription: "The performance tier of the virtual FTD, if virtual is set to false, this field is ignored.", + Optional: true, + // TODO: validator for performance tier, check valid performance tier is given + // TODO: ignore changes in this field when virtual is false + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), // TODO: can we change performance tier after it is created? + }, + }, + "virtual": schema.BoolAttribute{ + MarkdownDescription: "Whether this FTD is virtual. If false, performance_tier is ignored", + Required: true, + // TODO: can we change this after created? + // TODO: default value false + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + "licenses": schema.ListAttribute{ + ElementType: types.StringType, + MarkdownDescription: "Comma separated list of licenses of this FTD, it must at least contains the \"BASE\" license.", + Required: true, + // TODO: make this not required, when not given, use BASE license + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), // TODO: can we modify license after FTD is created? + // TODO: always sort the licenses so that it is the same order, so that it does not change when order of licenses changes + }, + // TODO: validate the licenses are valid input. + }, + "generated_command": schema.StringAttribute{ + MarkdownDescription: "The command to run in the FTD to register itself with Cloud FMC.", + Computed: true, + }, + "access_policy_id": schema.StringAttribute{ + MarkdownDescription: "The id of the access policy used by this FTD.", + Computed: 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 FTD 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 FTD resource", err.Error()) + return + } + + // 3. save data into terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &stateData)...) + tflog.Trace(ctx, "read FTD resource done") +} + +func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, res *resource.CreateResponse) { + tflog.Trace(ctx, "create FTD 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 FTD resource", err.Error()) + return + } + + // 3. fill terraform state using model data + res.Diagnostics.Append(res.State.Set(ctx, &planData)...) + tflog.Trace(ctx, "create FTD resource done") +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { + tflog.Trace(ctx, "update FTD 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 FTD resource", err.Error()) + return + } + + // 3. update terraform state with updated state data + res.Diagnostics.Append(res.State.Set(ctx, &stateData)...) + tflog.Trace(ctx, "update FTD resource done") +} + +func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { + tflog.Trace(ctx, "delete FTD 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 FTD 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/device/ftd/resource_test.go b/provider/internal/device/ftd/resource_test.go new file mode 100644 index 00000000..c39590b7 --- /dev/null +++ b/provider/internal/device/ftd/resource_test.go @@ -0,0 +1,84 @@ +package ftd_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/CiscoDevnet/terraform-provider-cdo/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +type ResourceType struct { + ID string + Name string + AccessPolicyName string + PerformanceTier string + Virtual string + Licenses string + AccessPolicyUid string + GeneratedCommand string +} + +const ResourceTemplate = ` +resource "cdo_ftd_device" "test" { + name = "{{.Name}}" + access_policy_name = "{{.AccessPolicyName}}" + performance_tier = "{{.PerformanceTier}}" + virtual = "{{.Virtual}}" + licenses = {{.Licenses}} +}` + +var testResource = ResourceType{ + Name: "ci-test-cloudftd-10", + AccessPolicyName: "Default Access Control Policy", + PerformanceTier: "FTDv5", + Virtual: "false", + Licenses: "[\"BASE\"]", + GeneratedCommand: "configure manager add terraform-provider-cdo.app.staging.cdo.cisco.com LvWGkKjYNrqZlYbz2JGZqbD0ibDuxlSp h2zTtFTvwxgDIbI9pGshHNWrJGDT0jzC terraform-provider-cdo.app.staging.cdo.cisco.com", +} +var testResourceConfig = acctest.MustParseTemplate(ResourceTemplate, testResource) + +var testResource_NewName = acctest.MustOverrideFields(testResource, map[string]any{ + "Name": "ci-test-cloudftd-new-name", +}) + +var testResourceConfig_NewName = acctest.MustParseTemplate(ResourceTemplate, testResource_NewName) + +func TestAccFtdResource(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: acctest.PreCheckFunc(t), + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: acctest.ProviderConfig() + testResourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("cdo_ftd_device.test", "name", testResource.Name), + resource.TestCheckResourceAttr("cdo_ftd_device.test", "access_policy_name", testResource.AccessPolicyName), + resource.TestCheckResourceAttr("cdo_ftd_device.test", "performance_tier", testResource.PerformanceTier), + resource.TestCheckResourceAttr("cdo_ftd_device.test", "virtual", testResource.Virtual), + resource.TestCheckResourceAttrSet("cdo_ftd_device.test", "licenses.0"), + resource.TestCheckResourceAttr("cdo_ftd_device.test", "licenses.#", "1"), + resource.TestCheckResourceAttr("cdo_ftd_device.test", "access_policy_name", testResource.AccessPolicyName), + resource.TestCheckResourceAttrWith("cdo_ftd_device.test", "generated_command", func(value string) error { + ok := strings.HasPrefix(value, "configure manager add") + if !ok { + return fmt.Errorf("generated command should starts with \"configure manager add\", but it was \"%s\"", value) + } + return nil + }), + ), + }, + // Update and Read testing + { + Config: acctest.ProviderConfig() + testResourceConfig_NewName, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("cdo_ftd_device.test", "name", testResource_NewName.Name), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/provider/internal/provider/provider.go b/provider/internal/provider/provider.go index 7c0ce4ac..9e566e2e 100644 --- a/provider/internal/provider/provider.go +++ b/provider/internal/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "github.com/CiscoDevnet/terraform-provider-cdo/internal/connector" + "github.com/CiscoDevnet/terraform-provider-cdo/internal/device/ftd" "os" "github.com/CiscoDevnet/terraform-provider-cdo/internal/device/ios" @@ -147,6 +148,7 @@ func (p *CdoProvider) Resources(ctx context.Context) []func() resource.Resource connector.NewResource, asa.NewAsaDeviceResource, ios.NewIosDeviceResource, + ftd.NewResource, } } diff --git a/provider/internal/util/sliceutil/slice.go b/provider/internal/util/sliceutil/slice.go new file mode 100644 index 00000000..839ca484 --- /dev/null +++ b/provider/internal/util/sliceutil/slice.go @@ -0,0 +1,25 @@ +package sliceutil + +// Map takes a slice and a function, apply the function to each element in the slice, and return the mapped slice. +func Map[T any, V any](sliceT []T, mapFunc func(T) V) []V { + sliceV := make([]V, len(sliceT)) + for i, item := range sliceT { + sliceV[i] = mapFunc(item) + } + return sliceV +} + +// MapWithError takes a slice and a function, apply the function to each element in the slice, and return the mapped slice. +// It allows the mapping function to return error, when it happens, it will terminate and return early. +func MapWithError[T any, V any](sliceT []T, mapFunc func(T) (V, error)) ([]V, error) { + sliceV := make([]V, len(sliceT)) + for i, item := range sliceT { + v, err := mapFunc(item) + if err != nil { + return nil, err + } + sliceV[i] = v + + } + return sliceV, nil +} diff --git a/provider/internal/util/togo.go b/provider/internal/util/togo.go new file mode 100644 index 00000000..7f283afe --- /dev/null +++ b/provider/internal/util/togo.go @@ -0,0 +1,11 @@ +package util + +import "github.com/hashicorp/terraform-plugin-framework/types" + +func TFStringListToGoStringList(l []types.String) []string { + res := make([]string, len(l)) + for i, v := range l { + res[i] = v.ValueString() + } + return res +} diff --git a/provider/internal/util/totf.go b/provider/internal/util/totf.go new file mode 100644 index 00000000..8bd0c9e0 --- /dev/null +++ b/provider/internal/util/totf.go @@ -0,0 +1,14 @@ +package util + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func GoStringSliceToTFStringList(stringSlice []string) []types.String { + l := make([]types.String, len(stringSlice)) + for i, v := range stringSlice { + l[i] = types.StringValue(v) + } + + return l +}