Skip to content

Commit

Permalink
[feature] Network Policy Resource (#271)
Browse files Browse the repository at this point in the history
Adds support for managing Snowflake Network Policy objects via the new `snowflake_network_policy` resource type.

## Test Plan
<!-- detail ways in which this PR has been tested or needs to be tested -->
* [x] acceptance tests
<!-- add more below if you think they are relevant -->
* [x] tested built provider locally with trial Snowflake account

## References
<!-- issues documentation links, etc  -->
* Issue: #221
* https://docs.snowflake.com/en/sql-reference/sql/create-network-policy.html
  • Loading branch information
grahamplace committed Oct 23, 2020
1 parent 9ea90d9 commit 4dd6077
Show file tree
Hide file tree
Showing 11 changed files with 1,085 additions and 24 deletions.
13 changes: 13 additions & 0 deletions docs/resources/network_policy.md
@@ -0,0 +1,13 @@

# snowflake_network_policy

<!-- These docs are auto-generated by code in ./docgen, run by with make docs. Manual edits will be overwritten. -->

## properties

| NAME | TYPE | DESCRIPTION | OPTIONAL | REQUIRED | COMPUTED | DEFAULT |
|-----------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------|----------|---------|
| allowed_ip_list | set | Specifies one or more IPv4 addresses (CIDR notation) that are allowed access to your Snowflake account | false | true | false | |
| blocked_ip_list | set | Specifies one or more IPv4 addresses (CIDR notation) that are denied access to your Snowflake account<br><br>**Do not** add `0.0.0.0/0` to `blocked_ip_list` | true | false | false | |
| comment | string | Specifies a comment for the network policy. | true | false | false | |
| name | string | Specifies the identifier for the network policy; must be unique for the account in which the network policy is created. | false | true | false | |
12 changes: 12 additions & 0 deletions docs/resources/network_policy_attachment.md
@@ -0,0 +1,12 @@

# snowflake_network_policy_attachment

<!-- These docs are auto-generated by code in ./docgen, run by with make docs. Manual edits will be overwritten. -->

## properties

| NAME | TYPE | DESCRIPTION | OPTIONAL | REQUIRED | COMPUTED | DEFAULT |
|---------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------|----------|---------|
| network_policy_name | string | Specifies the identifier for the network policy; must be unique for the account in which the network policy is created. | false | true | false | |
| set_for_account | bool | Specifies whether the network policy should be applied globally to your Snowflake account<br><br>**Note:** The Snowflake user running `terraform apply` must be on an IP address allowed by the network policy to set that policy globally on the Snowflake account.<br><br>Additionally, a Snowflake account can only have one network policy set globally at any given time. This resource does not enforce one-policy-per-account, it is the user's responsibility to enforce this. If multiple network policy resources have `set_for_account: true`, the final policy set on the account will be non-deterministic. | true | false | false | false |
| users | set | Specifies which users the network policy should be attached to | true | false | false | |
50 changes: 26 additions & 24 deletions pkg/provider/provider.go
Expand Up @@ -68,30 +68,32 @@ func Provider() *schema.Provider {
},
},
ResourcesMap: map[string]*schema.Resource{
"snowflake_account_grant": resources.AccountGrant(),
"snowflake_database": resources.Database(),
"snowflake_database_grant": resources.DatabaseGrant(),
"snowflake_integration_grant": resources.IntegrationGrant(),
"snowflake_managed_account": resources.ManagedAccount(),
"snowflake_pipe": resources.Pipe(),
"snowflake_resource_monitor": resources.ResourceMonitor(),
"snowflake_resource_monitor_grant": resources.ResourceMonitorGrant(),
"snowflake_role": resources.Role(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_schema": resources.Schema(),
"snowflake_schema_grant": resources.SchemaGrant(),
"snowflake_share": resources.Share(),
"snowflake_stage": resources.Stage(),
"snowflake_stage_grant": resources.StageGrant(),
"snowflake_storage_integration": resources.StorageIntegration(),
"snowflake_user": resources.User(),
"snowflake_view": resources.View(),
"snowflake_view_grant": resources.ViewGrant(),
"snowflake_task": resources.Task(),
"snowflake_table": resources.Table(),
"snowflake_table_grant": resources.TableGrant(),
"snowflake_warehouse": resources.Warehouse(),
"snowflake_warehouse_grant": resources.WarehouseGrant(),
"snowflake_account_grant": resources.AccountGrant(),
"snowflake_database": resources.Database(),
"snowflake_database_grant": resources.DatabaseGrant(),
"snowflake_integration_grant": resources.IntegrationGrant(),
"snowflake_managed_account": resources.ManagedAccount(),
"snowflake_network_policy": resources.NetworkPolicy(),
"snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(),
"snowflake_pipe": resources.Pipe(),
"snowflake_resource_monitor": resources.ResourceMonitor(),
"snowflake_resource_monitor_grant": resources.ResourceMonitorGrant(),
"snowflake_role": resources.Role(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_schema": resources.Schema(),
"snowflake_schema_grant": resources.SchemaGrant(),
"snowflake_share": resources.Share(),
"snowflake_stage": resources.Stage(),
"snowflake_stage_grant": resources.StageGrant(),
"snowflake_storage_integration": resources.StorageIntegration(),
"snowflake_user": resources.User(),
"snowflake_view": resources.View(),
"snowflake_view_grant": resources.ViewGrant(),
"snowflake_task": resources.Task(),
"snowflake_table": resources.Table(),
"snowflake_table_grant": resources.TableGrant(),
"snowflake_warehouse": resources.Warehouse(),
"snowflake_warehouse_grant": resources.WarehouseGrant(),
},
DataSourcesMap: map[string]*schema.Resource{
"snowflake_system_get_aws_sns_iam_policy": datasources.SystemGetAWSSNSIAMPolicy(),
Expand Down
224 changes: 224 additions & 0 deletions pkg/resources/network_policy.go
@@ -0,0 +1,224 @@
package resources

import (
"database/sql"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/pkg/errors"

"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
)

var networkPolicySchema = map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
Description: "Specifies the identifier for the network policy; must be unique for the account in which the network policy is created.",
ForceNew: true,
},
"allowed_ip_list": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Required: true,
Description: "Specifies one or more IPv4 addresses (CIDR notation) that are allowed access to your Snowflake account",
},
// TODO: Add a ValidationFunc to ensure 0.0.0.0/0 is not in blocked_ip_list
// See: https://docs.snowflake.com/en/user-guide/network-policies.html#create-an-account-level-network-policy
"blocked_ip_list": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Description: "Specifies one or more IPv4 addresses (CIDR notation) that are denied access to your Snowflake account<br><br>**Do not** add `0.0.0.0/0` to `blocked_ip_list`",
},
"comment": {
Type: schema.TypeString,
Optional: true,
Description: "Specifies a comment for the network policy.",
},
}

// NetworkPolicy returns a pointer to the resource representing a network policy
func NetworkPolicy() *schema.Resource {
return &schema.Resource{
Create: CreateNetworkPolicy,
Read: ReadNetworkPolicy,
Update: UpdateNetworkPolicy,
Delete: DeleteNetworkPolicy,

Schema: networkPolicySchema,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
}
}

// CreateNetworkPolicy implements schema.CreateFunc
func CreateNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := data.Get("name").(string)
builder := snowflake.NetworkPolicy(name)

// Set optionals
if v, ok := data.GetOk("comment"); ok {
builder.WithComment(v.(string))
}

if v, ok := data.GetOk("allowed_ip_list"); ok {
builder.WithAllowedIpList(expandStringList(v.(*schema.Set).List()))
}

if v, ok := data.GetOk("blocked_ip_list"); ok {
builder.WithBlockedIpList(expandStringList(v.(*schema.Set).List()))
}

stmt := builder.Create()
err := snowflake.Exec(db, stmt)
if err != nil {
return errors.Wrapf(err, "error creating network policy %v", name)
}
data.SetId(name)

return ReadNetworkPolicy(data, meta)
}

// ReadNetworkPolicy implements schema.ReadFunc
func ReadNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
policyName := data.Id()

builder := snowflake.NetworkPolicy(policyName)

// There is no way to SHOW a single Network Policy, so we have to read *all* network policies and filter in memory
showSql := builder.ShowAllNetworkPolicies()

rows, err := snowflake.Query(db, showSql)
if err != nil {
return err
}

allPolicies, err := snowflake.ScanNetworkPolicies(rows)
if err != nil {
return err
}

var s *snowflake.NetworkPolicyStruct
for _, value := range allPolicies {
if value.Name.String == policyName {
s = value
}
}

descSql := builder.Describe()
rows, err = snowflake.Query(db, descSql)
if err != nil {
return err
}

err = data.Set("name", s.Name.String)
if err != nil {
return err
}

err = data.Set("comment", s.Comment.String)
if err != nil {
return err
}

var (
name string
value string
)
for rows.Next() {
err := rows.Scan(&name, &value)
if err != nil {
return err
}

if name == "ALLOWED_IP_LIST" {
err = data.Set("allowed_ip_list", strings.Split(value, ","))
if err != nil {
return err
}
} else if name == "BLOCKED_IP_LIST" {
err = data.Set("blocked_ip_list", strings.Split(value, ","))
if err != nil {
return err
}
}
}

return err
}

// UpdateNetworkPolicy implements schema.UpdateFunc
func UpdateNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := data.Id()
builder := snowflake.NetworkPolicy(name)

if data.HasChange("comment") {
_, comment := data.GetChange("comment")

if c := comment.(string); c == "" {
q := builder.RemoveComment()
err := snowflake.Exec(db, q)
if err != nil {
return errors.Wrapf(err, "error unsetting comment for network policy %v", name)
}
} else {
q := builder.ChangeComment(c)
err := snowflake.Exec(db, q)
if err != nil {
return errors.Wrapf(err, "error updating comment for network policy %v", name)
}
}
}

if data.HasChange("allowed_ip_list") {
newIps := ipChangeParser(data, "allowed_ip_list")
q := builder.ChangeIpList("ALLOWED", newIps)
err := snowflake.Exec(db, q)
if err != nil {
return errors.Wrapf(err, "error updating ALLOWED_IP_LIST for network policy %v", name)
}
}

if data.HasChange("blocked_ip_list") {
newIps := ipChangeParser(data, "blocked_ip_list")
q := builder.ChangeIpList("BLOCKED", newIps)
err := snowflake.Exec(db, q)
if err != nil {
return errors.Wrapf(err, "error updating BLOCKED_IP_LIST for network policy %v", name)
}
}

return ReadNetworkPolicy(data, meta)
}

// DeleteNetworkPolicy implements schema.DeleteFunc
func DeleteNetworkPolicy(data *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := data.Id()

dropSql := snowflake.NetworkPolicy(name).Drop()
err := snowflake.Exec(db, dropSql)
if err != nil {
return errors.Wrapf(err, "error deleting network policy %v", name)
}

data.SetId("")
return nil
}

// ipChangeParser is a helper function to parse a given ip list change from ResourceData
func ipChangeParser(data *schema.ResourceData, key string) []string {
_, ipChangeSet := data.GetChange(key)
ipList := ipChangeSet.(*schema.Set).List()
newIps := make([]string, len(ipList))
for idx, value := range ipList {
newIps[idx] = fmt.Sprintf("%v", value)
}
return newIps
}
74 changes: 74 additions & 0 deletions pkg/resources/network_policy_acceptance_test.go
@@ -0,0 +1,74 @@
package resources_test

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

const (
networkPolicyComment = "Created by a Terraform acceptance test"
)

func TestAccNetworkPolicy(t *testing.T) {
if _, ok := os.LookupEnv("SKIP_NETWORK_POLICY_TESTS"); ok {
t.Skip("Skipping TestAccNetworkPolicy")
}

name := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha)

resource.Test(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: networkPolicyConfig(name),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_network_policy.test", "name", name),
resource.TestCheckResourceAttr("snowflake_network_policy.test", "comment", networkPolicyComment),
resource.TestCheckResourceAttr("snowflake_network_policy.test", "allowed_ip_list.#", "2"),
resource.TestCheckResourceAttr("snowflake_network_policy.test", "blocked_ip_list.#", "0"),
),
},
// CHANGE PROPERTIES
{
Config: networkPolicyConfig2(name),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_network_policy.test", "name", name),
resource.TestCheckResourceAttr("snowflake_network_policy.test", "comment", networkPolicyComment),
resource.TestCheckResourceAttr("snowflake_network_policy.test", "allowed_ip_list.#", "1"),
resource.TestCheckResourceAttr("snowflake_network_policy.test", "blocked_ip_list.#", "1"),
),
},
// IMPORT
{
ResourceName: "snowflake_network_policy.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func networkPolicyConfig(name string) string {
return fmt.Sprintf(`
resource "snowflake_network_policy" "test" {
name = "%v"
comment = "%v"
allowed_ip_list = ["192.168.0.100/24", "29.254.123.20"]
}
`, name, networkPolicyComment)
}

func networkPolicyConfig2(name string) string {
return fmt.Sprintf(`
resource "snowflake_network_policy" "test" {
name = "%v"
comment = "%v"
allowed_ip_list = ["192.168.0.100/24"]
blocked_ip_list = ["192.168.0.101"]
}
`, name, networkPolicyComment)
}

0 comments on commit 4dd6077

Please sign in to comment.