diff --git a/docs/resources/deployment.md b/docs/resources/deployment.md new file mode 100644 index 00000000..eee48786 --- /dev/null +++ b/docs/resources/deployment.md @@ -0,0 +1,39 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "fmc_deployment Resource - terraform-provider-fmc" +subcategory: "Deployment" +description: |- + This resource can manage a Deployment. +--- + +# fmc_deployment (Resource) + +This resource can manage a Deployment. + +## Example Usage + +```terraform +resource "fmc_deployment" "example" { + device_list = ["2fe9063e-8bd5-11ef-9475-e4aeac78cf37"] + deployment_note = "Deployment note" +} +``` + + +## Schema + +### Required + +- `device_list` (List of String) List of device ids to be deployed. If forceDeploy is not set to true, only devices that are deployable will be deployed. + +### Optional + +- `deploy` (Boolean) This attribute is only used internally. +- `deployment_note` (String) User note related to deployment. +- `domain` (String) The name of the FMC domain +- `force_deploy` (Boolean) Force deployment (even if there are no configuration changes). +- `version` (String) Version to which the deployment should be done. By default set to current unix timestamp. + +### Read-Only + +- `id` (String) The id of the object diff --git a/examples/data-sources/fmc_deployment/data-source.tf b/examples/data-sources/fmc_deployment/data-source.tf new file mode 100644 index 00000000..4c6e8ffa --- /dev/null +++ b/examples/data-sources/fmc_deployment/data-source.tf @@ -0,0 +1,3 @@ +data "fmc_deployment" "example" { + id = "76d24097-41c4-4558-a4d0-a8c07ac08470" +} diff --git a/examples/resources/fmc_deployment/resource.tf b/examples/resources/fmc_deployment/resource.tf new file mode 100644 index 00000000..fa9b63f8 --- /dev/null +++ b/examples/resources/fmc_deployment/resource.tf @@ -0,0 +1,4 @@ +resource "fmc_deployment" "example" { + device_list = ["2fe9063e-8bd5-11ef-9475-e4aeac78cf37"] + deployment_note = "Deployment note" +} diff --git a/gen/definitions/deploy_device.yaml b/gen/definitions/deploy_device.yaml new file mode 100644 index 00000000..ef8f64d8 --- /dev/null +++ b/gen/definitions/deploy_device.yaml @@ -0,0 +1,46 @@ +--- +name: Deployment +rest_endpoint: /api/fmc_config/v1/domain/{DOMAIN_UUID}/deployment/deploymentrequests +no_data_source: true +no_import: true +no_delete: true +doc_category: Deployment +test_tags: [TF_VAR_timestamp, TF_VAR_device_id_list] +attributes: + - model_name: type + type: String + value: "DeploymentRequest" + - model_name: version + type: String + description: Version to which the deployment should be done. By default set to current unix timestamp. + exclude_example: true + - model_name: forceDeploy + type: Bool + description: Force deployment (even if there are no configuration changes). + exclude_example: true + exclude_test: true + - model_name: ignoreWarning + type: Bool + value: "true" + - model_name: deviceList + type: List + description: List of device ids to be deployed. If forceDeploy is not set to true, only devices that are deployable will be deployed. + mandatory: true + element_type: String + example: 2fe9063e-8bd5-11ef-9475-e4aeac78cf37 + test_value: var.device_id_list + - model_name: deploymentNote + type: String + description: User note related to deployment. + example: "Deployment note" + exclude_test: true + - model_name: deploy + description: "This attribute is only used internally" + type: Bool + exclude_test: true + exclude_example: true +# test_prerequisites: |- +# variable "device_id_list" { // tests will set $TF_VAR_device_id_list +# type = list(string) +# default = null +# } diff --git a/internal/provider/data_source_fmc_deployment.go b/internal/provider/data_source_fmc_deployment.go new file mode 100644 index 00000000..21ba11e7 --- /dev/null +++ b/internal/provider/data_source_fmc_deployment.go @@ -0,0 +1,138 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "context" + "fmt" + "net/url" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/netascode/go-fmc" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin model + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &DeploymentDataSource{} + _ datasource.DataSourceWithConfigure = &DeploymentDataSource{} +) + +func NewDeploymentDataSource() datasource.DataSource { + return &DeploymentDataSource{} +} + +type DeploymentDataSource struct { + client *fmc.Client +} + +func (d *DeploymentDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_deployment" +} + +func (d *DeploymentDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "This data source can read the Deployment.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The id of the object", + Required: true, + }, + "domain": schema.StringAttribute{ + MarkdownDescription: "The name of the FMC domain", + Optional: true, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "Epoch unix time stamp (13 digits).", + Computed: true, + }, + "force_deploy": schema.BoolAttribute{ + MarkdownDescription: "Force deployment (even if there are no configuration changes).", + Computed: true, + }, + "ignore_warning": schema.BoolAttribute{ + MarkdownDescription: "Ignore warnings during deployment.", + Computed: true, + }, + "device_list": schema.SetAttribute{ + MarkdownDescription: "List of device ids to be deployed.", + ElementType: types.StringType, + Computed: true, + }, + "deployment_note": schema.StringAttribute{ + MarkdownDescription: "User note related to deployment.", + Computed: true, + }, + }, + } +} + +func (d *DeploymentDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + d.client = req.ProviderData.(*FmcProviderData).Client +} + +// End of section. //template:end model + +// Section below is generated&owned by "gen/generator.go". //template:begin read + +func (d *DeploymentDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config Deployment + + // Read config + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Set request domain if provided + reqMods := [](func(*fmc.Req)){} + if !config.Domain.IsNull() && config.Domain.ValueString() != "" { + reqMods = append(reqMods, fmc.DomainName(config.Domain.ValueString())) + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", config.Id.String())) + urlPath := config.getPath() + "/" + url.QueryEscape(config.Id.ValueString()) + res, err := d.client.Get(urlPath, reqMods...) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve object, got error: %s", err)) + return + } + + config.fromBody(ctx, res) + + tflog.Debug(ctx, fmt.Sprintf("%s: Read finished successfully", config.Id.ValueString())) + + diags = resp.State.Set(ctx, &config) + resp.Diagnostics.Append(diags...) +} + +// End of section. //template:end read diff --git a/internal/provider/data_source_fmc_deployment_test.go b/internal/provider/data_source_fmc_deployment_test.go new file mode 100644 index 00000000..212ff0e5 --- /dev/null +++ b/internal/provider/data_source_fmc_deployment_test.go @@ -0,0 +1,80 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccDataSource + +func TestAccDataSourceFmcDeployment(t *testing.T) { + if os.Getenv("TF_VAR_timestamp") == "" && os.Getenv("TF_VAR_device_id_list") == "" { + t.Skip("skipping test, set environment variable TF_VAR_timestamp or TF_VAR_device_id_list") + } + var checks []resource.TestCheckFunc + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceFmcDeploymentPrerequisitesConfig + testAccDataSourceFmcDeploymentConfig(), + Check: resource.ComposeTestCheckFunc(checks...), + }, + }, + }) +} + +// End of section. //template:end testAccDataSource + +// Section below is generated&owned by "gen/generator.go". //template:begin testPrerequisites + +const testAccDataSourceFmcDeploymentPrerequisitesConfig = ` +variable "timestamp" { default = null } // tests will set $TF_VAR_timestamp +variable "device_id_list" { // tests will set $TF_VAR_device_id_list + type = list(string) + default = null +} +` + +// End of section. //template:end testPrerequisites + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccDataSourceConfig + +func testAccDataSourceFmcDeploymentConfig() string { + config := `resource "fmc_deployment" "test" {` + "\n" + config += ` version = var.timestamp` + "\n" + config += ` ignore_warning = true` + "\n" + config += ` device_list = var.device_id_list` + "\n" + config += `}` + "\n" + + config += ` + data "fmc_deployment" "test" { + id = fmc_deployment.test.id + } + ` + return config +} + +// End of section. //template:end testAccDataSourceConfig diff --git a/internal/provider/helpers/utils.go b/internal/provider/helpers/utils.go index 1de59406..a50265d9 100644 --- a/internal/provider/helpers/utils.go +++ b/internal/provider/helpers/utils.go @@ -48,6 +48,14 @@ func GetStringList(result []gjson.Result) types.List { return types.ListValueMust(types.StringType, v) } +func GetStringListFromStringSlice(result []string) types.List { + v := make([]attr.Value, len(result)) + for i, e := range result { + v[i] = types.StringValue(e) + } + return types.ListValueMust(types.StringType, v) +} + func GetInt64List(result []gjson.Result) types.List { v := make([]attr.Value, len(result)) for r := range result { diff --git a/internal/provider/model_fmc_deployment.go b/internal/provider/model_fmc_deployment.go new file mode 100644 index 00000000..461a6445 --- /dev/null +++ b/internal/provider/model_fmc_deployment.go @@ -0,0 +1,168 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/netascode/terraform-provider-fmc/internal/provider/helpers" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin types + +type Deployment struct { + Id types.String `tfsdk:"id"` + Domain types.String `tfsdk:"domain"` + Version types.String `tfsdk:"version"` + ForceDeploy types.Bool `tfsdk:"force_deploy"` + DeviceList types.List `tfsdk:"device_list"` + DeploymentNote types.String `tfsdk:"deployment_note"` + Deploy types.Bool `tfsdk:"deploy"` +} + +// End of section. //template:end types + +// Section below is generated&owned by "gen/generator.go". //template:begin getPath + +func (data Deployment) getPath() string { + return "/api/fmc_config/v1/domain/{DOMAIN_UUID}/deployment/deploymentrequests" +} + +// End of section. //template:end getPath + +// Section below is generated&owned by "gen/generator.go". //template:begin toBody + +func (data Deployment) toBody(ctx context.Context, state Deployment) string { + body := "" + if data.Id.ValueString() != "" { + body, _ = sjson.Set(body, "id", data.Id.ValueString()) + } + body, _ = sjson.Set(body, "type", "DeploymentRequest") + if !data.Version.IsNull() { + body, _ = sjson.Set(body, "version", data.Version.ValueString()) + } + if !data.ForceDeploy.IsNull() { + body, _ = sjson.Set(body, "forceDeploy", data.ForceDeploy.ValueBool()) + } + body, _ = sjson.Set(body, "ignoreWarning", true) + if !data.DeviceList.IsNull() { + var values []string + data.DeviceList.ElementsAs(ctx, &values, false) + body, _ = sjson.Set(body, "deviceList", values) + } + if !data.DeploymentNote.IsNull() { + body, _ = sjson.Set(body, "deploymentNote", data.DeploymentNote.ValueString()) + } + if !data.Deploy.IsNull() { + body, _ = sjson.Set(body, "deploy", data.Deploy.ValueBool()) + } + return body +} + +// End of section. //template:end toBody + +// Section below is generated&owned by "gen/generator.go". //template:begin fromBody + +func (data *Deployment) fromBody(ctx context.Context, res gjson.Result) { + if value := res.Get("version"); value.Exists() { + data.Version = types.StringValue(value.String()) + } else { + data.Version = types.StringNull() + } + if value := res.Get("forceDeploy"); value.Exists() { + data.ForceDeploy = types.BoolValue(value.Bool()) + } else { + data.ForceDeploy = types.BoolNull() + } + if value := res.Get("deviceList"); value.Exists() { + data.DeviceList = helpers.GetStringList(value.Array()) + } else { + data.DeviceList = types.ListNull(types.StringType) + } + if value := res.Get("deploymentNote"); value.Exists() { + data.DeploymentNote = types.StringValue(value.String()) + } else { + data.DeploymentNote = types.StringNull() + } + if value := res.Get("deploy"); value.Exists() { + data.Deploy = types.BoolValue(value.Bool()) + } else { + data.Deploy = types.BoolNull() + } +} + +// End of section. //template:end fromBody + +// Section below is generated&owned by "gen/generator.go". //template:begin fromBodyPartial + +// fromBodyPartial reads values from a gjson.Result into a tfstate model. It ignores null attributes in order to +// uncouple the provider from the exact values that the backend API might summon to replace nulls. (Such behavior might +// easily change across versions of the backend API.) For List/Set/Map attributes, the func only updates the +// "managed" elements, instead of all elements. +func (data *Deployment) fromBodyPartial(ctx context.Context, res gjson.Result) { + if value := res.Get("version"); value.Exists() && !data.Version.IsNull() { + data.Version = types.StringValue(value.String()) + } else { + data.Version = types.StringNull() + } + if value := res.Get("forceDeploy"); value.Exists() && !data.ForceDeploy.IsNull() { + data.ForceDeploy = types.BoolValue(value.Bool()) + } else { + data.ForceDeploy = types.BoolNull() + } + if value := res.Get("deviceList"); value.Exists() && !data.DeviceList.IsNull() { + data.DeviceList = helpers.GetStringList(value.Array()) + } else { + data.DeviceList = types.ListNull(types.StringType) + } + if value := res.Get("deploymentNote"); value.Exists() && !data.DeploymentNote.IsNull() { + data.DeploymentNote = types.StringValue(value.String()) + } else { + data.DeploymentNote = types.StringNull() + } + if value := res.Get("deploy"); value.Exists() && !data.Deploy.IsNull() { + data.Deploy = types.BoolValue(value.Bool()) + } else { + data.Deploy = types.BoolNull() + } +} + +// End of section. //template:end fromBodyPartial + +// Section below is generated&owned by "gen/generator.go". //template:begin fromBodyUnknowns + +// fromBodyUnknowns updates the Unknown Computed tfstate values from a JSON. +// Known values are not changed (usual for Computed attributes with UseStateForUnknown or with Default). +func (data *Deployment) fromBodyUnknowns(ctx context.Context, res gjson.Result) { +} + +// End of section. //template:end fromBodyUnknowns + +// Section below is generated&owned by "gen/generator.go". //template:begin Clone + +// End of section. //template:end Clone + +// Section below is generated&owned by "gen/generator.go". //template:begin toBodyNonBulk + +// End of section. //template:end toBodyNonBulk diff --git a/internal/provider/provider.go b/internal/provider/provider.go index da76c28d..469d58d9 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -293,6 +293,7 @@ func (p *FmcProvider) Configure(ctx context.Context, req provider.ConfigureReque func (p *FmcProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewAccessControlPolicyResource, + NewDeploymentResource, NewDeviceResource, NewDeviceEtherChannelInterfaceResource, NewDeviceIPv4StaticRouteResource, diff --git a/internal/provider/resource_fmc_deployment.go b/internal/provider/resource_fmc_deployment.go new file mode 100644 index 00000000..0ddb742e --- /dev/null +++ b/internal/provider/resource_fmc_deployment.go @@ -0,0 +1,303 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "strconv" + "time" + + "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/booldefault" + "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" + "github.com/netascode/go-fmc" + "github.com/netascode/terraform-provider-fmc/internal/provider/helpers" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// End of section. //template:end imports + +// Ensure provider defined types fully satisfy framework interfaces +var ( + _ resource.Resource = &DeploymentResource{} +) + +func NewDeploymentResource() resource.Resource { + return &DeploymentResource{} +} + +type DeploymentResource struct { + client *fmc.Client +} + +func (r *DeploymentResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_deployment" +} + +func (r *DeploymentResource) 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: helpers.NewAttributeDescription("This resource can manage a Deployment.").String, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The id of the object", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "domain": schema.StringAttribute{ + MarkdownDescription: "The name of the FMC domain", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "version": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Version to which the deployment should be done. By default set to current unix timestamp.").String, + Optional: true, + Computed: true, + }, + "force_deploy": schema.BoolAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("Force deployment (even if there are no configuration changes).").String, + Optional: true, + }, + "device_list": schema.ListAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("List of device ids to be deployed. If forceDeploy is not set to true, only devices that are deployable will be deployed.").String, + ElementType: types.StringType, + Required: true, + }, + "deployment_note": schema.StringAttribute{ + MarkdownDescription: helpers.NewAttributeDescription("User note related to deployment.").String, + Optional: true, + }, + "deploy": schema.BoolAttribute{ + MarkdownDescription: "This attribute is only used internally.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + }, + } +} + +func (r *DeploymentResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*FmcProviderData).Client +} + +func (r *DeploymentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan Deployment + + // Read plan + diags := req.Plan.Get(ctx, &plan) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + // Set request domain if provided + reqMods := [](func(*fmc.Req)){} + if !plan.Domain.IsNull() && plan.Domain.ValueString() != "" { + reqMods = append(reqMods, fmc.DomainName(plan.Domain.ValueString())) + } + + // If version not provided, set it to the current unix timestamp + if plan.Version.IsUnknown() { + plan.Version = types.StringValue(strconv.FormatInt(time.Now().UnixMilli(), 10)) + } + + origDeviceList := plan.DeviceList + + if !plan.ForceDeploy.ValueBool() { + // Get list of deployable devices + resDeployable, err := r.client.Get("/api/fmc_config/v1/domain/{DOMAIN_UUID}/deployment/deployabledevices?expanded=true", reqMods...) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to obtain list of deployable devices object (GET), got error: %s, %s", err, resDeployable.String())) + return + } + deployableDevices := resDeployable.Get("items.#.device.id") + + var deployableDeviceIds []string + deployableDevices.ForEach(func(_, value gjson.Result) bool { + deployableDeviceIds = append(deployableDeviceIds, value.String()) + return true + }) + + var deviceIdsList []string + plan.DeviceList.ElementsAs(ctx, &deviceIdsList, false) + + devicesToDeploy := filterForDeployableDevices(deviceIdsList, deployableDeviceIds) + + plan.DeviceList = helpers.GetStringListFromStringSlice(devicesToDeploy) + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Create", plan.Id.ValueString())) + + var deviceIdsSlice []string + plan.DeviceList.ElementsAs(ctx, &deviceIdsSlice, false) + + if len(deviceIdsSlice) != 0 { + // Create object + body := plan.toBody(ctx, Deployment{}) + body, _ = sjson.Delete(body, "deploy") + res, err := r.client.Post(plan.getPath(), body, reqMods...) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (POST/PUT), got error: %s, %s", err, res.String())) + return + } + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Create finished successfully", plan.Id.ValueString())) + + plan.DeviceList = origDeviceList + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + + helpers.SetFlagImporting(ctx, false, resp.Private, &resp.Diagnostics) +} + +func (r *DeploymentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + resp.State.SetAttribute(ctx, path.Root("deploy"), false) +} + +func (r *DeploymentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan Deployment + + // Read plan + diags := req.Plan.Get(ctx, &plan) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + // Set request domain if provided + reqMods := [](func(*fmc.Req)){} + if !plan.Domain.IsNull() && plan.Domain.ValueString() != "" { + reqMods = append(reqMods, fmc.DomainName(plan.Domain.ValueString())) + } + + // If version not provided, set it to the current unix timestamp + if plan.Version.IsUnknown() { + plan.Version = types.StringValue(strconv.FormatInt(time.Now().UnixMilli(), 10)) + } + + origDeviceList := plan.DeviceList + + if !plan.ForceDeploy.ValueBool() { + // Get list of deployable devices + resDeployable, err := r.client.Get("/api/fmc_config/v1/domain/{DOMAIN_UUID}/deployment/deployabledevices?expanded=true", reqMods...) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to obtain list of deployable devices object (GET), got error: %s, %s", err, resDeployable.String())) + return + } + deployableDevices := resDeployable.Get("items.#.device.id") + + var deployableDeviceIds []string + deployableDevices.ForEach(func(_, value gjson.Result) bool { + deployableDeviceIds = append(deployableDeviceIds, value.String()) + return true + }) + + var deviceIdsList []string + plan.DeviceList.ElementsAs(ctx, &deviceIdsList, false) + + devicesToDeploy := filterForDeployableDevices(deviceIdsList, deployableDeviceIds) + + plan.DeviceList = helpers.GetStringListFromStringSlice(devicesToDeploy) + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Create", plan.Id.ValueString())) + + var deviceIdsSlice []string + plan.DeviceList.ElementsAs(ctx, &deviceIdsSlice, false) + + if len(deviceIdsSlice) != 0 { + // Create object + body := plan.toBody(ctx, Deployment{}) + body, _ = sjson.Delete(body, "deploy") + res, err := r.client.Post(plan.getPath(), body, reqMods...) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (POST/PUT), got error: %s, %s", err, res.String())) + return + } + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Create finished successfully", plan.Id.ValueString())) + + plan.DeviceList = origDeviceList + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + + helpers.SetFlagImporting(ctx, false, resp.Private, &resp.Diagnostics) +} + +// Section below is generated&owned by "gen/generator.go". //template:begin delete + +func (r *DeploymentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state Deployment + + // Read state + diags := req.State.Get(ctx, &state) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + // Set request domain if provided + reqMods := [](func(*fmc.Req)){} + if !state.Domain.IsNull() && state.Domain.ValueString() != "" { + reqMods = append(reqMods, fmc.DomainName(state.Domain.ValueString())) + } + + tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Delete", state.Id.ValueString())) + + tflog.Debug(ctx, fmt.Sprintf("%s: Delete finished successfully", state.Id.ValueString())) + + resp.State.RemoveResource(ctx) +} + +// End of section. //template:end delete + +// Checks what devices from those that were requested for deployment are actually deployable +// Function returns list of requested device ids that are deployable +func filterForDeployableDevices(listOfDevices, deployableDevices []string) []string { + devicesMap := make(map[string]bool) + for _, str := range listOfDevices { + devicesMap[str] = true + } + + var result []string + for _, str := range deployableDevices { + if devicesMap[str] { + result = append(result, str) + // Remove the element to avoid duplicates in the result + delete(devicesMap, str) + } + } + + return result +} diff --git a/internal/provider/resource_fmc_deployment_test.go b/internal/provider/resource_fmc_deployment_test.go new file mode 100644 index 00000000..b6cd1778 --- /dev/null +++ b/internal/provider/resource_fmc_deployment_test.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Mozilla Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://mozilla.org/MPL/2.0/ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: MPL-2.0 + +package provider + +// Section below is generated&owned by "gen/generator.go". //template:begin imports +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +// End of section. //template:end imports + +// Section below is generated&owned by "gen/generator.go". //template:begin testAcc + +func TestAccFmcDeployment(t *testing.T) { + if os.Getenv("TF_VAR_timestamp") == "" || os.Getenv("TF_VAR_device_id_list") == "" { + t.Skip("skipping test, set environment variable TF_VAR_timestamp and TF_VAR_device_id_list") + } + var checks []resource.TestCheckFunc + checks = append(checks, resource.TestCheckResourceAttr("fmc_deployment.test", "version", "")) + + var steps []resource.TestStep + if os.Getenv("SKIP_MINIMUM_TEST") == "" { + steps = append(steps, resource.TestStep{ + Config: testAccFmcDeploymentConfig_minimum(), + }) + } + steps = append(steps, resource.TestStep{ + Config: testAccFmcDeploymentConfig_all(), + Check: resource.ComposeTestCheckFunc(checks...), + }) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + ErrorCheck: func(err error) error { return testAccErrorCheck(t, err) }, + Steps: steps, + }) +} + +// End of section. //template:end testAcc + +// Section below is generated&owned by "gen/generator.go". //template:begin testPrerequisites +// End of section. //template:end testPrerequisites + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccConfigMinimal + +func testAccFmcDeploymentConfig_minimum() string { + config := `resource "fmc_deployment" "test" {` + "\n" + config += ` device_list = var.device_id_list` + "\n" + config += `}` + "\n" + return config +} + +// End of section. //template:end testAccConfigMinimal + +// Section below is generated&owned by "gen/generator.go". //template:begin testAccConfigAll + +func testAccFmcDeploymentConfig_all() string { + config := `resource "fmc_deployment" "test" {` + "\n" + config += ` version = ""` + "\n" + config += ` device_list = var.device_id_list` + "\n" + config += `}` + "\n" + return config +} + +// End of section. //template:end testAccConfigAll