diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d9cb6be..ba602e0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
- Add `sdwan_cisco_ospf_feature_template` resource and data source
- Add `sdwan_cisco_vpn_interface_ipsec_feature_template` resource and data source
- Add `sdwan_cisco_secure_internet_gateway_feature_template` resource and data source
+- Add `sdwan_hub_and_spoke_topology_policy_definition` resource and data source
## 0.2.0
diff --git a/docs/data-sources/hub_and_spoke_topology_policy_definition.md b/docs/data-sources/hub_and_spoke_topology_policy_definition.md
new file mode 100644
index 00000000..1947dead
--- /dev/null
+++ b/docs/data-sources/hub_and_spoke_topology_policy_definition.md
@@ -0,0 +1,61 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "sdwan_hub_and_spoke_topology_policy_definition Data Source - terraform-provider-sdwan"
+subcategory: "Centralized Policies"
+description: |-
+ This data source can read the Hub and Spoke Topology policy definition.
+---
+
+# sdwan_hub_and_spoke_topology_policy_definition (Data Source)
+
+This data source can read the Hub and Spoke Topology policy definition.
+
+## Example Usage
+
+```terraform
+data "sdwan_hub_and_spoke_topology_policy_definition" "example" {
+ id = "f6b2c44c-693c-4763-b010-895aa3d236bd"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `id` (String) The id of the policy definition
+
+### Read-Only
+
+- `description` (String) The description of the policy definition
+- `name` (String) The name of the policy definition
+- `topologies` (Attributes List) List of topologies (see [below for nested schema](#nestedatt--topologies))
+- `type` (String) The policy definition type
+- `version` (Number) The version of the policy definition
+- `vpn_list_id` (String) VPN list ID
+- `vpn_list_version` (Number) VPN list version
+
+
+### Nested Schema for `topologies`
+
+Read-Only:
+
+- `name` (String) Topology name
+- `spokes` (Attributes List) List of spokes (see [below for nested schema](#nestedatt--topologies--spokes))
+
+
+### Nested Schema for `topologies.spokes`
+
+Read-Only:
+
+- `hubs` (Attributes List) List of hubs (see [below for nested schema](#nestedatt--topologies--spokes--hubs))
+- `site_list_id` (String) Site list ID
+- `site_list_version` (Number) Site list version
+
+
+### Nested Schema for `topologies.spokes.hubs`
+
+Read-Only:
+
+- `site_list_id` (String) Site list ID
+- `site_list_version` (Number) Site list version
diff --git a/docs/guides/changelog.md b/docs/guides/changelog.md
index d16a076b..b52aad27 100644
--- a/docs/guides/changelog.md
+++ b/docs/guides/changelog.md
@@ -12,6 +12,7 @@ description: |-
- Add `sdwan_cisco_ospf_feature_template` resource and data source
- Add `sdwan_cisco_vpn_interface_ipsec_feature_template` resource and data source
- Add `sdwan_cisco_secure_internet_gateway_feature_template` resource and data source
+- Add `sdwan_hub_and_spoke_topology_policy_definition` resource and data source
## 0.2.0
diff --git a/docs/resources/hub_and_spoke_topology_policy_definition.md b/docs/resources/hub_and_spoke_topology_policy_definition.md
new file mode 100644
index 00000000..fcfd0fec
--- /dev/null
+++ b/docs/resources/hub_and_spoke_topology_policy_definition.md
@@ -0,0 +1,92 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "sdwan_hub_and_spoke_topology_policy_definition Resource - terraform-provider-sdwan"
+subcategory: "Centralized Policies"
+description: |-
+ This resource can manage a Hub and Spoke Topology policy definition.
+---
+
+# sdwan_hub_and_spoke_topology_policy_definition (Resource)
+
+This resource can manage a Hub and Spoke Topology policy definition.
+
+## Example Usage
+
+```terraform
+resource "sdwan_hub_and_spoke_topology_policy_definition" "example" {
+ name = "Example"
+ description = "My description"
+ vpn_list_id = "04fcbb0b-efbf-43d2-a04b-847d3a7b104e"
+ topologies = [
+ {
+ name = "Topology1"
+ spokes = [
+ {
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ hubs = [
+ {
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
+
+
+## Schema
+
+### Required
+
+- `description` (String) The description of the policy definition
+- `name` (String) The name of the policy definition
+- `topologies` (Attributes List) List of topologies (see [below for nested schema](#nestedatt--topologies))
+
+### Optional
+
+- `vpn_list_id` (String) VPN list ID
+- `vpn_list_version` (Number) VPN list version
+
+### Read-Only
+
+- `id` (String) The id of the policy definition
+- `type` (String) The policy defintion type
+- `version` (Number) The version of the policy definition
+
+
+### Nested Schema for `topologies`
+
+Required:
+
+- `name` (String) Topology name
+
+Optional:
+
+- `spokes` (Attributes List) List of spokes (see [below for nested schema](#nestedatt--topologies--spokes))
+
+
+### Nested Schema for `topologies.spokes`
+
+Optional:
+
+- `hubs` (Attributes List) List of hubs (see [below for nested schema](#nestedatt--topologies--spokes--hubs))
+- `site_list_id` (String) Site list ID
+- `site_list_version` (Number) Site list version
+
+
+### Nested Schema for `topologies.spokes.hubs`
+
+Optional:
+
+- `site_list_id` (String) Site list ID
+- `site_list_version` (Number) Site list version
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import sdwan_hub_and_spoke_topology_policy_definition.example "f6b2c44c-693c-4763-b010-895aa3d236bd"
+```
diff --git a/examples/data-sources/sdwan_hub_and_spoke_topology_policy_definition/data-source.tf b/examples/data-sources/sdwan_hub_and_spoke_topology_policy_definition/data-source.tf
new file mode 100644
index 00000000..d71047b7
--- /dev/null
+++ b/examples/data-sources/sdwan_hub_and_spoke_topology_policy_definition/data-source.tf
@@ -0,0 +1,3 @@
+data "sdwan_hub_and_spoke_topology_policy_definition" "example" {
+ id = "f6b2c44c-693c-4763-b010-895aa3d236bd"
+}
diff --git a/examples/resources/sdwan_hub_and_spoke_topology_policy_definition/import.sh b/examples/resources/sdwan_hub_and_spoke_topology_policy_definition/import.sh
new file mode 100644
index 00000000..799b66b2
--- /dev/null
+++ b/examples/resources/sdwan_hub_and_spoke_topology_policy_definition/import.sh
@@ -0,0 +1 @@
+terraform import sdwan_hub_and_spoke_topology_policy_definition.example "f6b2c44c-693c-4763-b010-895aa3d236bd"
diff --git a/examples/resources/sdwan_hub_and_spoke_topology_policy_definition/resource.tf b/examples/resources/sdwan_hub_and_spoke_topology_policy_definition/resource.tf
new file mode 100644
index 00000000..a7ba0b30
--- /dev/null
+++ b/examples/resources/sdwan_hub_and_spoke_topology_policy_definition/resource.tf
@@ -0,0 +1,20 @@
+resource "sdwan_hub_and_spoke_topology_policy_definition" "example" {
+ name = "Example"
+ description = "My description"
+ vpn_list_id = "04fcbb0b-efbf-43d2-a04b-847d3a7b104e"
+ topologies = [
+ {
+ name = "Topology1"
+ spokes = [
+ {
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ hubs = [
+ {
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/gen/definitions/policy_definitions/acl.yaml b/gen/definitions/policy_definitions/acl.yaml
index 940a87a8..44ad62bb 100644
--- a/gen/definitions/policy_definitions/acl.yaml
+++ b/gen/definitions/policy_definitions/acl.yaml
@@ -1,6 +1,7 @@
---
name: ACL
type: acl
+doc_category: Localized Policies
skip_templates: [model.go, resource.go]
attributes:
- model_name: type
diff --git a/gen/definitions/policy_definitions/device_acl.yaml b/gen/definitions/policy_definitions/device_acl.yaml
index 66c89ade..2805952b 100644
--- a/gen/definitions/policy_definitions/device_acl.yaml
+++ b/gen/definitions/policy_definitions/device_acl.yaml
@@ -1,6 +1,7 @@
---
name: Device ACL
type: deviceAccessPolicy
+doc_category: Localized Policies
skip_templates: [model.go, resource.go]
attributes:
- model_name: type
diff --git a/gen/definitions/policy_definitions/hub_and_spoke_topology.yaml b/gen/definitions/policy_definitions/hub_and_spoke_topology.yaml
new file mode 100644
index 00000000..4e09c3f7
--- /dev/null
+++ b/gen/definitions/policy_definitions/hub_and_spoke_topology.yaml
@@ -0,0 +1,58 @@
+---
+name: Hub and Spoke Topology
+type: hubAndSpoke
+root_element: definition
+doc_category: Centralized Policies
+attributes:
+ - model_name: vpnList
+ tf_name: vpn_list_id
+ type: String
+ description: VPN list ID
+ example: 04fcbb0b-efbf-43d2-a04b-847d3a7b104e
+ - tf_name: vpn_list_version
+ tf_only: true
+ type: Int64
+ description: VPN list version
+ exclude_test: true
+ - model_name: subDefinitions
+ tf_name: topologies
+ type: List
+ mandatory: true
+ description: List of topologies
+ attributes:
+ - model_name: name
+ tf_name: name
+ type: String
+ mandatory: true
+ description: Topology name
+ example: Topology1
+ - model_name: spokes
+ tf_name: spokes
+ type: List
+ description: List of spokes
+ attributes:
+ - model_name: siteList
+ tf_name: site_list_id
+ type: String
+ description: Site list ID
+ example: e858e1c4-6aa8-4de7-99df-c3adbf80290d
+ - tf_name: site_list_version
+ tf_only: true
+ type: Int64
+ description: Site list version
+ exclude_test: true
+ - model_name: hubs
+ tf_name: hubs
+ type: List
+ description: List of hubs
+ attributes:
+ - model_name: siteList
+ tf_name: site_list_id
+ type: String
+ description: Site list ID
+ example: e858e1c4-6aa8-4de7-99df-c3adbf80290d
+ - tf_name: site_list_version
+ tf_only: true
+ type: Int64
+ description: Site list version
+ exclude_test: true
diff --git a/gen/definitions/policy_definitions/qos_map.yaml b/gen/definitions/policy_definitions/qos_map.yaml
index b85d9413..93ea0de6 100644
--- a/gen/definitions/policy_definitions/qos_map.yaml
+++ b/gen/definitions/policy_definitions/qos_map.yaml
@@ -2,6 +2,7 @@
name: QoS Map
type: qosMap
root_element: definition
+doc_category: Localized Policies
skip_templates: [data_source_test.go, resource_test.go, model.go, resource.go]
attributes:
- model_name: qosSchedulers
diff --git a/gen/definitions/policy_definitions/rewrite_policy.yaml b/gen/definitions/policy_definitions/rewrite_policy.yaml
index 17f0ce83..770fe3ad 100644
--- a/gen/definitions/policy_definitions/rewrite_policy.yaml
+++ b/gen/definitions/policy_definitions/rewrite_policy.yaml
@@ -2,6 +2,7 @@
name: Rewrite Rule
type: rewriteRule
root_element: definition
+doc_category: Localized Policies
skip_templates: [data_source_test.go, resource_test.go, model.go, resource.go]
attributes:
- model_name: rules
diff --git a/gen/definitions/policy_definitions/route.yaml b/gen/definitions/policy_definitions/route.yaml
index 19a0f954..ae5b6c66 100644
--- a/gen/definitions/policy_definitions/route.yaml
+++ b/gen/definitions/policy_definitions/route.yaml
@@ -1,6 +1,7 @@
---
name: Route
type: vedgeRoute
+doc_category: Localized Policies
skip_templates: [model.go, resource.go, data_source.go]
attributes:
- model_name: type
diff --git a/gen/doc_category.go b/gen/doc_category.go
index 28461dac..39083a11 100644
--- a/gen/doc_category.go
+++ b/gen/doc_category.go
@@ -35,7 +35,8 @@ const (
)
type YamlConfig struct {
- Name string `yaml:"name"`
+ Name string `yaml:"name"`
+ DocCategory string `yaml:"doc_category"`
}
var docPaths = []string{"./docs/data-sources/", "./docs/resources/"}
@@ -152,8 +153,9 @@ func main() {
log.Fatalf("Error opening documentation: %v", err)
}
+ cat := policyDefinitionConfigs[i].DocCategory
s := string(content)
- s = strings.ReplaceAll(s, `subcategory: ""`, `subcategory: "Localized Policies"`)
+ s = strings.ReplaceAll(s, `subcategory: ""`, `subcategory: "`+cat+`"`)
ioutil.WriteFile(filename, []byte(s), 0644)
}
diff --git a/gen/templates/policy_definitions/data_source.go b/gen/templates/policy_definitions/data_source.go
index ebcf04d3..0576651c 100644
--- a/gen/templates/policy_definitions/data_source.go
+++ b/gen/templates/policy_definitions/data_source.go
@@ -108,6 +108,23 @@ func (d *{{camelCase .Name}}PolicyDefinitionDataSource) Schema(ctx context.Conte
ElementType: types.StringType,
{{- end}}
Computed: true,
+ {{- if eq .Type "List"}}
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ {{- range .Attributes}}
+ {{- if not .Value}}
+ "{{.TfName}}": schema.{{if eq .Type "List"}}ListNested{{else if eq .Type "ListString"}}List{{else}}{{.Type}}{{end}}Attribute{
+ MarkdownDescription: "{{.Description}}",
+ {{- if eq .Type "ListString"}}
+ ElementType: types.StringType,
+ {{- end}}
+ Computed: true,
+ },
+ {{- end}}
+ {{- end}}
+ },
+ },
+ {{- end}}
},
{{- end}}
{{- end}}
diff --git a/gen/templates/policy_definitions/data_source_test.go b/gen/templates/policy_definitions/data_source_test.go
index cc822ee8..8521a035 100644
--- a/gen/templates/policy_definitions/data_source_test.go
+++ b/gen/templates/policy_definitions/data_source_test.go
@@ -47,9 +47,18 @@ func TestAccDataSourceSdwan{{camelCase .Name}}PolicyDefinition(t *testing.T) {
{{- $clist := .TfName }}
{{- range .Attributes}}
{{- if and (not .WriteOnly) (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ {{- if eq .Type "List"}}
+ {{- $cclist := .TfName }}
+ {{- range .Attributes}}
+ {{- if and (not .WriteOnly) (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ resource.TestCheckResourceAttr("data.sdwan_{{snakeCase $name}}_policy_definition.test", "{{$list}}.0.{{$clist}}.0.{{$cclist}}.0.{{.TfName}}", "{{.Example}}"),
+ {{- end}}
+ {{- end}}
+ {{- else}}
resource.TestCheckResourceAttr("data.sdwan_{{snakeCase $name}}_policy_definition.test", "{{$list}}.0.{{$clist}}.0.{{.TfName}}", "{{.Example}}"),
{{- end}}
{{- end}}
+ {{- end}}
{{- else}}
resource.TestCheckResourceAttr("data.sdwan_{{snakeCase $name}}_policy_definition.test", "{{$list}}.0.{{.TfName}}", "{{.Example}}"),
{{- end}}
@@ -81,9 +90,19 @@ resource "sdwan_{{snakeCase $name}}_policy_definition" "test" {
{{.TfName}} = [{
{{- range .Attributes}}
{{- if and (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ {{- if eq .Type "List"}}
+ {{.TfName}} = [{
+ {{- range .Attributes}}
+ {{- if and (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ {{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
+ {{- end}}
+ {{- end}}
+ }]
+ {{- else}}
{{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
{{- end}}
{{- end}}
+ {{- end}}
}]
{{- else}}
{{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
diff --git a/gen/templates/policy_definitions/model.go b/gen/templates/policy_definitions/model.go
index 355c09e5..977f3c1e 100644
--- a/gen/templates/policy_definitions/model.go
+++ b/gen/templates/policy_definitions/model.go
@@ -79,10 +79,42 @@ type {{$name}}{{toGoName .TfName}} struct {
{{- if eq .Type "List"}}
{{ range .Attributes}}
{{- if not .Value}}
+{{- $childChildName := toGoName .TfName}}
{{- if eq .Type "List"}}
type {{$name}}{{$childName}}{{toGoName .TfName}} struct {
{{- range .Attributes}}
{{- if not .Value}}
+{{- if eq .Type "List"}}
+ {{toGoName .TfName}} []{{$name}}{{$childName}}{{$childChildName}}{{toGoName .TfName}} `tfsdk:"{{.TfName}}"`
+{{- else if eq .Type "ListString"}}
+ {{toGoName .TfName}} types.List `tfsdk:"{{.TfName}}"`
+{{- else}}
+ {{toGoName .TfName}} types.{{.Type}} `tfsdk:"{{.TfName}}"`
+{{- end}}
+{{- end}}
+{{- end}}
+}
+{{- end}}
+{{- end}}
+{{- end}}
+{{- end}}
+{{- end}}
+{{ end}}
+
+{{ range .Attributes}}
+{{- if not .Value}}
+{{- $childName := toGoName .TfName}}
+{{- if eq .Type "List"}}
+{{ range .Attributes}}
+{{- if not .Value}}
+{{- $childChildName := toGoName .TfName}}
+{{- if eq .Type "List"}}
+{{ range .Attributes}}
+{{- if not .Value}}
+{{- if eq .Type "List"}}
+type {{$name}}{{$childName}}{{$childChildName}}{{toGoName .TfName}} struct {
+{{- range .Attributes}}
+{{- if not .Value}}
{{- if eq .Type "ListString"}}
{{toGoName .TfName}} types.List `tfsdk:"{{.TfName}}"`
{{- else}}
@@ -96,6 +128,9 @@ type {{$name}}{{$childName}}{{toGoName .TfName}} struct {
{{- end}}
{{- end}}
{{- end}}
+{{- end}}
+{{- end}}
+{{- end}}
{{ end}}
func (data {{camelCase .Name}}) getType() string {
@@ -149,15 +184,40 @@ func (data {{camelCase .Name}}) toBody(ctx context.Context) string {
{{- if .Value}}
itemChildBody, _ = sjson.Set(itemChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", "{{.Value}}")
{{- else if not .TfOnly}}
- {{- if eq .Type "ListString"}}
+ {{- if or (eq .Type "String") (eq .Type "Int64") (eq .Type "Float64")}}
+ if !childItem.{{toGoName .TfName}}.IsNull() {
+ itemChildBody, _ = sjson.Set(itemChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", {{if .ModelTypeString}}fmt.Sprint({{end}}childItem.{{toGoName .TfName}}.Value{{.Type}}(){{if .ModelTypeString}}){{end}})
+ }
+ {{- else if eq .Type "ListString"}}
if !childItem.{{toGoName .TfName}}.IsNull() {
var values []string
childItem.{{toGoName .TfName}}.ElementsAs(ctx, &values, false)
itemChildBody, _ = sjson.Set(itemChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", values)
}
- {{- else}}
- if !childItem.{{toGoName .TfName}}.IsNull() {
- itemChildBody, _ = sjson.Set(itemChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", {{if .ModelTypeString}}fmt.Sprint({{end}}childItem.{{toGoName .TfName}}.Value{{.Type}}(){{if .ModelTypeString}}){{end}})
+ {{- else if eq .Type "List"}}
+ if len(childItem.{{toGoName .TfName}}) > 0 {
+ itemChildBody, _ = sjson.Set(itemChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", []interface{}{})
+ for _, childChildItem := range childItem.{{toGoName .TfName}} {
+ itemChildChildBody := ""
+ {{- range .Attributes}}
+ {{- if .Value}}
+ itemChildChildBody, _ = sjson.Set(itemChildChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", "{{.Value}}")
+ {{- else if not .TfOnly}}
+ {{- if or (eq .Type "String") (eq .Type "Int64") (eq .Type "Float64")}}
+ if !childChildItem.{{toGoName .TfName}}.IsNull() {
+ itemChildChildBody, _ = sjson.Set(itemChildChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", {{if .ModelTypeString}}fmt.Sprint({{end}}childChildItem.{{toGoName .TfName}}.Value{{.Type}}(){{if .ModelTypeString}}){{end}})
+ }
+ {{- else if eq .Type "ListString"}}
+ if !childChildItem.{{toGoName .TfName}}.IsNull() {
+ var values []string
+ childChildItem.{{toGoName .TfName}}.ElementsAs(ctx, &values, false)
+ itemChildChildBody, _ = sjson.Set(itemChildChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}", values)
+ }
+ {{- end}}
+ {{- end}}
+ {{- end}}
+ itemChildBody, _ = sjson.SetRaw(itemChildBody, "{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}.-1", itemChildChildBody)
+ }
}
{{- end}}
{{- end}}
@@ -227,6 +287,7 @@ func (data *{{camelCase .Name}}) fromBody(ctx context.Context, res gjson.Result)
value.ForEach(func(k, v gjson.Result) bool {
item := {{$name}}{{toGoName .TfName}}{}
{{- range .Attributes}}
+ {{- $ccname := toGoName .TfName}}
{{- if and (not .TfOnly) (not .Value)}}
{{- if eq .Type "String"}}
if cValue := v.Get("{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}"); cValue.Exists() {
@@ -283,6 +344,44 @@ func (data *{{camelCase .Name}}) fromBody(ctx context.Context, res gjson.Result)
} else {
cItem.{{toGoName .TfName}} = types.ListNull(types.StringType)
}
+ {{- else if eq .Type "List"}}
+ if ccValue := cv.Get("{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}"); ccValue.Exists() {
+ cItem.{{toGoName .TfName}} = make([]{{$name}}{{$cname}}{{$ccname}}{{toGoName .TfName}}, 0)
+ ccValue.ForEach(func(cck, ccv gjson.Result) bool {
+ ccItem := {{$name}}{{$cname}}{{$ccname}}{{toGoName .TfName}}{}
+ {{- range .Attributes}}
+ {{- if and (not .TfOnly) (not .Value)}}
+ {{- if eq .Type "String"}}
+ if cccValue := ccv.Get("{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}"); cccValue.Exists() {
+ ccItem.{{toGoName .TfName}} = types.StringValue(cccValue.String())
+ } else {
+ ccItem.{{toGoName .TfName}} = types.StringNull()
+ }
+ {{- else if eq .Type "Int64"}}
+ if cccValue := ccv.Get("{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}"); cccValue.Exists() {
+ ccItem.{{toGoName .TfName}} = types.Int64Value(cccValue.Int())
+ } else {
+ ccItem.{{toGoName .TfName}} = types.Int64Null()
+ }
+ {{- else if eq .Type "Float64"}}
+ if cccValue := ccv.Get("{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}"); cccValue.Exists() {
+ ccItem.{{toGoName .TfName}} = types.Float64Value(cccValue.Float())
+ } else {
+ ccItem.{{toGoName .TfName}} = types.Float64Null()
+ }
+ {{- else if eq .Type "ListString"}}
+ if cccValue := ccv.Get("{{range .DataPath}}{{.}}.{{end}}{{.ModelName}}"); cccValue.Exists() {
+ ccItem.{{toGoName .TfName}} = helpers.GetStringList(cccValue.Array())
+ } else {
+ ccItem.{{toGoName .TfName}} = types.ListNull(types.StringType)
+ }
+ {{- end}}
+ {{- end}}
+ {{- end}}
+ cItem.{{toGoName .TfName}} = append(cItem.{{toGoName .TfName}}, ccItem)
+ return true
+ })
+ }
{{- end}}
{{- end}}
{{- end}}
@@ -335,10 +434,27 @@ func (data *{{camelCase .Name}}) hasChanges(ctx context.Context, state *{{camelC
} else {
for ii := range data.{{$name}}[i].{{toGoName .TfName}} {
{{- range .Attributes}}
+ {{- $ccname := toGoName .TfName}}
{{- if and (not .Value) (not .TfOnly)}}
+ {{- if ne .Type "List"}}
if !data.{{$name}}[i].{{$cname}}[ii].{{toGoName .TfName}}.Equal(state.{{$name}}[i].{{$cname}}[ii].{{toGoName .TfName}}) {
hasChanges = true
}
+ {{- else}}
+ if len(data.{{$name}}[i].{{$cname}}[ii].{{toGoName .TfName}}) != len(state.{{$name}}[i].{{$cname}}[ii].{{toGoName .TfName}}) {
+ hasChanges = true
+ } else {
+ for iii := range data.{{$name}}[i].{{$cname}}[ii].{{toGoName .TfName}} {
+ {{- range .Attributes}}
+ {{- if and (not .Value) (not .TfOnly)}}
+ if !data.{{$name}}[i].{{$cname}}[ii].{{$ccname}}[iii].{{toGoName .TfName}}.Equal(state.{{$name}}[i].{{$cname}}[ii].{{$ccname}}[iii].{{toGoName .TfName}}) {
+ hasChanges = true
+ }
+ {{- end}}
+ {{- end}}
+ }
+ }
+ {{- end}}
{{- end}}
{{- end}}
}
diff --git a/gen/templates/policy_definitions/resource.go b/gen/templates/policy_definitions/resource.go
index 60943a35..8b3a8403 100644
--- a/gen/templates/policy_definitions/resource.go
+++ b/gen/templates/policy_definitions/resource.go
@@ -188,7 +188,7 @@ func (r *{{camelCase .Name}}PolicyDefinitionResource) Schema(ctx context.Context
Attributes: map[string]schema.Attribute{
{{- range .Attributes}}
{{- if not .Value}}
- "{{.TfName}}": schema.{{if eq .Type "ListString"}}List{{else}}{{.Type}}{{end}}Attribute{
+ "{{.TfName}}": schema.{{if eq .Type "List"}}ListNested{{else if eq .Type "ListString"}}List{{else}}{{.Type}}{{end}}Attribute{
MarkdownDescription: helpers.NewAttributeDescription("{{.Description}}")
{{- if len .EnumValues -}}
.AddStringEnumDescription({{range .EnumValues}}"{{.}}", {{end}})
@@ -233,6 +233,72 @@ func (r *{{camelCase .Name}}PolicyDefinitionResource) Schema(ctx context.Context
float64validator.Between({{.MinFloat}}, {{.MaxFloat}}),
},
{{- end}}
+ {{- if eq .Type "List"}}
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ {{- range .Attributes}}
+ {{- if not .Value}}
+ "{{.TfName}}": schema.{{if eq .Type "ListString"}}List{{else}}{{.Type}}{{end}}Attribute{
+ MarkdownDescription: helpers.NewAttributeDescription("{{.Description}}")
+ {{- if len .EnumValues -}}
+ .AddStringEnumDescription({{range .EnumValues}}"{{.}}", {{end}})
+ {{- end -}}
+ {{- if or (ne .MinInt 0) (ne .MaxInt 0) -}}
+ .AddIntegerRangeDescription({{.MinInt}}, {{.MaxInt}})
+ {{- end -}}
+ {{- if or (ne .MinFloat 0.0) (ne .MaxFloat 0.0) -}}
+ .AddFloatRangeDescription({{.MinFloat}}, {{.MaxFloat}})
+ {{- end -}}
+ {{- if .DefaultValue -}}
+ .AddDefaultValueDescription("{{.DefaultValue}}")
+ {{- end -}}
+ .String,
+ {{- if eq .Type "ListString"}}
+ ElementType: types.StringType,
+ {{- end}}
+ {{- if .Mandatory}}
+ Required: true,
+ {{- else}}
+ Optional: true,
+ {{- end}}
+ {{- if len .EnumValues}}
+ Validators: []validator.String{
+ stringvalidator.OneOf({{range .EnumValues}}"{{.}}", {{end}}),
+ },
+ {{- else if or (len .StringPatterns) (ne .StringMinLength 0) (ne .StringMaxLength 0) }}
+ Validators: []validator.String{
+ {{- if or (ne .StringMinLength 0) (ne .StringMaxLength 0)}}
+ stringvalidator.LengthBetween({{.StringMinLength}}, {{.StringMaxLength}}),
+ {{- end}}
+ {{- range .StringPatterns}}
+ stringvalidator.RegexMatches(regexp.MustCompile(`{{.}}`), ""),
+ {{- end}}
+ },
+ {{- else if or (ne .MinInt 0) (ne .MaxInt 0)}}
+ Validators: []validator.Int64{
+ int64validator.Between({{.MinInt}}, {{.MaxInt}}),
+ },
+ {{- else if or (ne .MinFloat 0.0) (ne .MaxFloat 0.0)}}
+ Validators: []validator.Float64{
+ float64validator.Between({{.MinFloat}}, {{.MaxFloat}}),
+ },
+ {{- end}}
+ },
+ {{- end}}
+ {{- end}}
+ },
+ },
+ {{- if or (ne .MinList 0) (ne .MaxList 0)}}
+ Validators: []validator.List{
+ {{- if ne .MinList 0}}
+ listvalidator.SizeAtLeast({{.MinList}}),
+ {{- end}}
+ {{- if ne .MaxList 0}}
+ listvalidator.SizeAtMost({{.MaxList}}),
+ {{- end}}
+ },
+ {{- end}}
+ {{- end}}
},
{{- end}}
{{- end}}
diff --git a/gen/templates/policy_definitions/resource.tf b/gen/templates/policy_definitions/resource.tf
index 3343708c..cf198e45 100644
--- a/gen/templates/policy_definitions/resource.tf
+++ b/gen/templates/policy_definitions/resource.tf
@@ -11,11 +11,23 @@ resource "sdwan_{{snakeCase .Name}}_policy_definition" "example" {
{{- if eq .Type "List"}}
{{.TfName}} = [
{
- {{- range .Attributes}}
- {{- if and (not .ExcludeTest) (not .ExcludeExample) (not .TfOnly) (not .Value)}}
- {{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
- {{- end}}
- {{- end}}
+ {{- range .Attributes}}
+ {{- if and (not .ExcludeTest) (not .ExcludeExample) (not .TfOnly) (not .Value)}}
+ {{- if eq .Type "List"}}
+ {{.TfName}} = [
+ {
+ {{- range .Attributes}}
+ {{- if and (not .ExcludeTest) (not .ExcludeExample) (not .TfOnly) (not .Value)}}
+ {{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
+ {{- end}}
+ {{- end}}
+ }
+ ]
+ {{- else}}
+ {{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
+ {{- end}}
+ {{- end}}
+ {{- end}}
}
]
{{- else}}
diff --git a/gen/templates/policy_definitions/resource_test.go b/gen/templates/policy_definitions/resource_test.go
index ca056181..4d08042a 100644
--- a/gen/templates/policy_definitions/resource_test.go
+++ b/gen/templates/policy_definitions/resource_test.go
@@ -47,9 +47,18 @@ func TestAccSdwan{{camelCase .Name}}PolicyDefinition(t *testing.T) {
{{- $clist := .TfName }}
{{- range .Attributes}}
{{- if and (not .WriteOnly) (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ {{- if eq .Type "List"}}
+ {{- $cclist := .TfName }}
+ {{- range .Attributes}}
+ {{- if and (not .WriteOnly) (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ resource.TestCheckResourceAttr("sdwan_{{snakeCase $name}}_policy_definition.test", "{{$list}}.0.{{$clist}}.0.{{$cclist}}.0.{{.TfName}}", "{{.Example}}"),
+ {{- end}}
+ {{- end}}
+ {{- else}}
resource.TestCheckResourceAttr("sdwan_{{snakeCase $name}}_policy_definition.test", "{{$list}}.0.{{$clist}}.0.{{.TfName}}", "{{.Example}}"),
{{- end}}
{{- end}}
+ {{- end}}
{{- else}}
resource.TestCheckResourceAttr("sdwan_{{snakeCase $name}}_policy_definition.test", "{{$list}}.0.{{.TfName}}", "{{.Example}}"),
{{- end}}
@@ -81,9 +90,19 @@ func testAccSdwan{{camelCase .Name}}PolicyDefinitionConfig_all() string {
{{.TfName}} = [{
{{- range .Attributes}}
{{- if and (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ {{- if eq .Type "List"}}
+ {{.TfName}} = [{
+ {{- range .Attributes}}
+ {{- if and (not .ExcludeTest) (not .TfOnly) (not .Value)}}
+ {{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
+ {{- end}}
+ {{- end}}
+ }]
+ {{- else}}
{{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
{{- end}}
{{- end}}
+ {{- end}}
}]
{{- else}}
{{.TfName}} = {{if eq .Type "String"}}"{{end}}{{.Example}}{{if eq .Type "String"}}"{{end}}
diff --git a/internal/provider/data_source_sdwan_hub_and_spoke_topology_policy_definition.go b/internal/provider/data_source_sdwan_hub_and_spoke_topology_policy_definition.go
new file mode 100644
index 00000000..8f3e70b2
--- /dev/null
+++ b/internal/provider/data_source_sdwan_hub_and_spoke_topology_policy_definition.go
@@ -0,0 +1,164 @@
+// 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
+
+// Code generated by "gen/generator.go"; DO NOT EDIT.
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/netascode/go-sdwan"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &HubAndSpokeTopologyPolicyDefinitionDataSource{}
+ _ datasource.DataSourceWithConfigure = &HubAndSpokeTopologyPolicyDefinitionDataSource{}
+)
+
+func NewHubAndSpokeTopologyPolicyDefinitionDataSource() datasource.DataSource {
+ return &HubAndSpokeTopologyPolicyDefinitionDataSource{}
+}
+
+type HubAndSpokeTopologyPolicyDefinitionDataSource struct {
+ client *sdwan.Client
+}
+
+func (d *HubAndSpokeTopologyPolicyDefinitionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_hub_and_spoke_topology_policy_definition"
+}
+
+func (d *HubAndSpokeTopologyPolicyDefinitionDataSource) 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 Hub and Spoke Topology policy definition.",
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The id of the policy definition",
+ Required: true,
+ },
+ "version": schema.Int64Attribute{
+ MarkdownDescription: "The version of the policy definition",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The policy definition type",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the policy definition",
+ Computed: true,
+ },
+ "description": schema.StringAttribute{
+ MarkdownDescription: "The description of the policy definition",
+ Computed: true,
+ },
+ "vpn_list_id": schema.StringAttribute{
+ MarkdownDescription: "VPN list ID",
+ Computed: true,
+ },
+ "vpn_list_version": schema.Int64Attribute{
+ MarkdownDescription: "VPN list version",
+ Computed: true,
+ },
+ "topologies": schema.ListNestedAttribute{
+ MarkdownDescription: "List of topologies",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ MarkdownDescription: "Topology name",
+ Computed: true,
+ },
+ "spokes": schema.ListNestedAttribute{
+ MarkdownDescription: "List of spokes",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "site_list_id": schema.StringAttribute{
+ MarkdownDescription: "Site list ID",
+ Computed: true,
+ },
+ "site_list_version": schema.Int64Attribute{
+ MarkdownDescription: "Site list version",
+ Computed: true,
+ },
+ "hubs": schema.ListNestedAttribute{
+ MarkdownDescription: "List of hubs",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "site_list_id": schema.StringAttribute{
+ MarkdownDescription: "Site list ID",
+ Computed: true,
+ },
+ "site_list_version": schema.Int64Attribute{
+ MarkdownDescription: "Site list version",
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (d *HubAndSpokeTopologyPolicyDefinitionDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ d.client = req.ProviderData.(*SdwanProviderData).Client
+}
+
+func (d *HubAndSpokeTopologyPolicyDefinitionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var config HubAndSpokeTopology
+
+ // Read config
+ diags := req.Config.Get(ctx, &config)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", config.Id.String()))
+
+ res, err := d.client.Get("/template/policy/definition/hubandspoke/" + config.Id.ValueString())
+ 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...)
+}
diff --git a/internal/provider/data_source_sdwan_hub_and_spoke_topology_policy_definition_test.go b/internal/provider/data_source_sdwan_hub_and_spoke_topology_policy_definition_test.go
new file mode 100644
index 00000000..21c5b7c3
--- /dev/null
+++ b/internal/provider/data_source_sdwan_hub_and_spoke_topology_policy_definition_test.go
@@ -0,0 +1,66 @@
+// 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
+
+// Code generated by "gen/generator.go"; DO NOT EDIT.
+
+package provider
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccDataSourceSdwanHubAndSpokeTopologyPolicyDefinition(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccDataSourceSdwanHubAndSpokeTopologyPolicyDefinitionConfig,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("data.sdwan_hub_and_spoke_topology_policy_definition.test", "vpn_list_id", "04fcbb0b-efbf-43d2-a04b-847d3a7b104e"),
+ resource.TestCheckResourceAttr("data.sdwan_hub_and_spoke_topology_policy_definition.test", "topologies.0.name", "Topology1"),
+ resource.TestCheckResourceAttr("data.sdwan_hub_and_spoke_topology_policy_definition.test", "topologies.0.spokes.0.site_list_id", "e858e1c4-6aa8-4de7-99df-c3adbf80290d"),
+ resource.TestCheckResourceAttr("data.sdwan_hub_and_spoke_topology_policy_definition.test", "topologies.0.spokes.0.hubs.0.site_list_id", "e858e1c4-6aa8-4de7-99df-c3adbf80290d"),
+ ),
+ },
+ },
+ })
+}
+
+const testAccDataSourceSdwanHubAndSpokeTopologyPolicyDefinitionConfig = `
+
+resource "sdwan_hub_and_spoke_topology_policy_definition" "test" {
+ name = "TF_TEST_MIN"
+ description = "Terraform integration test"
+ vpn_list_id = "04fcbb0b-efbf-43d2-a04b-847d3a7b104e"
+ topologies = [{
+ name = "Topology1"
+ spokes = [{
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ hubs = [{
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ }]
+ }]
+ }]
+}
+
+data "sdwan_hub_and_spoke_topology_policy_definition" "test" {
+ id = sdwan_hub_and_spoke_topology_policy_definition.test.id
+}
+`
diff --git a/internal/provider/model_sdwan_hub_and_spoke_topology_policy_definition.go b/internal/provider/model_sdwan_hub_and_spoke_topology_policy_definition.go
new file mode 100644
index 00000000..25e9d052
--- /dev/null
+++ b/internal/provider/model_sdwan_hub_and_spoke_topology_policy_definition.go
@@ -0,0 +1,204 @@
+// 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
+
+// Code generated by "gen/generator.go"; DO NOT EDIT.
+
+package provider
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/tidwall/gjson"
+ "github.com/tidwall/sjson"
+)
+
+type HubAndSpokeTopology struct {
+ Id types.String `tfsdk:"id"`
+ Version types.Int64 `tfsdk:"version"`
+ Type types.String `tfsdk:"type"`
+ Name types.String `tfsdk:"name"`
+ Description types.String `tfsdk:"description"`
+ VpnListId types.String `tfsdk:"vpn_list_id"`
+ VpnListVersion types.Int64 `tfsdk:"vpn_list_version"`
+ Topologies []HubAndSpokeTopologyTopologies `tfsdk:"topologies"`
+}
+
+type HubAndSpokeTopologyTopologies struct {
+ Name types.String `tfsdk:"name"`
+ Spokes []HubAndSpokeTopologyTopologiesSpokes `tfsdk:"spokes"`
+}
+
+type HubAndSpokeTopologyTopologiesSpokes struct {
+ SiteListId types.String `tfsdk:"site_list_id"`
+ SiteListVersion types.Int64 `tfsdk:"site_list_version"`
+ Hubs []HubAndSpokeTopologyTopologiesSpokesHubs `tfsdk:"hubs"`
+}
+
+type HubAndSpokeTopologyTopologiesSpokesHubs struct {
+ SiteListId types.String `tfsdk:"site_list_id"`
+ SiteListVersion types.Int64 `tfsdk:"site_list_version"`
+}
+
+func (data HubAndSpokeTopology) getType() string {
+ return "hubAndSpoke"
+}
+
+func (data HubAndSpokeTopology) toBody(ctx context.Context) string {
+ body, _ := sjson.Set("", "name", data.Name.ValueString())
+ body, _ = sjson.Set(body, "description", data.Description.ValueString())
+ body, _ = sjson.Set(body, "type", "hubAndSpoke")
+ path := "definition."
+ if !data.VpnListId.IsNull() {
+ body, _ = sjson.Set(body, path+"vpnList", data.VpnListId.ValueString())
+ }
+ if len(data.Topologies) > 0 {
+ body, _ = sjson.Set(body, path+"subDefinitions", []interface{}{})
+ for _, item := range data.Topologies {
+ itemBody := ""
+ if !item.Name.IsNull() {
+ itemBody, _ = sjson.Set(itemBody, "name", item.Name.ValueString())
+ }
+ if len(item.Spokes) > 0 {
+ itemBody, _ = sjson.Set(itemBody, "spokes", []interface{}{})
+ for _, childItem := range item.Spokes {
+ itemChildBody := ""
+ if !childItem.SiteListId.IsNull() {
+ itemChildBody, _ = sjson.Set(itemChildBody, "siteList", childItem.SiteListId.ValueString())
+ }
+ if len(childItem.Hubs) > 0 {
+ itemChildBody, _ = sjson.Set(itemChildBody, "hubs", []interface{}{})
+ for _, childChildItem := range childItem.Hubs {
+ itemChildChildBody := ""
+ if !childChildItem.SiteListId.IsNull() {
+ itemChildChildBody, _ = sjson.Set(itemChildChildBody, "siteList", childChildItem.SiteListId.ValueString())
+ }
+ itemChildBody, _ = sjson.SetRaw(itemChildBody, "hubs.-1", itemChildChildBody)
+ }
+ }
+ itemBody, _ = sjson.SetRaw(itemBody, "spokes.-1", itemChildBody)
+ }
+ }
+ body, _ = sjson.SetRaw(body, path+"subDefinitions.-1", itemBody)
+ }
+ }
+ return body
+}
+
+func (data *HubAndSpokeTopology) fromBody(ctx context.Context, res gjson.Result) {
+ if value := res.Get("name"); value.Exists() {
+ data.Name = types.StringValue(value.String())
+ } else {
+ data.Name = types.StringNull()
+ }
+ if value := res.Get("description"); value.Exists() {
+ data.Description = types.StringValue(value.String())
+ } else {
+ data.Description = types.StringNull()
+ }
+ if value := res.Get("type"); value.Exists() {
+ data.Type = types.StringValue(value.String())
+ } else {
+ data.Type = types.StringNull()
+ }
+ path := "definition."
+ if value := res.Get(path + "vpnList"); value.Exists() {
+ data.VpnListId = types.StringValue(value.String())
+ } else {
+ data.VpnListId = types.StringNull()
+ }
+ if value := res.Get(path + "subDefinitions"); value.Exists() {
+ data.Topologies = make([]HubAndSpokeTopologyTopologies, 0)
+ value.ForEach(func(k, v gjson.Result) bool {
+ item := HubAndSpokeTopologyTopologies{}
+ if cValue := v.Get("name"); cValue.Exists() {
+ item.Name = types.StringValue(cValue.String())
+ } else {
+ item.Name = types.StringNull()
+ }
+ if cValue := v.Get("spokes"); cValue.Exists() {
+ item.Spokes = make([]HubAndSpokeTopologyTopologiesSpokes, 0)
+ cValue.ForEach(func(ck, cv gjson.Result) bool {
+ cItem := HubAndSpokeTopologyTopologiesSpokes{}
+ if ccValue := cv.Get("siteList"); ccValue.Exists() {
+ cItem.SiteListId = types.StringValue(ccValue.String())
+ } else {
+ cItem.SiteListId = types.StringNull()
+ }
+ if ccValue := cv.Get("hubs"); ccValue.Exists() {
+ cItem.Hubs = make([]HubAndSpokeTopologyTopologiesSpokesHubs, 0)
+ ccValue.ForEach(func(cck, ccv gjson.Result) bool {
+ ccItem := HubAndSpokeTopologyTopologiesSpokesHubs{}
+ if cccValue := ccv.Get("siteList"); cccValue.Exists() {
+ ccItem.SiteListId = types.StringValue(cccValue.String())
+ } else {
+ ccItem.SiteListId = types.StringNull()
+ }
+ cItem.Hubs = append(cItem.Hubs, ccItem)
+ return true
+ })
+ }
+ item.Spokes = append(item.Spokes, cItem)
+ return true
+ })
+ }
+ data.Topologies = append(data.Topologies, item)
+ return true
+ })
+ }
+}
+
+func (data *HubAndSpokeTopology) hasChanges(ctx context.Context, state *HubAndSpokeTopology) bool {
+ hasChanges := false
+ if !data.Name.Equal(state.Name) {
+ hasChanges = true
+ }
+ if !data.Description.Equal(state.Description) {
+ hasChanges = true
+ }
+ if !data.VpnListId.Equal(state.VpnListId) {
+ hasChanges = true
+ }
+ if len(data.Topologies) != len(state.Topologies) {
+ hasChanges = true
+ } else {
+ for i := range data.Topologies {
+ if !data.Topologies[i].Name.Equal(state.Topologies[i].Name) {
+ hasChanges = true
+ }
+ if len(data.Topologies[i].Spokes) != len(state.Topologies[i].Spokes) {
+ hasChanges = true
+ } else {
+ for ii := range data.Topologies[i].Spokes {
+ if !data.Topologies[i].Spokes[ii].SiteListId.Equal(state.Topologies[i].Spokes[ii].SiteListId) {
+ hasChanges = true
+ }
+ if len(data.Topologies[i].Spokes[ii].Hubs) != len(state.Topologies[i].Spokes[ii].Hubs) {
+ hasChanges = true
+ } else {
+ for iii := range data.Topologies[i].Spokes[ii].Hubs {
+ if !data.Topologies[i].Spokes[ii].Hubs[iii].SiteListId.Equal(state.Topologies[i].Spokes[ii].Hubs[iii].SiteListId) {
+ hasChanges = true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return hasChanges
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 0ef4d973..8ae3a78e 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -283,6 +283,7 @@ func (p *SdwanProvider) Resources(ctx context.Context) []func() resource.Resourc
NewVPNListPolicyObjectResource,
NewACLPolicyDefinitionResource,
NewDeviceACLPolicyDefinitionResource,
+ NewHubAndSpokeTopologyPolicyDefinitionResource,
NewQoSMapPolicyDefinitionResource,
NewRewriteRulePolicyDefinitionResource,
NewRoutePolicyDefinitionResource,
@@ -337,6 +338,7 @@ func (p *SdwanProvider) DataSources(ctx context.Context) []func() datasource.Dat
NewVPNListPolicyObjectDataSource,
NewACLPolicyDefinitionDataSource,
NewDeviceACLPolicyDefinitionDataSource,
+ NewHubAndSpokeTopologyPolicyDefinitionDataSource,
NewQoSMapPolicyDefinitionDataSource,
NewRewriteRulePolicyDefinitionDataSource,
NewRoutePolicyDefinitionDataSource,
diff --git a/internal/provider/resource_sdwan_hub_and_spoke_topology_policy_definition.go b/internal/provider/resource_sdwan_hub_and_spoke_topology_policy_definition.go
new file mode 100644
index 00000000..ec23118d
--- /dev/null
+++ b/internal/provider/resource_sdwan_hub_and_spoke_topology_policy_definition.go
@@ -0,0 +1,281 @@
+// 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
+
+// Code generated by "gen/generator.go"; DO NOT EDIT.
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/CiscoDevNet/terraform-provider-sdwan/internal/provider/helpers"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/netascode/go-sdwan"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces
+var _ resource.Resource = &HubAndSpokeTopologyPolicyDefinitionResource{}
+var _ resource.ResourceWithImportState = &HubAndSpokeTopologyPolicyDefinitionResource{}
+
+func NewHubAndSpokeTopologyPolicyDefinitionResource() resource.Resource {
+ return &HubAndSpokeTopologyPolicyDefinitionResource{}
+}
+
+type HubAndSpokeTopologyPolicyDefinitionResource struct {
+ client *sdwan.Client
+ updateMutex *sync.Mutex
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_hub_and_spoke_topology_policy_definition"
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) 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 Hub and Spoke Topology policy definition.").String,
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The id of the policy definition",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "version": schema.Int64Attribute{
+ MarkdownDescription: "The version of the policy definition",
+ Computed: true,
+ },
+ "type": schema.StringAttribute{
+ MarkdownDescription: "The policy defintion type",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the policy definition",
+ Required: true,
+ },
+ "description": schema.StringAttribute{
+ MarkdownDescription: "The description of the policy definition",
+ Required: true,
+ },
+ "vpn_list_id": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("VPN list ID").String,
+ Optional: true,
+ },
+ "vpn_list_version": schema.Int64Attribute{
+ MarkdownDescription: helpers.NewAttributeDescription("VPN list version").String,
+ Optional: true,
+ },
+ "topologies": schema.ListNestedAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("List of topologies").String,
+ Required: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Topology name").String,
+ Required: true,
+ },
+ "spokes": schema.ListNestedAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("List of spokes").String,
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "site_list_id": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Site list ID").String,
+ Optional: true,
+ },
+ "site_list_version": schema.Int64Attribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Site list version").String,
+ Optional: true,
+ },
+ "hubs": schema.ListNestedAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("List of hubs").String,
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "site_list_id": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Site list ID").String,
+ Optional: true,
+ },
+ "site_list_version": schema.Int64Attribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Site list version").String,
+ Optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ r.client = req.ProviderData.(*SdwanProviderData).Client
+ r.updateMutex = req.ProviderData.(*SdwanProviderData).UpdateMutex
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan HubAndSpokeTopology
+
+ // Read plan
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Create", plan.Name.ValueString()))
+
+ // Create object
+ body := plan.toBody(ctx)
+
+ res, err := r.client.Post("/template/policy/definition/hubandspoke", body)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (POST), got error: %s, %s", err, res.String()))
+ return
+ }
+
+ plan.Id = types.StringValue(res.Get("definitionId").String())
+ plan.Version = types.Int64Value(0)
+ plan.Type = types.StringValue(plan.getType())
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Create finished successfully", plan.Name.ValueString()))
+
+ diags = resp.State.Set(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state HubAndSpokeTopology
+
+ // Read state
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", state.Name.String()))
+
+ res, err := r.client.Get("/template/policy/definition/hubandspoke/" + state.Id.ValueString())
+ if res.Get("error.message").String() == "Failed to find specified resource" {
+ resp.State.RemoveResource(ctx)
+ return
+ } else if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve object (GET), got error: %s, %s", err, res.String()))
+ return
+ }
+
+ state.fromBody(ctx, res)
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Read finished successfully", state.Name.ValueString()))
+
+ diags = resp.State.Set(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan, state HubAndSpokeTopology
+
+ // Read plan
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Read state
+ diags = req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Update", plan.Name.ValueString()))
+
+ if plan.hasChanges(ctx, &state) {
+ body := plan.toBody(ctx)
+ r.updateMutex.Lock()
+ res, err := r.client.Put("/template/policy/definition/hubandspoke/"+plan.Id.ValueString(), body)
+ r.updateMutex.Unlock()
+ if err != nil {
+ if strings.Contains(res.Get("error.message").String(), "Failed to acquire lock") {
+ resp.Diagnostics.AddWarning("Client Warning", "Failed to modify policy due to policy being locked by another change. Policy changes will not be applied. Re-run 'terraform apply' to try again.")
+ } else {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (PUT), got error: %s, %s", err, res.String()))
+ return
+ }
+ }
+ } else {
+ tflog.Debug(ctx, fmt.Sprintf("%s: No changes detected", plan.Name.ValueString()))
+ }
+
+ plan.Version = types.Int64Value(state.Version.ValueInt64() + 1)
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Update finished successfully", plan.Name.ValueString()))
+
+ diags = resp.State.Set(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state HubAndSpokeTopology
+
+ // Read state
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Delete", state.Name.ValueString()))
+
+ res, err := r.client.Delete("/template/policy/definition/hubandspoke/" + state.Id.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to delete object (DELETE), got error: %s, %s", err, res.String()))
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Delete finished successfully", state.Name.ValueString()))
+
+ resp.State.RemoveResource(ctx)
+}
+
+func (r *HubAndSpokeTopologyPolicyDefinitionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
diff --git a/internal/provider/resource_sdwan_hub_and_spoke_topology_policy_definition_test.go b/internal/provider/resource_sdwan_hub_and_spoke_topology_policy_definition_test.go
new file mode 100644
index 00000000..a5af9f8b
--- /dev/null
+++ b/internal/provider/resource_sdwan_hub_and_spoke_topology_policy_definition_test.go
@@ -0,0 +1,63 @@
+// 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
+
+// Code generated by "gen/generator.go"; DO NOT EDIT.
+
+package provider
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAccSdwanHubAndSpokeTopologyPolicyDefinition(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccSdwanHubAndSpokeTopologyPolicyDefinitionConfig_all(),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("sdwan_hub_and_spoke_topology_policy_definition.test", "vpn_list_id", "04fcbb0b-efbf-43d2-a04b-847d3a7b104e"),
+ resource.TestCheckResourceAttr("sdwan_hub_and_spoke_topology_policy_definition.test", "topologies.0.name", "Topology1"),
+ resource.TestCheckResourceAttr("sdwan_hub_and_spoke_topology_policy_definition.test", "topologies.0.spokes.0.site_list_id", "e858e1c4-6aa8-4de7-99df-c3adbf80290d"),
+ resource.TestCheckResourceAttr("sdwan_hub_and_spoke_topology_policy_definition.test", "topologies.0.spokes.0.hubs.0.site_list_id", "e858e1c4-6aa8-4de7-99df-c3adbf80290d"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccSdwanHubAndSpokeTopologyPolicyDefinitionConfig_all() string {
+ return `
+ resource "sdwan_hub_and_spoke_topology_policy_definition" "test" {
+ name = "TF_TEST_ALL"
+ description = "Terraform integration test"
+ vpn_list_id = "04fcbb0b-efbf-43d2-a04b-847d3a7b104e"
+ topologies = [{
+ name = "Topology1"
+ spokes = [{
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ hubs = [{
+ site_list_id = "e858e1c4-6aa8-4de7-99df-c3adbf80290d"
+ }]
+ }]
+ }]
+ }
+ `
+}
diff --git a/templates/guides/changelog.md.tmpl b/templates/guides/changelog.md.tmpl
index d16a076b..b52aad27 100644
--- a/templates/guides/changelog.md.tmpl
+++ b/templates/guides/changelog.md.tmpl
@@ -12,6 +12,7 @@ description: |-
- Add `sdwan_cisco_ospf_feature_template` resource and data source
- Add `sdwan_cisco_vpn_interface_ipsec_feature_template` resource and data source
- Add `sdwan_cisco_secure_internet_gateway_feature_template` resource and data source
+- Add `sdwan_hub_and_spoke_topology_policy_definition` resource and data source
## 0.2.0