diff --git a/client/client.go b/client/client.go index cd123730..321d9458 100644 --- a/client/client.go +++ b/client/client.go @@ -196,6 +196,10 @@ func (c *Client) ReadTenantDetails(ctx context.Context) (*tenant.ReadTenantDetai return tenant.ReadTenantDetails(ctx, c.client) } +func (c *Client) CreateCloudFmcDevice(ctx context.Context, inp cloudfmc.CreateInput) (*cloudfmc.CreateOutput, error) { + return cloudfmc.Create(ctx, c.client, inp) +} + func (c *Client) ReadCloudFmcDevice(ctx context.Context) (*device.ReadOutput, error) { return cloudfmc.Read(ctx, c.client, cloudfmc.NewReadInput()) } diff --git a/client/device/application/error.go b/client/device/application/error.go new file mode 100644 index 00000000..bf41bcc0 --- /dev/null +++ b/client/device/application/error.go @@ -0,0 +1,7 @@ +package application + +import "fmt" + +var ( + NotFoundError = fmt.Errorf("unable to find application") +) diff --git a/client/device/application/read.go b/client/device/application/read.go new file mode 100644 index 00000000..ed25d33a --- /dev/null +++ b/client/device/application/read.go @@ -0,0 +1,49 @@ +package application + +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/application/applicationstatus" +) + +type ReadInput struct { +} + +type ReadOutput struct { + Uid string `json:"uid"` + Name string `json:"name"` + Version int `json:"version"` + ApplicationType string `json:"applicationType"` + ApplicationStatus applicationstatus.Type `json:"applicationStatus"` + ApplicationContent ApplicationContent `json:"applicationContent"` +} + +type ApplicationContent struct { + Type string `json:"@type"` + FmceDeviceUid interface{} `json:"fmceDeviceUid"` + DevicesCount int `json:"devicesCount"` + SfcnDevicesCount int `json:"sfcnDevicesCount"` + FmcApplianceUid interface{} `json:"fmcApplianceUid"` + RequestedDevicesCount int `json:"requestedDevicesCount"` + EstimatedDevicesCountRange string `json:"estimatedDevicesCountRange"` +} + +func Read(ctx context.Context, client http.Client, readInp ReadInput) (*ReadOutput, error) { + // create request + readUrl := url.ReadApplication(client.BaseUrl()) + readReq := client.NewGet(ctx, readUrl) + + // send request & map response + var readApplicationOutput []ReadOutput + err := readReq.Send(&readApplicationOutput) + if err != nil { + return nil, err + } + + // check and return + if len(readApplicationOutput) < 1 { + return nil, NotFoundError + } + return &readApplicationOutput[0], nil +} diff --git a/client/device/asa/create.go b/client/device/asa/create.go index ff25ff0f..2f46a0fd 100644 --- a/client/device/asa/create.go +++ b/client/device/asa/create.go @@ -86,17 +86,22 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr metadata = &Metadata{IsNewPolicyObjectModel: "true"} } // 1.3 create the device - deviceCreateOutp, err := device.Create(ctx, client, *device.NewCreateRequestInput( - createInp.Name, - "ASA", - createInp.ConnectorUid, - createInp.ConnectorType, - createInp.SocketAddress, - false, - createInp.IgnoreCertificate, - metadata, - createInp.Tags, - )) + deviceCreateOutp, err := device.Create( + ctx, + client, + device.NewCreateInputBuilder(). + Name(createInp.Name). + DeviceType(devicetype.Asa). + ConnectorUid(createInp.ConnectorUid). + ConnectorType(createInp.ConnectorType). + SocketAddress(createInp.SocketAddress). + Model(false). + IgnoreCertificate(&createInp.IgnoreCertificate). + Metadata(metadata). + Tags(createInp.Tags). + Build(), + ) + var createdResourceId *string = nil if deviceCreateOutp != nil { createdResourceId = &deviceCreateOutp.Uid diff --git a/client/device/cloudfmc/create.go b/client/device/cloudfmc/create.go new file mode 100644 index 00000000..a17a4ef3 --- /dev/null +++ b/client/device/cloudfmc/create.go @@ -0,0 +1,123 @@ +package cloudfmc + +import ( + "context" + "errors" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/application" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/goutil" + "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/application/applicationstatus" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/devicetype" + "time" +) + +type CreateInput struct { +} + +func NewCreateInput() CreateInput { + return CreateInput{} +} + +type createApplicationBody struct { + ApplicationType string `json:"applicationType"` + ApplicationStatus string `json:"applicationStatus"` + ApplicationContent applicationContent `json:"applicationContent"` +} + +type applicationContent struct { + Type string `json:"@type"` +} + +type CreateOutput = device.CreateOutput + +func Create(ctx context.Context, client http.Client, createInp CreateInput) (*CreateOutput, error) { + + client.Logger.Println("Creating application object for cdFMC") + + // 1. POST /aegis/rest/v1/services/targets/applications + createApplicationUrl := url.CreateApplication(client.BaseUrl()) + createApplicationReq := client.NewPost( + ctx, + createApplicationUrl, + createApplicationBody{ + ApplicationType: "FMCE", + ApplicationStatus: "REQUESTED", + ApplicationContent: applicationContent{ + Type: "FmceApplicationContent", + }, + }, + ) + var createApplicationOutp device.CreateOutput + err := createApplicationReq.Send(&createApplicationOutp) + if err != nil { + return nil, err + } + + client.Logger.Println("creating cloud FMC device") + + // 2. POST /aegis/rest/v1/services/targets/devices + createOutp, err := device.Create(ctx, client, device.NewCreateInputBuilder(). + Name("FMC"). + DeviceType(devicetype.CloudFmc). + Model(false). + ConnectorType("CDG"). + IgnoreCertificate(goutil.NewBoolPointer(true)). + EnableOobDetection(goutil.NewBoolPointer(false)). + Build(), + ) + if err != nil { + return nil, err + } + + client.Logger.Println("waiting for fmce state machine to be done") + + err = retry.Do(untilApplicationActive(ctx, client), + retry.NewOptionsBuilder(). + Retries(-1). + Timeout(30*time.Minute). // usually takes about 15-20 minutes + Delay(3*time.Second). + EarlyExitOnError(true). + Logger(client.Logger). + Build(), + ) + if err != nil { + return nil, err + } + + client.Logger.Println("cloud FMC application successfully created") + + return createOutp, nil +} + +func untilApplicationActive(ctx context.Context, client http.Client) retry.Func { + var unreachable bool + var initialUnreachableTime time.Time + return func() (bool, error) { + fmc, err := application.Read(ctx, client, application.ReadInput{}) + if err != nil { + if !errors.Is(err, application.NotFoundError) { + // maybe the application is not created yet, and hopefully this is temporarily, ignoring + return false, nil + } + return false, err + } + if fmc.ApplicationStatus == applicationstatus.Unreachable { + // initial unreachable is possibly caused by https://jira-eng-rtp3.cisco.com/jira/browse/LH-71821 + // wait for some time to confirm it is actually unreachable + if unreachable { + if initialUnreachableTime.Add(time.Minute * 5).After(time.Now()) { + // if long enough time has passed, and we are still unreachable, treat it as actual error + return false, err + } + } else { + unreachable = true + initialUnreachableTime = time.Now() + return false, nil + } + } + return fmc.ApplicationStatus == applicationstatus.Active, nil + } +} diff --git a/client/device/create.go b/client/device/create.go index b6971947..4fcda463 100644 --- a/client/device/create.go +++ b/client/device/create.go @@ -3,45 +3,30 @@ package device import ( "context" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/device/tags" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/devicetype" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/url" ) type CreateInput struct { - Name string `json:"name"` - DeviceType string `json:"deviceType"` - ConnectorUid string `json:"larUid,omitempty"` - ConnectorType string `json:"larType"` - SocketAddress string `json:"ipv4"` - Model bool `json:"model"` - IgnoreCertificate bool `json:"ignoreCertificate"` - Metadata *interface{} `json:"metadata,omitempty"` - Tags tags.Type `json:"tags,omitempty"` + // required + Name string `json:"name"` + DeviceType devicetype.Type `json:"deviceType"` + Model bool `json:"model"` + + // optional + ConnectorUid string `json:"larUid,omitempty"` + ConnectorType string `json:"larType,omitempty"` + SocketAddress string `json:"ipv4,omitempty"` + IgnoreCertificate *bool `json:"ignoreCertificate,omitempty"` + Metadata *interface{} `json:"metadata,omitempty"` + Tags tags.Type `json:"tags,omitempty"` + EnableOobDetection *bool `json:"enableOobDetection,omitempty"` } type CreateOutput = ReadOutput -func NewCreateRequestInput(name, deviceType, connectorUid, connectorType, socketAddress string, model bool, ignoreCertificate bool, metadata interface{}, tags tags.Type) *CreateInput { - // convert interface{} to a pointer - var metadataPtr *interface{} = nil - if metadata != nil { - metadataPtr = &metadata - } - - return &CreateInput{ - Name: name, - DeviceType: deviceType, - ConnectorUid: connectorUid, - ConnectorType: connectorType, - SocketAddress: socketAddress, - Model: model, - IgnoreCertificate: ignoreCertificate, - Metadata: metadataPtr, - Tags: tags, - } -} - func NewCreateRequest(ctx context.Context, client http.Client, createIn CreateInput) *http.Request { url := url.CreateDevice(client.BaseUrl()) diff --git a/client/device/create_inputbuilder.go b/client/device/create_inputbuilder.go new file mode 100644 index 00000000..a0e7c0a7 --- /dev/null +++ b/client/device/create_inputbuilder.go @@ -0,0 +1,72 @@ +package device + +import ( + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/goutil" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/device/tags" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/devicetype" +) + +// CreateInput builder pattern code +type CreateInputBuilder struct { + createInput *CreateInput +} + +func NewCreateInputBuilder() *CreateInputBuilder { + createInput := &CreateInput{} + b := &CreateInputBuilder{createInput: createInput} + return b +} + +func (b *CreateInputBuilder) Name(name string) *CreateInputBuilder { + b.createInput.Name = name + return b +} + +func (b *CreateInputBuilder) DeviceType(deviceType devicetype.Type) *CreateInputBuilder { + b.createInput.DeviceType = deviceType + return b +} + +func (b *CreateInputBuilder) Model(model bool) *CreateInputBuilder { + b.createInput.Model = model + return b +} + +func (b *CreateInputBuilder) ConnectorUid(connectorUid string) *CreateInputBuilder { + b.createInput.ConnectorUid = connectorUid + return b +} + +func (b *CreateInputBuilder) ConnectorType(connectorType string) *CreateInputBuilder { + b.createInput.ConnectorType = connectorType + return b +} + +func (b *CreateInputBuilder) SocketAddress(socketAddress string) *CreateInputBuilder { + b.createInput.SocketAddress = socketAddress + return b +} + +func (b *CreateInputBuilder) IgnoreCertificate(ignoreCertificate *bool) *CreateInputBuilder { + b.createInput.IgnoreCertificate = ignoreCertificate + return b +} + +func (b *CreateInputBuilder) Metadata(metadata interface{}) *CreateInputBuilder { + b.createInput.Metadata = goutil.AsPointer(metadata) + return b +} + +func (b *CreateInputBuilder) Tags(tags tags.Type) *CreateInputBuilder { + b.createInput.Tags = tags + return b +} + +func (b *CreateInputBuilder) EnableOobDetection(enableOobDetection *bool) *CreateInputBuilder { + b.createInput.EnableOobDetection = enableOobDetection + return b +} + +func (b *CreateInputBuilder) Build() CreateInput { + return *b.createInput +} diff --git a/client/device/genericssh/create.go b/client/device/genericssh/create.go index db7f8800..bcf419db 100644 --- a/client/device/genericssh/create.go +++ b/client/device/genericssh/create.go @@ -3,7 +3,9 @@ package genericssh import ( "context" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/goutil" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/device/tags" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/model/devicetype" "github.com/CiscoDevnet/terraform-provider-cdo/go-client/internal/http" ) @@ -31,18 +33,19 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr client.Logger.Println("creating generic ssh") - deviceInput := device.NewCreateRequestInput( - createInp.Name, - "GENERIC_SSH", - createInp.ConnectorUid, - createInp.ConnectorType, - createInp.SocketAddress, - false, - false, - nil, - createInp.Tags, - ) - outp, err := device.Create(ctx, client, *deviceInput) + deviceInput := device.NewCreateInputBuilder(). + Name(createInp.Name). + DeviceType(devicetype.GenericSSH). + ConnectorUid(createInp.ConnectorUid). + ConnectorType(createInp.ConnectorType). + SocketAddress(createInp.SocketAddress). + Model(false). + IgnoreCertificate(goutil.NewBoolPointer(false)). + Metadata(nil). + Tags(createInp.Tags). + Build() + + outp, err := device.Create(ctx, client, deviceInput) if err != nil { return nil, err } diff --git a/client/device/ios/create.go b/client/device/ios/create.go index 9f19c0f2..0952589b 100644 --- a/client/device/ios/create.go +++ b/client/device/ios/create.go @@ -67,16 +67,22 @@ func Create(ctx context.Context, client http.Client, createInp CreateInput) (*Cr client.Logger.Println("creating ios device") - deviceCreateOutp, err := device.Create(ctx, client, *device.NewCreateRequestInput( - createInp.Name, - "IOS", - createInp.ConnectorUid, - createInp.ConnectorType, - createInp.SocketAddress, - false, createInp.IgnoreCertificate, - nil, - createInp.Tags, - )) + deviceCreateOutp, err := device.Create( + ctx, + client, + device.NewCreateInputBuilder(). + Name(createInp.Name). + DeviceType(devicetype.Ios). + ConnectorUid(createInp.ConnectorUid). + ConnectorType(createInp.ConnectorType). + SocketAddress(createInp.SocketAddress). + Model(false). + IgnoreCertificate(&createInp.IgnoreCertificate). + Metadata(nil). + Tags(createInp.Tags). + Build(), + ) + var createdResourceId *string = nil if deviceCreateOutp != nil { createdResourceId = &deviceCreateOutp.Uid diff --git a/client/internal/goutil/goutil.go b/client/internal/goutil/goutil.go new file mode 100644 index 00000000..b575372c --- /dev/null +++ b/client/internal/goutil/goutil.go @@ -0,0 +1,15 @@ +package goutil + +// AsPointer convert interface{} to *interface{}, if input is not nil +func AsPointer(obj interface{}) *interface{} { + var ptr *interface{} = nil + if obj != nil { + ptr = &obj + } + return ptr +} + +// NewBoolPointer return a pointer of the given boolean value, this function is needed because you cant do &true or &false in golang +func NewBoolPointer(value bool) *bool { + return &value +} diff --git a/client/internal/statemachine/readinstance_by_name.go b/client/internal/statemachine/readinstance_by_name.go new file mode 100644 index 00000000..937d0d39 --- /dev/null +++ b/client/internal/statemachine/readinstance_by_name.go @@ -0,0 +1,46 @@ +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 ReadInstanceByNameInput struct { + Name string +} + +func NewReadInstanceByNameInput(name string) ReadInstanceByNameInput { + return ReadInstanceByNameInput{ + Name: name, + } +} + +type ReadInstanceByNameOutput = statemachine.Instance + +var NewReadInstanceByNameOutputBuilder = statemachine.NewInstanceBuilder + +func ReadInstanceByName(ctx context.Context, client http.Client, readInp ReadInstanceByNameInput) (*ReadInstanceByNameOutput, error) { + + readUrl := url.ReadStateMachineInstance(client.BaseUrl()) + req := client.NewGet(ctx, readUrl) + req.QueryParams.Add("limit", "1") + req.QueryParams.Add("q", fmt.Sprintf("stateMachineIdentifier:%s", readInp.Name)) + 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, NotFoundError + } + + if len(readRes) > 1 { + return nil, MoreThanOneRunningError + } + + return &readRes[0], nil +} diff --git a/client/internal/url/url.go b/client/internal/url/url.go index 21bed49e..ed6b7b99 100644 --- a/client/internal/url/url.go +++ b/client/internal/url/url.go @@ -149,3 +149,11 @@ func ReadFmcTaskStatus(baseUrl string, fmcDomainUid string, taskId string) strin func ReadTenantDetails(baseUrl string) string { return fmt.Sprintf("%s/anubis/rest/v1/oauth/check_token", baseUrl) } + +func CreateApplication(baseUrl string) string { + return fmt.Sprintf("%s/aegis/rest/v1/services/targets/applications", baseUrl) +} + +func ReadApplication(baseUrl string) string { + return fmt.Sprintf("%s/aegis/rest/v1/services/targets/applications", baseUrl) +} diff --git a/client/model/application/applicationstatus/status.go b/client/model/application/applicationstatus/status.go new file mode 100644 index 00000000..705ae656 --- /dev/null +++ b/client/model/application/applicationstatus/status.go @@ -0,0 +1,11 @@ +package applicationstatus + +type Type string + +// see: https://github.com/cisco-lockhart/aegis/blob/master/modules/configs/src/main/java/com/cisco/lockhart/applications/models/ApplicationStatus.java +const ( + Init Type = "INIT" // used to send sns/sqs message for ops ticket + Requested Type = "REQUESTED" // the cdFMC is being provisioned + Active Type = "ACTIVE" // application is ready for use + Unreachable Type = "UNREACHABLE" // when heartbeat fails +) diff --git a/client/model/application/info.go b/client/model/application/info.go new file mode 100644 index 00000000..922ed14f --- /dev/null +++ b/client/model/application/info.go @@ -0,0 +1,2 @@ +// Package application represents models for external applications/services that CDO interacts or interested in. +package application diff --git a/client/model/device/tags/tags.go b/client/model/device/tags/tags.go index 9bb43f53..88b70de8 100644 --- a/client/model/device/tags/tags.go +++ b/client/model/device/tags/tags.go @@ -7,7 +7,7 @@ import ( // Type should be used with json:"tags" type Type struct { - Labels []string `json:"labels"` + Labels []string `json:"labels,omitempty"` } func Empty() Type { diff --git a/client/model/devicetype/devicetype.go b/client/model/devicetype/devicetype.go index 53ee6221..67209fad 100644 --- a/client/model/devicetype/devicetype.go +++ b/client/model/devicetype/devicetype.go @@ -3,8 +3,9 @@ package devicetype type Type string const ( - Asa Type = "ASA" - Ios Type = "IOS" - CloudFmc Type = "FMCE" - CloudFtd Type = "FTDC" + Asa Type = "ASA" + Ios Type = "IOS" + CloudFmc Type = "FMCE" + CloudFtd Type = "FTDC" + GenericSSH Type = "GENERIC_SSH" ) diff --git a/client/model/statemachine/details.go b/client/model/statemachine/details.go index b39e1b9a..d84ab764 100644 --- a/client/model/statemachine/details.go +++ b/client/model/statemachine/details.go @@ -9,4 +9,15 @@ type Details struct { StateMachineInstanceCondition string `json:"stateMachineInstanceCondition"` StateMachineType string `json:"stateMachineType"` Uid string `json:"uid"` + LastError *Error `json:"lastError,omitempty"` +} + +type Error struct { + StateMachineType string `json:"stateMachineType"` + ErrorMessage string `json:"errorMessage"` + StateMachineIdentifier string `json:"stateMachineIdentifier"` + ActionIdentifier string `json:"actionIdentifier"` + EndState *string `json:"endState,omitempty"` + ErrorCode *string `json:"errorCode,omitempty"` + ErrorDate int64 `json:"errorDate"` } diff --git a/docs/resources/cdfmc.md b/docs/resources/cdfmc.md new file mode 100644 index 00000000..bbbc31fe --- /dev/null +++ b/docs/resources/cdfmc.md @@ -0,0 +1,21 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cdo_cdfmc Resource - cdo" +subcategory: "" +description: |- + Use this resource to create a Cloud-delivered FMC (cdFMC) in your tenant. You can have only one cdFMC in your tenant. In addition, you cannot delete a created cdFMC using this resource; to delete your cdFMC, please contact Cisco support. +--- + +# cdo_cdfmc (Resource) + +Use this resource to create a Cloud-delivered FMC (cdFMC) in your tenant. You can have only one cdFMC in your tenant. In addition, you cannot delete a created cdFMC using this resource; to delete your cdFMC, please contact Cisco support. + + + + +## Schema + +### Read-Only + +- `id` (String) The unique identifier of the cdFMC. This is automatically generated by CDO. +- `name` (String) The name of the cdFMC. This is automatically generated from the tenant name by CDO. diff --git a/provider/.github-action.env b/provider/.github-action.env index f91aadac..f6afbb50 100644 --- a/provider/.github-action.env +++ b/provider/.github-action.env @@ -54,4 +54,5 @@ CONNECTOR_DATA_SOURCE_NAME=CDO_terraform-provider-cdo-SDC-1 CONNECTOR_RESOURCE_NAME=test-sdc-1 CONNECTOR_RESOURCE_NEW_NAME=test-sdc-2 CLOUD_FMC_HOSTNAME=terraform-provider-cdo.app.staging.cdo.cisco.com -CLOUD_FMC_SOFTWARE_VERSION=7.3.1-build 6035 \ No newline at end of file +CLOUD_FMC_SOFTWARE_VERSION=7.3.1-build 6035 +CLOUD_FMC_RESOURCE_NAME=FMC \ No newline at end of file diff --git a/provider/GNUmakefile b/provider/GNUmakefile index 02fe4284..296db320 100644 --- a/provider/GNUmakefile +++ b/provider/GNUmakefile @@ -11,4 +11,4 @@ testacc: TF_ACC=1 \ go test ./... -v $(TESTARGS) -timeout 120m -count 1 -#TESTARGS=-run TestAccIosDeviceResource_SDC \ No newline at end of file +#TESTARGS=-run TestAccCdFmcResource \ No newline at end of file diff --git a/provider/internal/acctest/environment.go b/provider/internal/acctest/environment.go index 3fdae00d..e902abe4 100644 --- a/provider/internal/acctest/environment.go +++ b/provider/internal/acctest/environment.go @@ -303,6 +303,10 @@ func (e *env) CloudFmcDataSourceSoftwareVersion() string { return e.mustGetString("CLOUD_FMC_SOFTWARE_VERSION") } +func (e *env) CloudFmcResourceName() string { + return e.mustGetString("CLOUD_FMC_RESOURCE_NAME") +} + func (e *env) mustGetString(envName string) string { value, ok := os.LookupEnv(envName) if ok { diff --git a/provider/internal/cdfmc/operation.go b/provider/internal/cdfmc/operation.go new file mode 100644 index 00000000..a290cd94 --- /dev/null +++ b/provider/internal/cdfmc/operation.go @@ -0,0 +1,43 @@ +package cdfmc + +import ( + "context" + "github.com/CiscoDevnet/terraform-provider-cdo/go-client/device/cloudfmc" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func Read(ctx context.Context, resource *Resource, stateData *ResourceModel) error { + + // do read + // e.g. readOutp, err := resource.client.ReadExample(ctx, ...) + + readOut, err := resource.client.ReadCloudFmcDevice(ctx) + if err != nil { + return err + } + + // map response to terraform types + // e.g. stateData.ID = types.StringValue(readOutp.Uid) + stateData.Id = types.StringValue(readOut.Uid) + stateData.Name = types.StringValue(readOut.Name) + + return nil +} + +func Create(ctx context.Context, resource *Resource, planData *ResourceModel) error { + + // do create + // e.g. createOutp, err := resource.client.CreateExample(ctx, ...) + + createOut, err := resource.client.CreateCloudFmcDevice(ctx, cloudfmc.NewCreateInput()) + if err != nil { + return err + } + + // map response to terraform types + // e.g. planData.ID = types.StringValue(createOutp.Uid) + planData.Id = types.StringValue(createOut.Uid) + planData.Name = types.StringValue(createOut.Name) + + return nil +} diff --git a/provider/internal/cdfmc/resource.go b/provider/internal/cdfmc/resource.go new file mode 100644 index 00000000..eead2283 --- /dev/null +++ b/provider/internal/cdfmc/resource.go @@ -0,0 +1,127 @@ +package cdfmc + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/types" + + cdoClient "github.com/CiscoDevnet/terraform-provider-cdo/go-client" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var _ resource.Resource = &Resource{} + +func NewResource() resource.Resource { + return &Resource{} +} + +type Resource struct { + client *cdoClient.Client +} + +type ResourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} + +func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_cdfmc" +} + +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Use this resource to create a Cloud-delivered FMC (cdFMC) in your tenant. You can have only one cdFMC in your tenant. In addition, you cannot delete a created cdFMC using this resource; to delete your cdFMC, please contact Cisco support.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The unique identifier of the cdFMC. This is automatically generated by CDO.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the cdFMC. This is automatically generated from the tenant name by CDO.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + 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 cdfmc resource") + + // 1. read state data + 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 cdfmc resource", err.Error()) + } + + // 3. save data into terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &stateData)...) +} + +func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, res *resource.CreateResponse) { + + tflog.Trace(ctx, "create cdfmc resource") + + // 1. read plan data into planData + var planData ResourceModel + res.Diagnostics.Append(req.Plan.Get(ctx, &planData)...) + if res.Diagnostics.HasError() { + return + } + + // 2. use plan data to create device and fill up rest of the model + if err := Create(ctx, r, &planData); err != nil { + res.Diagnostics.AddError("failed to create cdfmc resource", err.Error()) + return + } + + // 3. set state using filled model + res.Diagnostics.Append(res.State.Set(ctx, &planData)...) +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { + // update is noop +} + +func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { + // delete is noop + res.Diagnostics.AddWarning("Delete cdFMC is an noop", "Please reach out to CDO TAC if you really want to delete a cdFMC.") +} diff --git a/provider/internal/cdfmc/resource_test.go b/provider/internal/cdfmc/resource_test.go new file mode 100644 index 00000000..befb701e --- /dev/null +++ b/provider/internal/cdfmc/resource_test.go @@ -0,0 +1,38 @@ +package cdfmc_test + +import ( + "testing" + + "github.com/CiscoDevnet/terraform-provider-cdo/internal/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +var resourceModel = struct { + Name string +}{ + Name: acctest.Env.CloudFmcResourceName(), +} + +const resourceTemplate = ` +resource "cdo_cdfmc" "test" { +}` + +var resourceConfig = acctest.MustParseTemplate(resourceTemplate, resourceModel) + +func TestAccCdFmcResource(t *testing.T) { + t.Skip("we cant delete a fmc so this test cannot be run, or we should find a way to spin up new environment, either seems uneasy, skipping for now.") + resource.Test(t, resource.TestCase{ + PreCheck: acctest.PreCheckFunc(t), + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: acctest.ProviderConfig() + resourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("cdo_cdfmc.test", "name", resourceModel.Name), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} diff --git a/provider/internal/provider/provider.go b/provider/internal/provider/provider.go index 9dad054e..953b1bee 100644 --- a/provider/internal/provider/provider.go +++ b/provider/internal/provider/provider.go @@ -162,6 +162,7 @@ func (p *CdoProvider) Resources(ctx context.Context) []func() resource.Resource user_api_token.NewResource, ftdonboarding.NewResource, connectoronboarding.NewResource, + cdfmc.NewResource, } }