From 240f5a42ee4972d928e401c7a267e3b0b66d6985 Mon Sep 17 00:00:00 2001 From: Weilue Luo Date: Wed, 13 Sep 2023 11:05:30 +0100 Subject: [PATCH] FTD Onboarding Verification Tests (#46) Co-authored-by: Siddhu Warrier --- client/device/cloudfmc/fmcconfig/retry.go | 3 + .../cloudftd/cloudftdonboarding/create.go | 28 +- .../cloudftdonboarding/create_test.go | 438 ++++++++++++++++++ .../cloudftdonboarding/fixture_test.go | 158 +++++++ client/device/cloudftd/create.go | 2 +- .../device/cloudftd/create_metadatabuilder.go | 2 +- client/device/cloudftd/metadata.go | 60 +-- client/device/cloudftd/retry.go | 8 +- client/examples/create_test.go | 54 +++ .../fmcconfig/devicerecordcreation.go | 8 +- client/model/ftd/license/license.go | 51 +- client/model/user/auth/role/role.go | 2 +- provider/internal/device/ftd/operation.go | 7 +- 13 files changed, 723 insertions(+), 98 deletions(-) create mode 100644 client/device/cloudftd/cloudftdonboarding/create_test.go create mode 100644 client/device/cloudftd/cloudftdonboarding/fixture_test.go create mode 100644 client/examples/create_test.go diff --git a/client/device/cloudfmc/fmcconfig/retry.go b/client/device/cloudfmc/fmcconfig/retry.go index b0c7336b..bd6ea831 100644 --- a/client/device/cloudfmc/fmcconfig/retry.go +++ b/client/device/cloudfmc/fmcconfig/retry.go @@ -37,6 +37,9 @@ func UntilTaskStatusSuccess(ctx context.Context, client http.Client, readInp Rea func UntilCreateDeviceRecordSuccess(ctx context.Context, client http.Client, createDeviceRecordInput CreateDeviceRecordInput, output *CreateDeviceRecordOutput) retry.Func { return func() (bool, error) { createDeviceOutp, err := CreateDeviceRecord(ctx, client, createDeviceRecordInput) + if err != nil { + return false, err + } *output = *createDeviceOutp if err != nil { return false, err diff --git a/client/device/cloudftd/cloudftdonboarding/create.go b/client/device/cloudftd/cloudftdonboarding/create.go index 22fcf8aa..319db389 100644 --- a/client/device/cloudftd/cloudftdonboarding/create.go +++ b/client/device/cloudftd/cloudftdonboarding/create.go @@ -9,6 +9,7 @@ import ( "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/retry" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/ftd/license" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/user" "time" ) @@ -71,13 +72,18 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr if err != nil { return nil, err } + // 3.1.5 handle license + licenseCaps, err := license.DeserializeAllFromCdo(readFtdOutp.Metadata.LicenseCaps) + if err != nil { + return nil, err + } // 3.2 create ftd device createDeviceInp := fmcconfig.NewCreateDeviceRecordInputBuilder(). Type("Device"). NatId(readFtdOutp.Metadata.NatID). Name(readFtdOutp.Name). AccessPolicyUid(readFtdOutp.Metadata.AccessPolicyUid). - LicenseCaps(readFtdOutp.Metadata.LicenseCaps). + LicenseCaps(&licenseCaps). PerformanceTier(readFtdOutp.Metadata.PerformanceTier). RegKey(readFtdOutp.Metadata.RegKey). FmcDomainUid(fmcDomainUid). @@ -117,6 +123,26 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr if err != nil { return nil, err } + // 4.3 wait until state machine done + err = retry.Do( + cloudftd.UntilSpecificStateDone( + ctx, + client, + cloudftd.NewReadSpecificInputBuilder(). + Uid(ftdSpecificOutp.SpecificUid). + Build(), + ), + retry.NewOptionsBuilder(). + Retries(-1). + Delay(1*time.Second). + Timeout(20*time.Minute). // usually done in less than 5 minutes because we already registered in FTDc + Logger(client.Logger). + EarlyExitOnError(false). + Build(), + ) + if err != nil { + return nil, err + } return &createOutp, nil } diff --git a/client/device/cloudftd/cloudftdonboarding/create_test.go b/client/device/cloudftd/cloudftdonboarding/create_test.go new file mode 100644 index 00000000..25b118d9 --- /dev/null +++ b/client/device/cloudftd/cloudftdonboarding/create_test.go @@ -0,0 +1,438 @@ +package cloudftdonboarding_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd/cloudftdonboarding" + 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" + "strings" + "testing" + "time" +) + +func TestCloudFtdOnboardingCreate(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + testCases := []struct { + testName string + input cloudftdonboarding.CreateInput + setupFunc func(t *testing.T) + assertFunc func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) + }{ + { + testName: "successful ftd onboarding", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.Nil(t, err) + assert.NotNil(t, output) + assert.Equal(t, *output, validCreateFmcDeviceRecordOutput) + }, + }, + { + testName: "error when read fmc failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(false) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.ReadAllDevicesByType(baseUrl))) + }, + }, + { + testName: "error when read fmc domain info failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(false) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.ReadFmcDomainInfo(fmcHost))) + }, + }, + { + testName: "error when read api token info failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(false) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.ReadTokenInfo(baseUrl))) + }, + }, + { + testName: "error when create system token failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(false) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.CreateSystemToken(baseUrl, systemTokenScope))) + }, + }, + { + testName: "error when read FTD metadata failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(false) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.ReadDevice(baseUrl, ftdUid))) + }, + }, + { + testName: "error when create FTD device record failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + t.Skip("requires override inner retry config support") + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(false) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.CreateFmcDeviceRecord(baseUrl, fmcDomainUid))) + }, + }, + { + testName: "error when read task status failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + t.Skip("requires override inner retry config support") + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(false) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.ReadFmcTaskStatus(baseUrl, fmcDomainUid, fmcCreateDeviceTaskId))) + + }, + }, + { + testName: "error when read FTD specific device failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(false) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.ReadSpecificDevice(baseUrl, ftdUid))) + }, + }, + { + testName: "error when update FTD specific device failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(false) + TriggerRegisterFmcStateMachineEndsInDone(true) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.UpdateSpecificCloudFtd(baseUrl, ftdSpecificUid))) + }, + }, + { + testName: "error when read FTD specific device failed", + input: cloudftdonboarding.NewCreateInput(ftdUid), + setupFunc: func(t *testing.T) { + t.Skip("requires override inner retry config support") + ReadFmcIsSuccessful(true) + ReadFmcDomainInfoIsSuccessful(true) + ReadApiTokenInfoIsSuccessful(true) + CreateSystemApiTokenIsSuccessful(true) + ReadFtdMetadataIsSuccessful(true) + CreateFmcDeviceRecordIsSuccessful(true) + ReadTaskStatusIsSuccessful(true) + ReadFtdSpecificDeviceIsSuccessful(true) + TriggerRegisterFmcStateMachineSuccess(true) + TriggerRegisterFmcStateMachineEndsInDone(false) + }, + assertFunc: func(output *cloudftdonboarding.CreateOutput, err error, t *testing.T) { + assert.NotNil(t, err) + assert.Nil(t, output) + assert.True(t, strings.Contains(err.Error(), url.ReadSpecificDevice(baseUrl, ftdSpecificUid))) + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc(t) + + output, err := cloudftdonboarding.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, validReadDomainInfo), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadFmcDomainInfo(fmcHost), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func ReadApiTokenInfoIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadTokenInfo(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadApiTokenInfo), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadTokenInfo(baseUrl), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func CreateSystemApiTokenIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodPost, + url.CreateSystemToken(baseUrl, systemTokenScope), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validCreateSystemApiTokenOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodPost, + url.CreateSystemToken(baseUrl, systemTokenScope), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func ReadFtdMetadataIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadDevice(baseUrl, ftdUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadFtdOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadDevice(baseUrl, ftdUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func CreateFmcDeviceRecordIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodPost, + url.CreateFmcDeviceRecord(baseUrl, fmcDomainUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validCreateFmcDeviceRecordOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodPost, + url.CreateFmcDeviceRecord(baseUrl, fmcDomainUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func ReadTaskStatusIsSuccessful(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadFmcTaskStatus(baseUrl, fmcDomainUid, fmcCreateDeviceTaskId), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadTaskOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadFmcTaskStatus(baseUrl, fmcDomainUid, fmcCreateDeviceTaskId), + 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, validReadSpecificOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, ftdUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func TriggerRegisterFmcStateMachineSuccess(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateSpecificCloudFtd(baseUrl, ftdSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validUpdateSpecificUidOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodPut, + url.UpdateSpecificCloudFtd(baseUrl, ftdSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} + +func TriggerRegisterFmcStateMachineEndsInDone(success bool) { + if success { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, ftdSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusOK, validReadFtdSpecificOutput), + ) + } else { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadSpecificDevice(baseUrl, ftdSpecificUid), + httpmock.NewJsonResponderOrPanic(http.StatusInternalServerError, "internal server error"), + ) + } +} diff --git a/client/device/cloudftd/cloudftdonboarding/fixture_test.go b/client/device/cloudftd/cloudftdonboarding/fixture_test.go new file mode 100644 index 00000000..caa97d52 --- /dev/null +++ b/client/device/cloudftd/cloudftdonboarding/fixture_test.go @@ -0,0 +1,158 @@ +package cloudftdonboarding_test + +import ( + "encoding/json" + "fmt" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc/fmcconfig" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc/fmcplatform" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudftd" + fmcconfig2 "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/fmcconfig" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/fmcconfig/fmctaskstatus" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/cloudfmc/fmcdomain" + "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" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/statemachine/state" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/user/auth" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/user/auth/role" +) + +const ( + baseUrl = "https://unit-test.cdo.cisco.com" + + fmcName = "unit-test-device-name" + fmcUid = "unit-test-uid" + fmcDomainUid = "unit-test-domain-uid" + fmcHost = "unit-test-fmc-host.com" + fmcPort = 1234 + + fmcDomainLinks = "unit-test-links" + fmcDomainCount = 123 + fmcDomainOffset = 234 + fmcDomainLimit = 345 + fmcDomainPages = 456 + fmcDomainName = "unit-test-name" + fmcDomainType_ = "unit-test-type" + + ftdName = "unit-test-ftdName" + ftdUid = "unit-test-ftdUid" + + ftdGeneratedCommand = "unit-test-ftdGeneratedCommand" + ftdAccessPolicyName = "unit-test-access-policy-item-name" + ftdNatID = "unit-test-ftdNatID" + ftdCloudManagerDomain = "unit-test-ftdCloudManagerDomain.com" + ftdRegKey = "unit-test-ftdRegKey" + + tenantUid = "unit-test-tenant-uid" + + systemToken = "unit-test-system-token" + systemTokenScope = tenantUid + + fmcCreateDeviceTaskId = "unit-test-task-id" + + ftdSpecificUid = "unit-test-ftd-specific-id" +) + +var ( + ftdLicenseCaps = &[]license.Type{license.Base, license.Carrier} + ftdPerformanceTier = tier.FTDv5 +) + +var ( + validReadFtdSpecificOutput = cloudftd.NewReadSpecificOutputBuilder(). + SpecificUid(ftdSpecificUid). + State(state.DONE). + Build() + + validUpdateSpecificUidOutput = cloudftd.NewUpdateSpecificFtdOutputBuilder(). + SpecificUid(ftdSpecificUid). + Build() + + validReadSpecificOutput = cloudftd.NewReadSpecificOutputBuilder(). + SpecificUid(ftdSpecificUid). + Type(string(devicetype.CloudFtd)). + State(state.DONE). + Build() + + validReadTaskOutput = fmcconfig.ReadTaskStatusOutput{ + Status: fmctaskstatus.Success, + } + + validCreateFmcDeviceRecordOutput = fmcconfig.CreateDeviceRecordOutput{ + Metadata: fmcconfig2.Metadata{ + Task: fmcconfig2.Task{ + Name: "", + Id: fmcCreateDeviceTaskId, + Type: "", + }, + IsPartOfContainer: false, + IsMultiInstance: false, + }, + } + + validReadFtdOutput = cloudftd.NewReadOutputBuilder(). + Uid(ftdUid). + Name(ftdName). + Metadata(cloudftd.NewMetadataBuilder(). + LicenseCaps(ftdLicenseCaps). + GeneratedCommand(ftdGeneratedCommand). + AccessPolicyName(ftdAccessPolicyName). + PerformanceTier(&ftdPerformanceTier). + NatID(ftdNatID). + CloudManagerDomain(ftdCloudManagerDomain). + RegKey(ftdRegKey). + Build()). + Build() + + validCreateSystemApiTokenOutput = auth.Token{ + TenantUid: tenantUid, + TenantName: "", + AccessToken: systemToken, + RefreshToken: "", + TokenType: "", + Scope: "", + } + + validReadFmcOutput = []device.ReadOutput{ + device.NewReadOutputBuilder(). + AsCloudFmc(). + WithName(fmcName). + WithUid(fmcUid). + WithLocation(fmcHost, fmcPort). + Build(), + } + + validReadDomainInfo = &fmcplatform.ReadDomainInfoOutput{ + Links: fmcdomain.NewLinks(fmcDomainLinks), + Paging: fmcdomain.NewPaging(fmcDomainCount, fmcDomainOffset, fmcDomainLimit, fmcDomainPages), + Items: []fmcdomain.Item{ + fmcdomain.NewItem(fmcDomainUid, fmcDomainName, fmcDomainType_), + }, + } + + validReadApiTokenInfo = auth.Info{UserAuthentication: auth.Authentication{ + Authorities: []auth.Authority{ + {Authority: role.Admin}, + }, + Details: auth.Details{ + TenantUid: tenantUid, + TenantName: "", + SseTenantUid: "", + TenantOrganizationName: "", + TenantDbFeatures: "", + TenantUserRoles: "", + TenantDatabaseName: "", + TenantPayType: "", + }, + Authenticated: false, + Principle: "", + Name: "", + }} +) + +func init() { + fmt.Println("json.Marshal(validReadApiTokenInfo)") + t, _ := json.Marshal(validReadApiTokenInfo) + fmt.Println(string(t)) +} diff --git a/client/device/cloudftd/create.go b/client/device/cloudftd/create.go index 07fb063b..8eedfe32 100644 --- a/client/device/cloudftd/create.go +++ b/client/device/cloudftd/create.go @@ -114,7 +114,7 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr Metadata: &Metadata{ AccessPolicyName: selectedPolicy.Name, AccessPolicyUid: selectedPolicy.Id, - LicenseCaps: createInp.Licenses, + LicenseCaps: license.SerializeAllAsCdo(*createInp.Licenses), PerformanceTier: performanceTier, }, State: "NEW", diff --git a/client/device/cloudftd/create_metadatabuilder.go b/client/device/cloudftd/create_metadatabuilder.go index d97f332f..945baefa 100644 --- a/client/device/cloudftd/create_metadatabuilder.go +++ b/client/device/cloudftd/create_metadatabuilder.go @@ -36,7 +36,7 @@ func (b *MetadataBuilder) GeneratedCommand(generatedCommand string) *MetadataBui } func (b *MetadataBuilder) LicenseCaps(licenseCaps *[]license.Type) *MetadataBuilder { - b.metadata.LicenseCaps = licenseCaps + b.metadata.LicenseCaps = license.SerializeAllAsCdo(*licenseCaps) return b } diff --git a/client/device/cloudftd/metadata.go b/client/device/cloudftd/metadata.go index 799c5f00..9774a397 100644 --- a/client/device/cloudftd/metadata.go +++ b/client/device/cloudftd/metadata.go @@ -1,72 +1,16 @@ package cloudftd import ( - "encoding/json" - "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"` + AccessPolicyUid 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 + LicenseCaps string `json:"license_caps,omitempty"` 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 { - 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) { - 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/retry.go b/client/device/cloudftd/retry.go index ef685b91..a734e081 100644 --- a/client/device/cloudftd/retry.go +++ b/client/device/cloudftd/retry.go @@ -29,11 +29,11 @@ func UntilGeneratedCommandAvailable(ctx context.Context, client http.Client, uid } } -func UntilStateDone(ctx context.Context, client http.Client, inp ReadByUidInput) retry.Func { +func UntilSpecificStateDone(ctx context.Context, client http.Client, inp ReadSpecificInput) retry.Func { return func() (bool, error) { - client.Logger.Println("check FTD state") + client.Logger.Println("check FTD specific device state") - readOutp, err := ReadByUid(ctx, client, inp) + readOutp, err := ReadSpecific(ctx, client, inp) if err != nil { return false, err } @@ -43,7 +43,7 @@ func UntilStateDone(ctx context.Context, client http.Client, inp ReadByUidInput) } else if readOutp.State == state.ERROR { return false, fmt.Errorf("workflow ended in error") } else { - return false, fmt.Errorf("generated command not found in metadata: %+v", readOutp.Metadata) + return false, nil } } } diff --git a/client/examples/create_test.go b/client/examples/create_test.go new file mode 100644 index 00000000..18f1ae87 --- /dev/null +++ b/client/examples/create_test.go @@ -0,0 +1,54 @@ +package examples_test + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/examples" + 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" + "net/http" + "testing" + "time" +) + +func TestExampleCreate(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + testCases := []struct { + testName string + input examples.CreateInput + setupFunc func() + assertFunc func(output *examples.CreateOutput, err error, t *testing.T) + }{ + { + testName: "example test", + input: examples.NewCreateInput("unittest-device-uid"), + setupFunc: func() { + httpmock.RegisterResponder( + http.MethodGet, + url.ReadDevice("https://unittest.cdo.cisco.com", "unittest-device-uid"), + httpmock.NewJsonResponderOrPanic(http.StatusOK, "{\"a\":\"b\"}"), + ) + }, + assertFunc: func(output *examples.CreateOutput, err error, t *testing.T) { + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.testName, func(t *testing.T) { + httpmock.Reset() + + testCase.setupFunc() + + output, err := examples.Create( + context.Background(), + *internalHttp.MustNewWithConfig("https://unittest.cdo.cisco.com", "a_valid_token", 0, 0, time.Minute), + testCase.input, + ) + + testCase.assertFunc(output, err, t) + }) + } +} diff --git a/client/model/cloudfmc/fmcconfig/devicerecordcreation.go b/client/model/cloudfmc/fmcconfig/devicerecordcreation.go index 1485eed6..603fbc9c 100644 --- a/client/model/cloudfmc/fmcconfig/devicerecordcreation.go +++ b/client/model/cloudfmc/fmcconfig/devicerecordcreation.go @@ -16,7 +16,7 @@ type DeviceRecordCreation struct { AccessPolicy accessPolicy `json:"accessPolicy"` LicenseCaps *[]license.Type `json:"license_caps"` - Metadata metadata `json:"metadata"` + Metadata Metadata `json:"metadata"` } type accessPolicy struct { @@ -24,13 +24,13 @@ type accessPolicy struct { Type string `json:"type"` } -type metadata struct { - Task task `json:"task"` +type Metadata struct { + Task Task `json:"task"` IsPartOfContainer bool `json:"isPartOfContainer"` IsMultiInstance bool `json:"isMultiInstance"` } -type task struct { +type Task struct { Name string `json:"name"` Id string `json:"id"` Type string `json:"type"` diff --git a/client/model/ftd/license/license.go b/client/model/ftd/license/license.go index e82c01a9..965c3840 100644 --- a/client/model/ftd/license/license.go +++ b/client/model/ftd/license/license.go @@ -19,7 +19,11 @@ const ( ) var All = []Type{ - Base, Carrier, Threat, Malware, URLFilter, + Base, + Carrier, + Threat, + Malware, + URLFilter, } var AllAsString = make([]string, len(All)) @@ -39,13 +43,9 @@ func (t *Type) MarshalJSON() ([]byte, error) { func (t *Type) UnmarshalJSON(b []byte) error { if len(b) <= 2 || b == nil { - return fmt.Errorf("cannot unmarshal empty tring as a license type, it should be one of valid roles: %+v", nameToTypeMap) + return fmt.Errorf("cannot unmarshal empty tring as a license type, it should be one of valid roles: %+v", AllAsString) } - unquoteType, err := strconv.Unquote(string(b)) - if err != nil { - return err - } - deserialized, err := deserialize(unquoteType) + deserialized, err := Deserialize(string(b[1 : len(b)-1])) // strip off quote if err != nil { return err } @@ -53,31 +53,34 @@ func (t *Type) UnmarshalJSON(b []byte) error { return nil } -func deserialize(name string) (Type, error) { +func MustParse(name string) Type { l, ok := nameToTypeMap[name] if !ok { - return "", fmt.Errorf("FTD License of name: \"%s\" not found, should be one of: %+v", name, nameToTypeMap) + panic(fmt.Errorf("FTD License of name: \"%s\" not found, should be one of %+v", name, AllAsString)) } - return l, nil + return l } -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 +func Deserialize(name string) (Type, error) { + l, ok := nameToTypeMap[name] + if !ok { + return "", fmt.Errorf("FTD License of name: \"%s\" not found, should be one of: %+v", name, AllAsString) } - return licenses, nil + return l, nil } -func SerializeAll(licenses []Type) string { - return strings.Join(sliceutil.Map(licenses, func(l Type) string { return serialize(l) }), ",") +func SerializeAllAsCdo(licenses []Type) string { + return strings.Join(sliceutil.Map(licenses, func(l Type) string { return string(l) }), ",") } -func serialize(license Type) string { - return string(license) +// DeserializeAllFromCdo exists because CDO store license caps as one comma-sep string +// but fmc store it as list of string, use this method to handle CDO's special case +func DeserializeAllFromCdo(licenses string) ([]Type, error) { + return sliceutil.MapWithError(strings.Split(licenses, ","), func(l string) (Type, error) { + t, ok := nameToTypeMap[l] + if !ok { + return "", fmt.Errorf("cannot deserialize %s as license, should be one of %+v", l, All) + } + return t, nil + }) } diff --git a/client/model/user/auth/role/role.go b/client/model/user/auth/role/role.go index 7caa8aac..cd47b444 100644 --- a/client/model/user/auth/role/role.go +++ b/client/model/user/auth/role/role.go @@ -54,5 +54,5 @@ func (t *Type) UnmarshalJSON(b []byte) error { } func (t *Type) MarshalJSON() ([]byte, error) { - return []byte(*t), nil + return []byte(strconv.Quote(string(*t))), nil } diff --git a/provider/internal/device/ftd/operation.go b/provider/internal/device/ftd/operation.go index 88d9804c..bdfc562e 100644 --- a/provider/internal/device/ftd/operation.go +++ b/provider/internal/device/ftd/operation.go @@ -6,7 +6,6 @@ import ( "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" "strings" ) @@ -26,7 +25,7 @@ func Read(ctx context.Context, resource *Resource, stateData *ResourceModel) err 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) })) + stateData.Licenses = util.GoStringSliceToTFStringList(strings.Split(res.Metadata.LicenseCaps, ",")) if res.Metadata.PerformanceTier != nil { // nil means physical cloudftd stateData.PerformanceTier = types.StringValue(string(*res.Metadata.PerformanceTier)) } @@ -51,7 +50,7 @@ func Create(ctx context.Context, resource *Resource, planData *ResourceModel) er } licensesGoList := util.TFStringListToGoStringList(planData.Licenses) - licenses, err := license.DeserializeAll(strings.Join(licensesGoList, ",")) + licenses, err := license.DeserializeAllFromCdo(strings.Join(licensesGoList, ",")) if err != nil { return err } @@ -72,7 +71,7 @@ func Create(ctx context.Context, resource *Resource, planData *ResourceModel) er planData.Name = types.StringValue(res.Name) planData.AccessPolicyName = types.StringValue(res.Metadata.AccessPolicyName) planData.AccessPolicyUid = types.StringValue(res.Metadata.AccessPolicyUid) - planData.Licenses = util.GoStringSliceToTFStringList(sliceutil.Map(*res.Metadata.LicenseCaps, func(l license.Type) string { return string(l) })) + planData.Licenses = util.GoStringSliceToTFStringList(strings.Split(res.Metadata.LicenseCaps, ",")) if res.Metadata.PerformanceTier != nil { // nil means physical cloud ftd planData.PerformanceTier = types.StringValue(string(*res.Metadata.PerformanceTier)) }