diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1456e9fd..abfdeba93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## 0.6.3 (unreleased)
- Add missing options under `unsupported_features` attribute of `sdwan_configuration_group`, [link](https://github.com/CiscoDevNet/terraform-provider-sdwan/issues/478)
+- Add `sdwan_policy_group` resource and data source
## 0.6.2
diff --git a/docs/data-sources/policy_group.md b/docs/data-sources/policy_group.md
new file mode 100644
index 000000000..28e366525
--- /dev/null
+++ b/docs/data-sources/policy_group.md
@@ -0,0 +1,53 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "sdwan_policy_group Data Source - terraform-provider-sdwan"
+subcategory: "Policy Groups"
+description: |-
+ This data source can read the Policy Group .
+---
+
+# sdwan_policy_group (Data Source)
+
+This data source can read the Policy Group .
+
+## Example Usage
+
+```terraform
+data "sdwan_policy_group" "example" {
+ id = "f6b2c44c-693c-4763-b010-895aa3d236bd"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `id` (String) The id of the object
+
+### Read-Only
+
+- `description` (String) Description
+- `devices` (Attributes List) List of devices (see [below for nested schema](#nestedatt--devices))
+- `feature_profile_ids` (Set of String) List of feature profile IDs
+- `name` (String) The name of the policy group
+- `policy_versions` (List of String) List of all associated policy versions
+- `solution` (String) Type of solution
+
+
+### Nested Schema for `devices`
+
+Read-Only:
+
+- `deploy` (Boolean) Deploy to device if enabled.
+- `id` (String) Device ID
+- `variables` (Attributes Set) List of variables (see [below for nested schema](#nestedatt--devices--variables))
+
+
+### Nested Schema for `devices.variables`
+
+Read-Only:
+
+- `list_value` (List of String) Use this instead of `value` in case value is of type `List`.
+- `name` (String) Variable name
+- `value` (String) Variable value
diff --git a/docs/guides/changelog.md b/docs/guides/changelog.md
index 814a41d9e..7b5ea5699 100644
--- a/docs/guides/changelog.md
+++ b/docs/guides/changelog.md
@@ -10,6 +10,7 @@ description: |-
## 0.6.3 (unreleased)
- Add missing options under `unsupported_features` attribute of `sdwan_configuration_group`, [link](https://github.com/CiscoDevNet/terraform-provider-sdwan/issues/478)
+- Add `sdwan_policy_group` resource and data source
## 0.6.2
diff --git a/docs/resources/policy_group.md b/docs/resources/policy_group.md
new file mode 100644
index 000000000..b6189f842
--- /dev/null
+++ b/docs/resources/policy_group.md
@@ -0,0 +1,81 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "sdwan_policy_group Resource - terraform-provider-sdwan"
+subcategory: "Policy Groups"
+description: |-
+ This resource can manage a Policy Group .
+ Minimum SD-WAN Manager version: 20.12.0
+---
+
+# sdwan_policy_group (Resource)
+
+This resource can manage a Policy Group .
+ - Minimum SD-WAN Manager version: `20.12.0`
+
+## Example Usage
+
+```terraform
+resource "sdwan_policy_group" "example" {
+ name = "PG_1"
+ description = "My policy group 1"
+ solution = "sdwan"
+ feature_profile_ids = ["f6dd22c8-0b4f-496c-9a0b-6813d1f8b8ac"]
+ devices = [
+ {
+ id = "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"
+ }
+ ]
+}
+```
+
+
+## Schema
+
+### Required
+
+- `description` (String) Description
+- `name` (String) The name of the policy group
+- `solution` (String) Type of solution
+ - Choices: `sdwan`
+
+### Optional
+
+- `devices` (Attributes List) List of devices (see [below for nested schema](#nestedatt--devices))
+- `feature_profile_ids` (Set of String) List of feature profile IDs
+- `policy_versions` (List of String) List of all associated policy versions
+
+### Read-Only
+
+- `id` (String) The id of the object
+
+
+### Nested Schema for `devices`
+
+Optional:
+
+- `deploy` (Boolean) Deploy to device if enabled.
+ - Default value: `false`
+- `id` (String) Device ID
+- `variables` (Attributes Set) List of variables (see [below for nested schema](#nestedatt--devices--variables))
+
+
+### Nested Schema for `devices.variables`
+
+Required:
+
+- `name` (String) Variable name
+
+Optional:
+
+- `list_value` (List of String) Use this instead of `value` in case value is of type `List`.
+- `value` (String) Variable value
+
+## Import
+
+Import is supported using the following syntax:
+
+The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:
+
+```shell
+terraform import sdwan_policy_group.example "f6b2c44c-693c-4763-b010-895aa3d236bd"
+```
diff --git a/examples/data-sources/sdwan_policy_group/data-source.tf b/examples/data-sources/sdwan_policy_group/data-source.tf
new file mode 100644
index 000000000..94112efee
--- /dev/null
+++ b/examples/data-sources/sdwan_policy_group/data-source.tf
@@ -0,0 +1,3 @@
+data "sdwan_policy_group" "example" {
+ id = "f6b2c44c-693c-4763-b010-895aa3d236bd"
+}
diff --git a/examples/resources/sdwan_policy_group/import.sh b/examples/resources/sdwan_policy_group/import.sh
new file mode 100644
index 000000000..3a4c727c0
--- /dev/null
+++ b/examples/resources/sdwan_policy_group/import.sh
@@ -0,0 +1 @@
+terraform import sdwan_policy_group.example "f6b2c44c-693c-4763-b010-895aa3d236bd"
diff --git a/examples/resources/sdwan_policy_group/resource.tf b/examples/resources/sdwan_policy_group/resource.tf
new file mode 100644
index 000000000..ec3660e33
--- /dev/null
+++ b/examples/resources/sdwan_policy_group/resource.tf
@@ -0,0 +1,11 @@
+resource "sdwan_policy_group" "example" {
+ name = "PG_1"
+ description = "My policy group 1"
+ solution = "sdwan"
+ feature_profile_ids = ["f6dd22c8-0b4f-496c-9a0b-6813d1f8b8ac"]
+ devices = [
+ {
+ id = "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"
+ }
+ ]
+}
diff --git a/gen/definitions/generic/policy_group.yaml b/gen/definitions/generic/policy_group.yaml
new file mode 100644
index 000000000..017ebe8d6
--- /dev/null
+++ b/gen/definitions/generic/policy_group.yaml
@@ -0,0 +1,197 @@
+---
+name: Policy Group
+rest_endpoint: /v1/policy-group/
+id_attribute: id
+minimum_version: 20.12.0
+test_tags: [SDWAN_2012]
+doc_category: Policy Groups
+attributes:
+ - model_name: name
+ type: String
+ mandatory: true
+ description: The name of the policy group
+ example: PG_1
+ - model_name: description
+ type: String
+ mandatory: true
+ description: Description
+ example: My policy group 1
+ - model_name: solution
+ type: String
+ mandatory: true
+ enum_values: [sdwan]
+ description: Type of solution
+ example: sdwan
+ - model_name: profiles
+ tf_name: feature_profile_ids
+ type: Set
+ element_type: String
+ description: List of feature profile IDs
+ example: f6dd22c8-0b4f-496c-9a0b-6813d1f8b8ac
+ test_value: "[sdwan_application_priority_feature_profile.test.id]"
+ - model_name: devices
+ type: List
+ description: List of devices
+ attributes:
+ - model_name: id
+ type: String
+ id: true
+ description: Device ID
+ example: C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B
+ - tf_name: deploy
+ tf_only: true
+ type: Bool
+ description: Deploy to device if enabled.
+ default_value: false
+ example: true
+ - model_name: variables
+ type: Set
+ description: List of variables
+ exclude_test: true
+ attributes:
+ - model_name: name
+ type: String
+ mandatory: true
+ id: true
+ description: Variable name
+ example: qos_interfaces
+ - model_name: value
+ type: String
+ description: Variable value
+ example: GigabitEthernet1
+ - model_name: list_value
+ type: List
+ element_type: String
+ tf_only: true
+ description: Use this instead of `value` in case value is of type `List`.
+ example: GigabitEthernet1
+ - tf_name: policy_versions
+ tf_only: true
+ type: Versions
+ description: List of all associated policy versions
+ exclude_test: true
+
+test_prerequisites: |
+ resource "sdwan_system_feature_profile" "test" {
+ name = "SYSTEM_TF"
+ description = "Terraform test"
+ }
+
+ resource "sdwan_system_basic_feature" "test" {
+ name = "BASIC_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ }
+
+ resource "sdwan_system_aaa_feature" "test" {
+ name = "AAA_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ server_auth_order = ["local"]
+ users = [{
+ name = "admin"
+ password = "admin"
+ }]
+ }
+
+ resource "sdwan_system_bfd_feature" "test" {
+ name = "BFD_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ }
+
+ resource "sdwan_system_global_feature" "test" {
+ name = "GLOBAL_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ }
+
+ resource "sdwan_system_logging_feature" "test" {
+ name = "LOGGING_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ }
+
+ resource "sdwan_system_omp_feature" "test" {
+ name = "OMP_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ }
+
+ resource "sdwan_transport_feature_profile" "test" {
+ name = "TRANSPORT_TF"
+ description = "My transport feature profile 1"
+ }
+
+ resource "sdwan_transport_wan_vpn_feature" "test" {
+ name = "WAN_VPN_TF"
+ feature_profile_id = sdwan_transport_feature_profile.test.id
+ vpn = 0
+ }
+
+ resource "sdwan_transport_wan_vpn_interface_ethernet_feature" "test" {
+ name = "WAN_VPN_INT_TF"
+ feature_profile_id = sdwan_transport_feature_profile.test.id
+ transport_wan_vpn_feature_id = sdwan_transport_wan_vpn_feature.test.id
+ interface_name = "GigabitEthernet1"
+ shutdown = false
+ ipv4_configuration_type = "dynamic"
+ ipv4_dhcp_distance = 1
+ tunnel_interface = true
+ tunnel_interface_encapsulations = [
+ {
+ encapsulation = "ipsec"
+ }
+ ]
+ }
+
+ resource "sdwan_configuration_group" "test" {
+ name = "CG_1"
+ description = "My config group 1"
+ solution = "sdwan"
+ feature_profile_ids = [
+ sdwan_system_feature_profile.test.id,
+ sdwan_transport_feature_profile.test.id,
+ ]
+ devices = [{
+ id = "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"
+ deploy = true
+ variables = [
+ {
+ name = "host_name"
+ value = "edge1"
+ },
+ {
+ name = "pseudo_commit_timer"
+ value = 0
+ },
+ {
+ name = "site_id"
+ value = 1
+ },
+ {
+ name = "system_ip"
+ value = "10.1.1.1"
+ },
+ {
+ name = "ipv6_strict_control"
+ value = "false"
+ }
+ ]
+ }]
+ feature_versions = [
+ sdwan_system_basic_feature.test.version,
+ sdwan_system_aaa_feature.test.version,
+ sdwan_system_bfd_feature.test.version,
+ sdwan_system_global_feature.test.version,
+ sdwan_system_logging_feature.test.version,
+ sdwan_system_omp_feature.test.version,
+ sdwan_transport_wan_vpn_interface_ethernet_feature.test.version,
+ ]
+ }
+
+ resource "sdwan_application_priority_feature_profile" "test" {
+ name = "APPLICATION_PRIORITY_TF"
+ description = "Terraform test"
+ }
+
+ resource "sdwan_application_priority_qos_policy" "test" {
+ name = "qos"
+ description = "QoS policy for application priority"
+ feature_profile_id = sdwan_application_priority_feature_profile.test.id
+ target_interface_variable = "{{qos_interfaces}}"
+ }
diff --git a/internal/provider/data_source_sdwan_policy_group.go b/internal/provider/data_source_sdwan_policy_group.go
new file mode 100644
index 000000000..c09516a5b
--- /dev/null
+++ b/internal/provider/data_source_sdwan_policy_group.go
@@ -0,0 +1,193 @@
+// 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"
+ "strings"
+
+ "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-sdwan"
+)
+
+// 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 = &PolicyGroupDataSource{}
+ _ datasource.DataSourceWithConfigure = &PolicyGroupDataSource{}
+)
+
+func NewPolicyGroupDataSource() datasource.DataSource {
+ return &PolicyGroupDataSource{}
+}
+
+type PolicyGroupDataSource struct {
+ client *sdwan.Client
+}
+
+func (d *PolicyGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_policy_group"
+}
+
+func (d *PolicyGroupDataSource) 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 Policy Group .",
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The id of the object",
+ Required: true,
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the policy group",
+ Computed: true,
+ },
+ "description": schema.StringAttribute{
+ MarkdownDescription: "Description",
+ Computed: true,
+ },
+ "solution": schema.StringAttribute{
+ MarkdownDescription: "Type of solution",
+ Computed: true,
+ },
+ "feature_profile_ids": schema.SetAttribute{
+ MarkdownDescription: "List of feature profile IDs",
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ "devices": schema.ListNestedAttribute{
+ MarkdownDescription: "List of devices",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "Device ID",
+ Computed: true,
+ },
+ "deploy": schema.BoolAttribute{
+ MarkdownDescription: "Deploy to device if enabled.",
+ Computed: true,
+ },
+ "variables": schema.SetNestedAttribute{
+ MarkdownDescription: "List of variables",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ MarkdownDescription: "Variable name",
+ Computed: true,
+ },
+ "value": schema.StringAttribute{
+ MarkdownDescription: "Variable value",
+ Computed: true,
+ },
+ "list_value": schema.ListAttribute{
+ MarkdownDescription: "Use this instead of `value` in case value is of type `List`.",
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "policy_versions": schema.ListAttribute{
+ MarkdownDescription: "List of all associated policy versions",
+ ElementType: types.StringType,
+ Computed: true,
+ },
+ },
+ }
+}
+
+func (d *PolicyGroupDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ d.client = req.ProviderData.(*SdwanProviderData).Client
+}
+
+// End of section. //template:end model
+
+func (d *PolicyGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var config PolicyGroup
+
+ // 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()))
+
+ // Read policy group
+ res, err := d.client.Get(config.getPath() + url.QueryEscape(config.Id.ValueString()))
+ if res.Raw == "" && err != nil {
+ resp.State.RemoveResource(ctx)
+ return
+ } else if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve object, got error: %s", err))
+ return
+ }
+
+ config.fromBodyPolicyGroup(ctx, res)
+
+ // Read policy group devices
+ path := fmt.Sprintf("/v1/policy-group/%v/device/associate/", config.Id.ValueString())
+ res, err = d.client.Get(path)
+ if strings.Contains(res.Get("error.message").String(), "Invalid policy group passed") {
+ 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
+ }
+
+ config.fromBodyPolicyGroupDevices(ctx, res)
+
+ // Read policy group devices variables
+ path = fmt.Sprintf("/v1/policy-group/%v/device/variables/", config.Id.ValueString())
+ res, err = d.client.Get(path)
+ if strings.Contains(res.Get("error.message").String(), "Invalid policy group passed") {
+ 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
+ }
+
+ config.fromBodyPolicyGroupDeviceVariables(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_policy_group_test.go b/internal/provider/data_source_sdwan_policy_group_test.go
new file mode 100644
index 000000000..649902cb1
--- /dev/null
+++ b/internal/provider/data_source_sdwan_policy_group_test.go
@@ -0,0 +1,215 @@
+// 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 TestAccDataSourceSdwanPolicyGroup(t *testing.T) {
+ if os.Getenv("SDWAN_2012") == "" {
+ t.Skip("skipping test, set environment variable SDWAN_2012")
+ }
+ var checks []resource.TestCheckFunc
+ checks = append(checks, resource.TestCheckResourceAttr("data.sdwan_policy_group.test", "name", "PG_1"))
+ checks = append(checks, resource.TestCheckResourceAttr("data.sdwan_policy_group.test", "description", "My policy group 1"))
+ checks = append(checks, resource.TestCheckResourceAttr("data.sdwan_policy_group.test", "solution", "sdwan"))
+ checks = append(checks, resource.TestCheckResourceAttr("data.sdwan_policy_group.test", "devices.0.id", "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"))
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccDataSourceSdwanPolicyGroupPrerequisitesConfig + testAccDataSourceSdwanPolicyGroupConfig(),
+ Check: resource.ComposeTestCheckFunc(checks...),
+ },
+ },
+ })
+}
+
+// End of section. //template:end testAccDataSource
+
+// Section below is generated&owned by "gen/generator.go". //template:begin testPrerequisites
+const testAccDataSourceSdwanPolicyGroupPrerequisitesConfig = `
+resource "sdwan_system_feature_profile" "test" {
+ name = "SYSTEM_TF"
+ description = "Terraform test"
+}
+
+resource "sdwan_system_basic_feature" "test" {
+ name = "BASIC_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_aaa_feature" "test" {
+ name = "AAA_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ server_auth_order = ["local"]
+ users = [{
+ name = "admin"
+ password = "admin"
+ }]
+}
+
+resource "sdwan_system_bfd_feature" "test" {
+ name = "BFD_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_global_feature" "test" {
+ name = "GLOBAL_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_logging_feature" "test" {
+ name = "LOGGING_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_omp_feature" "test" {
+ name = "OMP_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_transport_feature_profile" "test" {
+ name = "TRANSPORT_TF"
+ description = "My transport feature profile 1"
+}
+
+resource "sdwan_transport_wan_vpn_feature" "test" {
+ name = "WAN_VPN_TF"
+ feature_profile_id = sdwan_transport_feature_profile.test.id
+ vpn = 0
+}
+
+resource "sdwan_transport_wan_vpn_interface_ethernet_feature" "test" {
+ name = "WAN_VPN_INT_TF"
+ feature_profile_id = sdwan_transport_feature_profile.test.id
+ transport_wan_vpn_feature_id = sdwan_transport_wan_vpn_feature.test.id
+ interface_name = "GigabitEthernet1"
+ shutdown = false
+ ipv4_configuration_type = "dynamic"
+ ipv4_dhcp_distance = 1
+ tunnel_interface = true
+ tunnel_interface_encapsulations = [
+ {
+ encapsulation = "ipsec"
+ }
+ ]
+}
+
+resource "sdwan_configuration_group" "test" {
+ name = "CG_1"
+ description = "My config group 1"
+ solution = "sdwan"
+ feature_profile_ids = [
+ sdwan_system_feature_profile.test.id,
+ sdwan_transport_feature_profile.test.id,
+ ]
+ devices = [{
+ id = "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"
+ deploy = true
+ variables = [
+ {
+ name = "host_name"
+ value = "edge1"
+ },
+ {
+ name = "pseudo_commit_timer"
+ value = 0
+ },
+ {
+ name = "site_id"
+ value = 1
+ },
+ {
+ name = "system_ip"
+ value = "10.1.1.1"
+ },
+ {
+ name = "ipv6_strict_control"
+ value = "false"
+ }
+ ]
+ }]
+ feature_versions = [
+ sdwan_system_basic_feature.test.version,
+ sdwan_system_aaa_feature.test.version,
+ sdwan_system_bfd_feature.test.version,
+ sdwan_system_global_feature.test.version,
+ sdwan_system_logging_feature.test.version,
+ sdwan_system_omp_feature.test.version,
+ sdwan_transport_wan_vpn_interface_ethernet_feature.test.version,
+ ]
+}
+
+resource "sdwan_application_priority_feature_profile" "test" {
+ name = "APPLICATION_PRIORITY_TF"
+ description = "Terraform test"
+}
+
+resource "sdwan_application_priority_qos_policy" "test" {
+ name = "qos"
+ description = "QoS policy for application priority"
+ feature_profile_id = sdwan_application_priority_feature_profile.test.id
+ target_interface_variable = "{{qos_interfaces}}"
+}
+
+`
+
+// End of section. //template:end testPrerequisites
+
+func testAccDataSourceSdwanPolicyGroupConfig() string {
+ config := `resource "sdwan_policy_group" "test" {` + "\n"
+ config += ` name = "PG_1"` + "\n"
+ config += ` description = "My policy group 1"` + "\n"
+ config += ` solution = "sdwan"` + "\n"
+ config += ` feature_profile_ids = [sdwan_application_priority_feature_profile.test.id]` + "\n"
+ config += ` devices = [{` + "\n"
+ config += ` id = "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"` + "\n"
+ config += ` deploy = true` + "\n"
+ config += ` variables = [` + "\n"
+ config += ` {` + "\n"
+ config += ` name = "qos_interfaces"` + "\n"
+ config += ` list_value = [` + "\n"
+ config += ` "GigabitEthernet1",` + "\n"
+ config += ` "GigabitEthernet2"` + "\n"
+ config += ` ]` + "\n"
+ config += ` },` + "\n"
+ config += ` ]` + "\n"
+ config += ` }]` + "\n"
+ config += ` policy_versions = [` + "\n"
+ config += ` sdwan_application_priority_qos_policy.test.version,` + "\n"
+ config += ` ]` + "\n"
+ config += ` depends_on = [ sdwan_configuration_group.test ]` + "\n"
+ config += `}` + "\n"
+
+ config += `
+ data "sdwan_policy_group" "test" {
+ id = sdwan_policy_group.test.id
+ }
+ `
+ return config
+}
diff --git a/internal/provider/model_sdwan_policy_group.go b/internal/provider/model_sdwan_policy_group.go
new file mode 100644
index 000000000..ca2725352
--- /dev/null
+++ b/internal/provider/model_sdwan_policy_group.go
@@ -0,0 +1,412 @@
+// 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"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/CiscoDevNet/terraform-provider-sdwan/internal/provider/helpers"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "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 PolicyGroup struct {
+ Id types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Description types.String `tfsdk:"description"`
+ Solution types.String `tfsdk:"solution"`
+ FeatureProfileIds types.Set `tfsdk:"feature_profile_ids"`
+ Devices []PolicyGroupDevices `tfsdk:"devices"`
+ PolicyVersions types.List `tfsdk:"policy_versions"`
+}
+
+type PolicyGroupDevices struct {
+ Id types.String `tfsdk:"id"`
+ Deploy types.Bool `tfsdk:"deploy"`
+ Variables []PolicyGroupDevicesVariables `tfsdk:"variables"`
+}
+
+type PolicyGroupDevicesVariables struct {
+ Name types.String `tfsdk:"name"`
+ Value types.String `tfsdk:"value"`
+ ListValue types.List `tfsdk:"list_value"`
+}
+
+// End of section. //template:end types
+
+// Section below is generated&owned by "gen/generator.go". //template:begin getPath
+func (data PolicyGroup) getPath() string {
+ return "/v1/policy-group/"
+}
+
+// End of section. //template:end getPath
+
+func (data PolicyGroup) toBodyPolicyGroup(ctx context.Context) string {
+ body := ""
+ if !data.Name.IsNull() {
+ body, _ = sjson.Set(body, "name", data.Name.ValueString())
+ }
+ if !data.Description.IsNull() {
+ body, _ = sjson.Set(body, "description", data.Description.ValueString())
+ }
+ if !data.Solution.IsNull() {
+ body, _ = sjson.Set(body, "solution", data.Solution.ValueString())
+ }
+ if true {
+ body, _ = sjson.Set(body, "profiles", []interface{}{})
+ for _, item := range data.FeatureProfileIds.Elements() {
+ itemBody := ""
+ if !item.IsNull() {
+ itemBody, _ = sjson.Set(itemBody, "id", strings.Trim(item.String(), "\""))
+ }
+ body, _ = sjson.SetRaw(body, "profiles.-1", itemBody)
+ }
+ }
+ return body
+}
+
+func (data PolicyGroup) toBodyPolicyGroupDevices(ctx context.Context) string {
+ body := ""
+ if true {
+ body, _ = sjson.Set(body, "devices", []interface{}{})
+ for _, item := range data.Devices {
+ itemBody := ""
+ if !item.Id.IsNull() {
+ itemBody, _ = sjson.Set(itemBody, "id", item.Id.ValueString())
+ }
+ body, _ = sjson.SetRaw(body, "devices.-1", itemBody)
+ }
+ }
+ return body
+}
+
+func (data PolicyGroup) toBodyPolicyGroupDeviceVariables(ctx context.Context) string {
+ body := ""
+ if !data.Solution.IsNull() {
+ body, _ = sjson.Set(body, "solution", data.Solution.ValueString())
+ }
+ if true {
+ body, _ = sjson.Set(body, "devices", []interface{}{})
+ for _, item := range data.Devices {
+ itemBody := ""
+ if !item.Id.IsNull() {
+ itemBody, _ = sjson.Set(itemBody, "device-id", item.Id.ValueString())
+ }
+ if true {
+ itemBody, _ = sjson.Set(itemBody, "variables", []interface{}{})
+ for _, childItem := range item.Variables {
+ itemChildBody := ""
+ if !childItem.Name.IsNull() {
+ itemChildBody, _ = sjson.Set(itemChildBody, "name", childItem.Name.ValueString())
+ }
+ if !childItem.ListValue.IsNull() {
+ var values []string
+ childItem.ListValue.ElementsAs(ctx, &values, false)
+ itemChildBody, _ = sjson.Set(itemChildBody, "value", values)
+ } else if !childItem.Value.IsNull() {
+ if val, err := strconv.Atoi(childItem.Value.ValueString()); err == nil {
+ itemChildBody, _ = sjson.Set(itemChildBody, "value", val)
+ } else if val, err := strconv.ParseFloat(childItem.Value.ValueString(), 64); err == nil {
+ itemChildBody, _ = sjson.Set(itemChildBody, "value", val)
+ } else if val, err := strconv.ParseBool(childItem.Value.ValueString()); err == nil {
+ itemChildBody, _ = sjson.Set(itemChildBody, "value", val)
+ } else {
+ itemChildBody, _ = sjson.Set(itemChildBody, "value", childItem.Value.ValueString())
+ }
+ }
+ itemBody, _ = sjson.SetRaw(itemBody, "variables.-1", itemChildBody)
+ }
+ }
+ body, _ = sjson.SetRaw(body, "devices.-1", itemBody)
+ }
+ }
+ return body
+}
+
+func (data *PolicyGroup) fromBodyPolicyGroup(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("solution"); value.Exists() {
+ data.Solution = types.StringValue(value.String())
+ } else {
+ data.Solution = types.StringNull()
+ }
+ if value := res.Get("profiles"); value.Exists() && len(value.Array()) > 0 {
+ a := make([]attr.Value, len(value.Array()))
+ c := 0
+ value.ForEach(func(k, v gjson.Result) bool {
+ if cValue := v.Get("id"); cValue.Exists() {
+ a[c] = types.StringValue(cValue.String())
+ } else {
+ a[c] = types.StringNull()
+ }
+ c += 1
+ return true
+ })
+ data.FeatureProfileIds = types.SetValueMust(types.StringType, a)
+ } else {
+ data.FeatureProfileIds = types.SetNull(types.StringType)
+ }
+}
+
+func (data *PolicyGroup) fromBodyPolicyGroupDevices(ctx context.Context, res gjson.Result) {
+ original := *data
+ if value := res.Get("devices"); value.Exists() && len(value.Array()) > 0 {
+ data.Devices = make([]PolicyGroupDevices, 0)
+ value.ForEach(func(k, v gjson.Result) bool {
+ item := PolicyGroupDevices{}
+ if cValue := v.Get("id"); cValue.Exists() {
+ item.Id = types.StringValue(cValue.String())
+ } else {
+ item.Id = types.StringNull()
+ }
+ data.Devices = append(data.Devices, item)
+ return true
+ })
+ } else {
+ if len(data.Devices) > 0 {
+ data.Devices = []PolicyGroupDevices{}
+ }
+ }
+ // reorder
+ slices.Reverse(original.Devices)
+ for i := range original.Devices {
+ keyValues := [...]string{original.Devices[i].Id.ValueString()}
+
+ for y := range data.Devices {
+ found := false
+ for _, keyValue := range keyValues {
+ if !data.Devices[y].Id.IsNull() {
+ if data.Devices[y].Id.ValueString() == keyValue {
+ found = true
+ continue
+ }
+ found = false
+ break
+ }
+ continue
+ }
+ if found {
+ //insert at the beginning
+ device := data.Devices[y]
+ data.Devices = append(data.Devices[:y], data.Devices[y+1:]...)
+ data.Devices = append([]PolicyGroupDevices{device}, data.Devices...)
+ }
+ }
+ }
+}
+
+func (data *PolicyGroup) fromBodyPolicyGroupDeviceVariables(ctx context.Context, res gjson.Result) {
+ original := *data
+ if value := res.Get("family"); value.Exists() {
+ data.Solution = types.StringValue(value.String())
+ } else {
+ data.Solution = types.StringNull()
+ }
+ if value := res.Get("devices"); value.Exists() && len(value.Array()) > 0 {
+ data.Devices = make([]PolicyGroupDevices, 0)
+ value.ForEach(func(k, v gjson.Result) bool {
+ item := PolicyGroupDevices{}
+ if cValue := v.Get("device-id"); cValue.Exists() {
+ item.Id = types.StringValue(cValue.String())
+ } else {
+ item.Id = types.StringNull()
+ }
+ if cValue := v.Get("variables"); cValue.Exists() && len(cValue.Array()) > 0 {
+ item.Variables = make([]PolicyGroupDevicesVariables, 0)
+ cValue.ForEach(func(ck, cv gjson.Result) bool {
+ // skip optional variables
+ if !cv.Get("value").Exists() {
+ return true
+ }
+ cItem := PolicyGroupDevicesVariables{}
+ if ccValue := cv.Get("name"); ccValue.Exists() {
+ cItem.Name = types.StringValue(ccValue.String())
+ } else {
+ cItem.Name = types.StringNull()
+ }
+ if ccValue := cv.Get("value"); ccValue.Exists() {
+ if ccValue.IsArray() {
+ cItem.ListValue = helpers.GetStringList(ccValue.Array())
+ cItem.Value = types.StringNull()
+ } else {
+ cItem.ListValue = types.ListNull(types.StringType)
+ if !strings.Contains(strings.ToLower(ccValue.String()), "$crypt_cluster") {
+ cItem.Value = types.StringValue(ccValue.String())
+ }
+ }
+ } else {
+ cItem.ListValue = types.ListNull(types.StringType)
+ cItem.Value = types.StringNull()
+ }
+ item.Variables = append(item.Variables, cItem)
+ return true
+ })
+ if len(item.Variables) == 0 {
+ item.Variables = nil
+ }
+ } else {
+ if len(item.Variables) > 0 {
+ item.Variables = []PolicyGroupDevicesVariables{}
+ }
+ }
+ data.Devices = append(data.Devices, item)
+ return true
+ })
+ } else {
+ if len(data.Devices) > 0 {
+ data.Devices = []PolicyGroupDevices{}
+ }
+ }
+
+ // reorder
+ slices.Reverse(original.Devices)
+ for i := range original.Devices {
+ keyValues := [...]string{original.Devices[i].Id.ValueString()}
+
+ for y := range data.Devices {
+ found := false
+ for _, keyValue := range keyValues {
+ if !data.Devices[y].Id.IsNull() {
+ if data.Devices[y].Id.ValueString() == keyValue {
+ found = true
+ continue
+ }
+ found = false
+ break
+ }
+ continue
+ }
+ if found {
+ //insert at the beginning
+ device := data.Devices[y]
+ data.Devices = append(data.Devices[:y], data.Devices[y+1:]...)
+ data.Devices = append([]PolicyGroupDevices{device}, data.Devices...)
+ }
+ }
+ }
+}
+
+func (data *PolicyGroup) updateTfAttributes(ctx context.Context, state *PolicyGroup) {
+ //data.FeatureVersions = state.FeatureVersions
+ for i := range data.Devices {
+ dataKeys := [...]string{fmt.Sprintf("%v", data.Devices[i].Id.ValueString())}
+ stateIndex := -1
+ for j := range state.Devices {
+ stateKeys := [...]string{fmt.Sprintf("%v", state.Devices[j].Id.ValueString())}
+ if dataKeys == stateKeys {
+ stateIndex = j
+ break
+ }
+ }
+ if stateIndex > -1 {
+ data.Devices[i].Deploy = state.Devices[stateIndex].Deploy
+ } else {
+ data.Devices[i].Deploy = types.BoolNull()
+ }
+ }
+}
+
+func (data PolicyGroup) hasPolicyGroupDeviceVariables(ctx context.Context) bool {
+ for _, device := range data.Devices {
+ if len(device.Variables) > 0 {
+ return true
+ }
+ }
+ return false
+}
+
+func (data PolicyGroup) getUpdatedDevices(ctx context.Context, state *PolicyGroup) []string {
+ updatedDevices := make([]string, 0)
+ for _, device := range data.Devices {
+ for _, stateDevice := range state.Devices {
+ if device.Id.ValueString() == stateDevice.Id.ValueString() {
+ for _, variable := range device.Variables {
+ found := false
+ for _, stateVariable := range stateDevice.Variables {
+ if variable.Name.ValueString() == stateVariable.Name.ValueString() {
+ found = true
+ if variable.Value.ValueString() != stateVariable.Value.ValueString() {
+ if !slices.Contains(updatedDevices, device.Id.ValueString()) {
+ updatedDevices = append(updatedDevices, device.Id.ValueString())
+ }
+ }
+ if variable.ListValue.String() != stateVariable.ListValue.String() {
+ if !slices.Contains(updatedDevices, device.Id.ValueString()) {
+ updatedDevices = append(updatedDevices, device.Id.ValueString())
+ }
+ }
+ }
+ }
+ if !found {
+ if !slices.Contains(updatedDevices, device.Id.ValueString()) {
+ updatedDevices = append(updatedDevices, device.Id.ValueString())
+ }
+ }
+ }
+ for _, stateVariable := range stateDevice.Variables {
+ found := false
+ for _, variable := range device.Variables {
+ if variable.Name.ValueString() == stateVariable.Name.ValueString() {
+ found = true
+ }
+ }
+ if !found {
+ if !slices.Contains(updatedDevices, device.Id.ValueString()) {
+ updatedDevices = append(updatedDevices, device.Id.ValueString())
+ }
+ }
+ }
+ }
+ }
+ }
+ return updatedDevices
+}
+
+func (data PolicyGroup) hasPolicyVersionChanges(ctx context.Context, state *PolicyGroup) bool {
+ var planValues, stateValues []string
+ data.PolicyVersions.ElementsAs(ctx, &planValues, false)
+ state.PolicyVersions.ElementsAs(ctx, &stateValues, false)
+ if len(planValues) != len(stateValues) {
+ return true
+ }
+ for i := range planValues {
+ if i >= len(stateValues) || planValues[i] != stateValues[i] {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 0af5b9b1f..a9afc1b04 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -434,6 +434,7 @@ func (p *SdwanProvider) Resources(ctx context.Context) []func() resource.Resourc
NewObjectGroupPolicyDefinitionResource,
NewOtherFeatureProfileResource,
NewPolicerPolicyObjectResource,
+ NewPolicyGroupResource,
NewPolicyObjectFeatureProfileResource,
NewPortListPolicyObjectResource,
NewPreferredColorGroupPolicyObjectResource,
@@ -673,6 +674,7 @@ func (p *SdwanProvider) DataSources(ctx context.Context) []func() datasource.Dat
NewObjectGroupPolicyDefinitionDataSource,
NewOtherFeatureProfileDataSource,
NewPolicerPolicyObjectDataSource,
+ NewPolicyGroupDataSource,
NewPolicyObjectFeatureProfileDataSource,
NewPortListPolicyObjectDataSource,
NewPreferredColorGroupPolicyObjectDataSource,
diff --git a/internal/provider/resource_sdwan_policy_group.go b/internal/provider/resource_sdwan_policy_group.go
new file mode 100644
index 000000000..d02696dd4
--- /dev/null
+++ b/internal/provider/resource_sdwan_policy_group.go
@@ -0,0 +1,506 @@
+// 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"
+ "strings"
+ "sync"
+
+ "github.com/CiscoDevNet/terraform-provider-sdwan/internal/provider/helpers"
+ "github.com/hashicorp/go-version"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "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/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/netascode/go-sdwan"
+ "github.com/tidwall/gjson"
+ "github.com/tidwall/sjson"
+)
+
+// End of section. //template:end imports
+
+var MinPolicyGroupUpdateVersion = version.Must(version.NewVersion("20.15.0"))
+
+// Section below is generated&owned by "gen/generator.go". //template:begin model
+
+// Ensure provider defined types fully satisfy framework interfaces
+var _ resource.Resource = &PolicyGroupResource{}
+var _ resource.ResourceWithImportState = &PolicyGroupResource{}
+
+func NewPolicyGroupResource() resource.Resource {
+ return &PolicyGroupResource{}
+}
+
+type PolicyGroupResource struct {
+ client *sdwan.Client
+ updateMutex *sync.Mutex
+}
+
+func (r *PolicyGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_policy_group"
+}
+
+func (r *PolicyGroupResource) 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 Policy Group .").AddMinimumVersionDescription("20.12.0").String,
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: "The id of the object",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("The name of the policy group").String,
+ Required: true,
+ },
+ "description": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Description").String,
+ Required: true,
+ },
+ "solution": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Type of solution").AddStringEnumDescription("sdwan").String,
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("sdwan"),
+ },
+ },
+ "feature_profile_ids": schema.SetAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("List of feature profile IDs").String,
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ "devices": schema.ListNestedAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("List of devices").String,
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Device ID").String,
+ Optional: true,
+ },
+ "deploy": schema.BoolAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Deploy to device if enabled.").AddDefaultValueDescription("false").String,
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ "variables": schema.SetNestedAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("List of variables").String,
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "name": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Variable name").String,
+ Required: true,
+ },
+ "value": schema.StringAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Variable value").String,
+ Optional: true,
+ },
+ "list_value": schema.ListAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("Use this instead of `value` in case value is of type `List`.").String,
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "policy_versions": schema.ListAttribute{
+ MarkdownDescription: helpers.NewAttributeDescription("List of all associated policy versions").String,
+ ElementType: types.StringType,
+ Optional: true,
+ },
+ },
+ }
+}
+
+func (r *PolicyGroupResource) 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
+}
+
+// End of section. //template:end model
+
+func (r *PolicyGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan PolicyGroup
+
+ // 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 policy group
+ body := plan.toBodyPolicyGroup(ctx)
+ res, err := r.client.Post(plan.getPath(), 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("id").String())
+
+ // Create policy group devices
+ if len(plan.Devices) > 0 {
+ body = plan.toBodyPolicyGroupDevices(ctx)
+
+ path := fmt.Sprintf("/v1/policy-group/%v/device/associate/", plan.Id.ValueString())
+ res, err = r.client.Post(path, body)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure policy group devices (POST), got error: %s, %s", err, res.String()))
+ r.DeletePolicyGroup(ctx, plan, &resp.Diagnostics)
+ return
+ }
+ }
+
+ // Create policy group device variables
+ if len(plan.Devices) > 0 && plan.hasPolicyGroupDeviceVariables(ctx) {
+ body = plan.toBodyPolicyGroupDeviceVariables(ctx)
+
+ path := fmt.Sprintf("/v1/policy-group/%v/device/variables/", plan.Id.ValueString())
+ res, err = r.client.Put(path, body)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure policy group device variables (PUT), got error: %s, %s", err, res.String()))
+ r.DeletePolicyGroup(ctx, plan, &resp.Diagnostics)
+ return
+ }
+ }
+
+ // Deploy policy group to devices
+ if len(plan.Devices) > 0 {
+ r.Deploy(ctx, plan, nil, &resp.Diagnostics, true)
+ }
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Create finished successfully", plan.Name.ValueString()))
+
+ diags = resp.State.Set(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *PolicyGroupResource) Deploy(ctx context.Context, plan PolicyGroup, state *PolicyGroup, diag *diag.Diagnostics, deleteOnError bool) {
+ var updatedDevices []string
+ if state != nil {
+ updatedDevices = plan.getUpdatedDevices(ctx, state)
+ }
+
+ hasPolicyVersionChanges := false
+ currentVersion := version.Must(version.NewVersion(r.client.ManagerVersion))
+ if state != nil && currentVersion.LessThan(MinPolicyGroupUpdateVersion) {
+ hasPolicyVersionChanges = plan.hasPolicyVersionChanges(ctx, state)
+ }
+
+ path := fmt.Sprintf("/v1/policy-group/%v/device/associate/", plan.Id.ValueString())
+ res, err := r.client.Get(path)
+ if err != nil {
+ diag.AddError("Client Error", fmt.Sprintf("Failed to retrieve object (GET), got error: %s, %s", err, res.String()))
+ return
+ }
+
+ // Build deploy body
+ body, _ := sjson.Set("", "devices", []interface{}{})
+ if value := res.Get("devices"); value.Exists() && len(value.Array()) > 0 {
+ value.ForEach(func(k, v gjson.Result) bool {
+ id := v.Get("id").String()
+ for _, item := range plan.Devices {
+ if item.Id.ValueString() == id && item.Deploy.ValueBool() && (!v.Get("policyGroupUpToDate").Bool() || updatedDevices == nil || helpers.Contains(updatedDevices, id) || hasPolicyVersionChanges) {
+ itemBody, _ := sjson.Set("", "id", id)
+ body, _ = sjson.SetRaw(body, "devices.-1", itemBody)
+ tflog.Debug(ctx, fmt.Sprintf("%s: Deploying to device %s", plan.Name.ValueString(), id))
+ }
+ }
+ return true
+ })
+ }
+ if len(gjson.Get(body, "devices").Array()) > 0 {
+ path := fmt.Sprintf("/v1/policy-group/%v/device/deploy/", plan.Id.ValueString())
+ res, err = r.client.Post(path, body)
+ if err != nil {
+ diag.AddError("Client Error", fmt.Sprintf("Failed to deploy to policy group devices (POST), got error: %s, %s", err, res.String()))
+ if deleteOnError {
+ r.DeletePolicyGroup(ctx, plan, diag)
+ }
+ return
+ }
+
+ // Wait for deploy action to complete
+ actionId := res.Get("parentTaskId").String()
+ err = helpers.WaitForActionToComplete(ctx, r.client, actionId)
+ if err != nil {
+ diag.AddError("Client Error", fmt.Sprintf("Failed to deploy to config group devices, got error: %s", err))
+ if deleteOnError {
+ r.DeletePolicyGroup(ctx, plan, diag)
+ }
+ return
+ }
+ }
+}
+
+func (r *PolicyGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state PolicyGroup
+
+ // 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.ValueString()))
+
+ oldState := state
+
+ // Read policy group
+ res, err := r.client.Get(state.getPath() + url.QueryEscape(state.Id.ValueString()))
+ if res.Raw == "" && err == nil {
+ 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.fromBodyPolicyGroup(ctx, res)
+
+ // Read policy group device associations
+ path := fmt.Sprintf("/v1/policy-group/%v/device/associate/", state.Id.ValueString())
+ res, err = r.client.Get(path)
+ if strings.Contains(res.Get("error.message").String(), "Invalid policy group passed") {
+ 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.fromBodyPolicyGroupDevices(ctx, res)
+
+ // Read policy group device variables
+ if value := res.Get("devices"); value.Exists() && len(value.Array()) > 0 {
+ path = fmt.Sprintf("/v1/policy-group/%v/device/variables/", state.Id.ValueString())
+ res, err = r.client.Get(path)
+ if strings.Contains(res.Get("error.message").String(), "Invalid policy group passed") {
+ 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.fromBodyPolicyGroupDeviceVariables(ctx, res)
+ }
+
+ state.updateTfAttributes(ctx, &oldState)
+
+ diags = resp.State.Set(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *PolicyGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan, state PolicyGroup
+
+ // 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()))
+
+ // Update policy group
+ body := plan.toBodyPolicyGroup(ctx)
+
+ res, err := r.client.Put(plan.getPath()+url.QueryEscape(plan.Id.ValueString()), body)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure object (PUT), got error: %s, %s", err, res.String()))
+ return
+ }
+
+ res, err = r.client.Get(fmt.Sprintf("/v1/policy-group/%v/device/associate/", plan.Id.ValueString()))
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve object (GET), got error: %s, %s", err, res.String()))
+ return
+ }
+
+ var currentDeviceIds []string
+ if value := res.Get("devices"); value.Exists() && len(value.Array()) > 0 {
+ value.ForEach(func(k, v gjson.Result) bool {
+ currentDeviceIds = append(currentDeviceIds, v.Get("id").String())
+ return true
+ })
+ }
+
+ associateBody, _ := sjson.Set("", "devices", []interface{}{})
+ for _, d := range plan.Devices {
+ found := false
+ for _, cdid := range currentDeviceIds {
+ if d.Id.ValueString() == cdid {
+ found = true
+ break
+ }
+ }
+ if !found {
+ associateBody, _ = sjson.SetRaw(associateBody, "devices.-1", helpers.Must(sjson.Set("", "id", d.Id.ValueString())))
+ }
+ }
+
+ disassociateBody, _ := sjson.Set("", "devices", []interface{}{})
+ for _, cdid := range currentDeviceIds {
+ found := false
+ for _, d := range plan.Devices {
+ if d.Id.ValueString() == cdid {
+ found = true
+ break
+ }
+ }
+ if !found {
+ disassociateBody, _ = sjson.SetRaw(disassociateBody, "devices.-1", helpers.Must(sjson.Set("", "id", cdid)))
+ }
+ }
+
+ // associate missing devices
+ if len(gjson.Get(associateBody, "devices").Array()) > 0 {
+ res, err = r.client.Put(fmt.Sprintf("/v1/policy-group/%v/device/associate/", plan.Id.ValueString()), associateBody)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure policy group devices (PUT), got error: %s, %s", err, res.String()))
+ return
+ }
+ }
+
+ // disassociate extra devices
+ if len(gjson.Get(disassociateBody, "devices").Array()) > 0 {
+ res, err = r.client.DeleteBody(fmt.Sprintf("/v1/policy-group/%v/device/associate/", plan.Id.ValueString()), disassociateBody)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to delete policy group devices (DELETE), got error: %s, %s", err, res.String()))
+ return
+ }
+ }
+
+ // Update policy group device variables
+ if len(plan.Devices) > 0 && plan.hasPolicyGroupDeviceVariables(ctx) {
+ body = plan.toBodyPolicyGroupDeviceVariables(ctx)
+
+ path := fmt.Sprintf("/v1/policy-group/%v/device/variables/", plan.Id.ValueString())
+ res, err = r.client.Put(path, body)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to configure policy group device variables (PUT), got error: %s, %s", err, res.String()))
+ return
+ }
+ }
+
+ // Deploy policy group to devices
+ if len(plan.Devices) > 0 {
+ r.Deploy(ctx, plan, &state, &resp.Diagnostics, false)
+ }
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Update finished successfully", plan.Name.ValueString()))
+
+ diags = resp.State.Set(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *PolicyGroupResource) DeletePolicyGroup(ctx context.Context, state PolicyGroup, diag *diag.Diagnostics) {
+ path := fmt.Sprintf("/v1/policy-group/%v/device/associate/", state.Id.ValueString())
+ res, err := r.client.Get(path)
+ if err == nil {
+ body, _ := sjson.Set("", "devices", []interface{}{})
+ if value := res.Get("devices"); value.Exists() && len(value.Array()) > 0 {
+ value.ForEach(func(k, v gjson.Result) bool {
+ id := v.Get("id").String()
+ itemBody, _ := sjson.Set("", "id", id)
+ body, _ = sjson.SetRaw(body, "devices.-1", itemBody)
+ return true
+ })
+ }
+ if len(gjson.Get(body, "devices").Array()) > 0 {
+ res, err := r.client.DeleteBody(path, body)
+ if err != nil {
+ diag.AddError("Client Error", fmt.Sprintf("Failed to delete policy group devices (DELETE), got error: %s, %s", err, res.String()))
+ return
+ }
+ }
+ }
+
+ res, err = r.client.Delete(state.getPath() + url.QueryEscape(state.Id.ValueString()))
+ if err != nil {
+ diag.AddError("Client Error", fmt.Sprintf("Failed to delete policy group (DELETE), got error: %s, %s", err, res.String()))
+ return
+ }
+}
+
+func (r *PolicyGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state PolicyGroup
+
+ // 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()))
+
+ r.DeletePolicyGroup(ctx, state, &resp.Diagnostics)
+
+ tflog.Debug(ctx, fmt.Sprintf("%s: Delete finished successfully", state.Name.ValueString()))
+
+ resp.State.RemoveResource(ctx)
+}
+
+// Section below is generated&owned by "gen/generator.go". //template:begin import
+func (r *PolicyGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+// End of section. //template:end import
diff --git a/internal/provider/resource_sdwan_policy_group_test.go b/internal/provider/resource_sdwan_policy_group_test.go
new file mode 100644
index 000000000..e42971c27
--- /dev/null
+++ b/internal/provider/resource_sdwan_policy_group_test.go
@@ -0,0 +1,209 @@
+// 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 TestAccSdwanPolicyGroup(t *testing.T) {
+ if os.Getenv("SDWAN_2012") == "" {
+ t.Skip("skipping test, set environment variable SDWAN_2012")
+ }
+ var checks []resource.TestCheckFunc
+ checks = append(checks, resource.TestCheckResourceAttr("sdwan_policy_group.test", "name", "PG_1"))
+ checks = append(checks, resource.TestCheckResourceAttr("sdwan_policy_group.test", "description", "My policy group 1"))
+ checks = append(checks, resource.TestCheckResourceAttr("sdwan_policy_group.test", "solution", "sdwan"))
+ checks = append(checks, resource.TestCheckResourceAttr("sdwan_policy_group.test", "devices.0.id", "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"))
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccSdwanPolicyGroupPrerequisitesConfig + testAccSdwanPolicyGroupConfig_all(),
+ Check: resource.ComposeTestCheckFunc(checks...),
+ },
+ },
+ })
+}
+
+// End of section. //template:end testAcc
+
+// Section below is generated&owned by "gen/generator.go". //template:begin testPrerequisites
+const testAccSdwanPolicyGroupPrerequisitesConfig = `
+resource "sdwan_system_feature_profile" "test" {
+ name = "SYSTEM_TF"
+ description = "Terraform test"
+}
+
+resource "sdwan_system_basic_feature" "test" {
+ name = "BASIC_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_aaa_feature" "test" {
+ name = "AAA_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+ server_auth_order = ["local"]
+ users = [{
+ name = "admin"
+ password = "admin"
+ }]
+}
+
+resource "sdwan_system_bfd_feature" "test" {
+ name = "BFD_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_global_feature" "test" {
+ name = "GLOBAL_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_logging_feature" "test" {
+ name = "LOGGING_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_system_omp_feature" "test" {
+ name = "OMP_TF"
+ feature_profile_id = sdwan_system_feature_profile.test.id
+}
+
+resource "sdwan_transport_feature_profile" "test" {
+ name = "TRANSPORT_TF"
+ description = "My transport feature profile 1"
+}
+
+resource "sdwan_transport_wan_vpn_feature" "test" {
+ name = "WAN_VPN_TF"
+ feature_profile_id = sdwan_transport_feature_profile.test.id
+ vpn = 0
+}
+
+resource "sdwan_transport_wan_vpn_interface_ethernet_feature" "test" {
+ name = "WAN_VPN_INT_TF"
+ feature_profile_id = sdwan_transport_feature_profile.test.id
+ transport_wan_vpn_feature_id = sdwan_transport_wan_vpn_feature.test.id
+ interface_name = "GigabitEthernet1"
+ shutdown = false
+ ipv4_configuration_type = "dynamic"
+ ipv4_dhcp_distance = 1
+ tunnel_interface = true
+ tunnel_interface_encapsulations = [
+ {
+ encapsulation = "ipsec"
+ }
+ ]
+}
+
+resource "sdwan_configuration_group" "test" {
+ name = "CG_1"
+ description = "My config group 1"
+ solution = "sdwan"
+ feature_profile_ids = [
+ sdwan_system_feature_profile.test.id,
+ sdwan_transport_feature_profile.test.id,
+ ]
+ devices = [{
+ id = "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"
+ deploy = true
+ variables = [
+ {
+ name = "host_name"
+ value = "edge1"
+ },
+ {
+ name = "pseudo_commit_timer"
+ value = 0
+ },
+ {
+ name = "site_id"
+ value = 1
+ },
+ {
+ name = "system_ip"
+ value = "10.1.1.1"
+ },
+ {
+ name = "ipv6_strict_control"
+ value = "false"
+ }
+ ]
+ }]
+ feature_versions = [
+ sdwan_system_basic_feature.test.version,
+ sdwan_system_aaa_feature.test.version,
+ sdwan_system_bfd_feature.test.version,
+ sdwan_system_global_feature.test.version,
+ sdwan_system_logging_feature.test.version,
+ sdwan_system_omp_feature.test.version,
+ sdwan_transport_wan_vpn_interface_ethernet_feature.test.version,
+ ]
+}
+
+resource "sdwan_application_priority_feature_profile" "test" {
+ name = "APPLICATION_PRIORITY_TF"
+ description = "Terraform test"
+}
+
+resource "sdwan_application_priority_qos_policy" "test" {
+ name = "qos"
+ description = "QoS policy for application priority"
+ feature_profile_id = sdwan_application_priority_feature_profile.test.id
+ target_interface_variable = "{{qos_interfaces}}"
+}
+
+`
+
+// End of section. //template:end testPrerequisites
+
+func testAccSdwanPolicyGroupConfig_all() string {
+ config := `resource "sdwan_policy_group" "test" {` + "\n"
+ config += ` name = "PG_1"` + "\n"
+ config += ` description = "My policy group 1"` + "\n"
+ config += ` solution = "sdwan"` + "\n"
+ config += ` feature_profile_ids = [sdwan_application_priority_feature_profile.test.id]` + "\n"
+ config += ` devices = [{` + "\n"
+ config += ` id = "C8K-40C0CCFD-9EA8-2B2E-E73B-32C5924EC79B"` + "\n"
+ config += ` deploy = true` + "\n"
+ config += ` variables = [` + "\n"
+ config += ` {` + "\n"
+ config += ` name = "qos_interfaces"` + "\n"
+ config += ` list_value = [` + "\n"
+ config += ` "GigabitEthernet1",` + "\n"
+ config += ` "GigabitEthernet2"` + "\n"
+ config += ` ]` + "\n"
+ config += ` },` + "\n"
+ config += ` ]` + "\n"
+ config += ` }]` + "\n"
+ config += ` policy_versions = [` + "\n"
+ config += ` sdwan_application_priority_qos_policy.test.version,` + "\n"
+ config += ` ]` + "\n"
+ config += ` depends_on = [ sdwan_configuration_group.test ]` + "\n"
+ config += `}` + "\n"
+ return config
+}
diff --git a/templates/guides/changelog.md.tmpl b/templates/guides/changelog.md.tmpl
index 814a41d9e..7b5ea5699 100644
--- a/templates/guides/changelog.md.tmpl
+++ b/templates/guides/changelog.md.tmpl
@@ -10,6 +10,7 @@ description: |-
## 0.6.3 (unreleased)
- Add missing options under `unsupported_features` attribute of `sdwan_configuration_group`, [link](https://github.com/CiscoDevNet/terraform-provider-sdwan/issues/478)
+- Add `sdwan_policy_group` resource and data source
## 0.6.2