diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index 02d989729..bbca1587d 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -29,5 +29,8 @@ jobs: - name: Get latest released version run: echo "PROVIDER_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Install Terraform + uses: hashicorp/setup-terraform@v3 + - name: hclcheck run: make hclcheck diff --git a/.goreleaser.yml b/.goreleaser.yml index d571403af..131f7bd7b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -12,7 +12,7 @@ builds: flags: - -trimpath ldflags: - - '-s -w -X github.com/vmware/terraform-provider-vcd/v3/vcd.BuildVersion={{.Env.BUILDVERSION}}' + - '-s -w -X github.com/schubergphilis/terraform-provider-vcd/v3/vcd.BuildVersion={{.Env.BUILDVERSION}}' goos: - freebsd - windows diff --git a/GNUmakefile b/GNUmakefile index f8ec4b1bd..2c53eb22b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -9,12 +9,11 @@ default: build # builds the plugin injecting output of `git describe` to BuildVersion variable build: fmtcheck - go install -ldflags="-X 'github.com/vmware/terraform-provider-vcd/v3/vcd.BuildVersion=$(GIT_DESCRIBE)'" + go install -ldflags="-X 'github.com/schubergphilis/terraform-provider-vcd/v3/vcd.BuildVersion=$(GIT_DESCRIBE)'" # builds the plugin with race detector enabled and injecting output of `git describe` to BuildVersion variable buildrace: fmtcheck - go install --race -ldflags="-X 'github.com/vmware/terraform-provider-vcd/v3/vcd.BuildVersion=$(GIT_DESCRIBE)'" - + go install --race -ldflags="-X 'github.com/schubergphilis/terraform-provider-vcd/v3/vcd.BuildVersion=$(GIT_DESCRIBE)'" # creates a .zip archive of the code dist: git archive --format=zip -o source.zip HEAD @@ -79,7 +78,6 @@ testunit: fmtcheck # Runs the basic execution test test: testunit tagverify - @sh -c "'$(CURDIR)/scripts/runtest.sh' short" # Runs the full acceptance test as Org user testacc-orguser: testunit diff --git a/README.md b/README.md index f24a3ff05..f9d119e8a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ Terraform VMware Cloud Director Provider ================== +## IMPORTANT: Fork notice + +This is a fork of the VCD provider intended for usage at Schuberg Philis. The official provider seems to be no longer +maintained. The aim of this fork is to add some missing functionalities to address the specific needs of Schuberg Philis. Use this fork at your own discretion and risk. + The official Terraform provider for [VMware Cloud Director](https://www.vmware.com/products/cloud-director.html) - Documentation of the latest binary release available at https://registry.terraform.io/providers/vmware/vcd/latest/docs @@ -155,4 +160,4 @@ In this block, the `vmware` part of the source corresponds to the directory Note that `versions.tf` is generated when you run the `terraform 0.13upgrade` command. If you have run such command, you need to edit the file and make sure the **`source`** path corresponds to the one installed, or remove the file -altogether if you have already the right block in your script. \ No newline at end of file +altogether if you have already the right block in your script. diff --git a/VERSION b/VERSION index 62b6d193d..64d4aab16 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.14.2 +v3.15.0 diff --git a/go.mod b/go.mod index 697ff4cad..073956464 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/vmware/terraform-provider-vcd/v3 +module github.com/schubergphilis/terraform-provider-vcd/v3 go 1.22.3 diff --git a/main.go b/main.go index a45b3efb1..b7615fae7 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,7 @@ package main import ( "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" - "github.com/vmware/terraform-provider-vcd/v3/vcd" + "github.com/schubergphilis/terraform-provider-vcd/v3/vcd" ) func main() { diff --git a/scripts/install-plugin.sh b/scripts/install-plugin.sh index 1fd900809..1a32331aa 100755 --- a/scripts/install-plugin.sh +++ b/scripts/install-plugin.sh @@ -68,7 +68,7 @@ arch=${goos}_${goarch} # if terraform executable is 0.13+, we use the new path if [[ $terraform_major -gt 0 || $terraform_major -eq 0 && $terraform_minor > 12 ]] then - target_dir=$HOME/.terraform.d/plugins/registry.terraform.io/vmware/vcd/$bare_version/$arch + target_dir=$HOME/.terraform.d/plugins/registry.terraform.io/schubergphilis/vcd/$bare_version/$arch fi plugin_name=terraform-provider-vcd diff --git a/vcd/provider.go b/vcd/provider.go index 4591e26a4..226f27abb 100644 --- a/vcd/provider.go +++ b/vcd/provider.go @@ -301,6 +301,7 @@ var globalResourceMap = map[string]*schema.Resource{ "vcd_nsxt_alb_virtual_service_http_req_rules": resourceVcdAlbVirtualServiceReqRules(), // 3.14 "vcd_nsxt_alb_virtual_service_http_resp_rules": resourceVcdAlbVirtualServiceRespRules(), // 3.14 "vcd_nsxt_alb_virtual_service_http_sec_rules": resourceVcdAlbVirtualServiceSecRules(), // 3.14 + "vcd_nsxt_firewall_rule": resourceVcdNsxtFirewallRule(), // 3.15 } // Provider returns a terraform.ResourceProvider. diff --git a/vcd/resource_vcd_nsxt_firewall_rule.go b/vcd/resource_vcd_nsxt_firewall_rule.go new file mode 100644 index 000000000..fdf1d9f79 --- /dev/null +++ b/vcd/resource_vcd_nsxt_firewall_rule.go @@ -0,0 +1,419 @@ +package vcd + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/vmware/go-vcloud-director/v2/govcd" + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +type NsxtFirewallRuleV2 struct { + // ID contains UUID (e.g. d0bf5d51-f83a-489a-9323-1661024874b8) + ID string `json:"id,omitempty"` + // Name - API does not enforce uniqueness + Name string `json:"name"` + // Action field. Can be 'ALLOW', 'DROP' + // Deprecated in favor of ActionValue in VCD 10.2.2+ (API V35.2) + Action string `json:"action,omitempty"` + + // ActionValue replaces deprecated field Action and defines action to be applied to all the + // traffic that meets the firewall rule criteria. It determines if the rule permits or blocks + // traffic. Property is required if action is not set. Below are valid values: + // * ALLOW permits traffic to go through the firewall. + // * DROP blocks the traffic at the firewall. No response is sent back to the source. + // * REJECT blocks the traffic at the firewall. A response is sent back to the source. + ActionValue string `json:"actionValue,omitempty"` + + // Active allows to enable or disable the rule + Active bool `json:"active"` + // SourceFirewallGroups contains a list of references to Firewall Groups. Empty list means 'Any' + SourceFirewallGroups []types.OpenApiReference `json:"sourceFirewallGroups,omitempty"` + // DestinationFirewallGroups contains a list of references to Firewall Groups. Empty list means 'Any' + DestinationFirewallGroups []types.OpenApiReference `json:"destinationFirewallGroups,omitempty"` + // ApplicationPortProfiles contains a list of references to Application Port Profiles. Empty list means 'Any' + ApplicationPortProfiles []types.OpenApiReference `json:"applicationPortProfiles,omitempty"` + // IpProtocol 'IPV4', 'IPV6', 'IPV4_IPV6' + IpProtocol string `json:"ipProtocol"` + Logging bool `json:"logging"` + // Direction 'IN_OUT', 'OUT', 'IN' + Direction string `json:"direction"` + // Version of firewall rule. Must not be set when creating. + Version *struct { + // Version is incremented after each update + Version *int `json:"version,omitempty"` + } `json:"version,omitempty"` +} + +type NsxtFirewallRuleContainerV2 struct { + SystemRules []*NsxtFirewallRuleV2 `json:"systemRules"` + DefaultRules []*NsxtFirewallRuleV2 `json:"defaultRules"` + UserDefinedRules []*NsxtFirewallRuleV2 `json:"userDefinedRules"` +} + +func resourceVcdNsxtFirewallRule() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVcdNsxtFirewallRuleCreate, + ReadContext: resourceVcdNsxtFirewallRuleRead, + UpdateContext: resourceVcdNsxtFirewallRuleUpdate, + DeleteContext: resourceVcdNsxtFirewallRuleDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceVcdNsxtFirewallRuleImport, + }, + + Schema: map[string]*schema.Schema{ + "org": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of organization to use, optional if defined at provider " + + "level. Useful when connected as sysadmin working across different organizations", + }, + "edge_gateway_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Edge Gateway ID in which Firewall Rule are located", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "Firewall Rule name", + }, + "action": { + Type: schema.TypeString, + Required: true, + Description: "Defines if the rule should 'ALLOW', 'DROP' or 'REJECT' matching traffic", + ValidateFunc: validation.StringInSlice([]string{"ALLOW", "DROP", "REJECT"}, false), + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Defined if Firewall Rule is active", + }, + "logging": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Defines if matching traffic should be logged", + }, + "direction": { + Type: schema.TypeString, + Required: true, + Description: "Direction on which Firewall Rule applies (One of 'IN', 'OUT', 'IN_OUT')", + ValidateFunc: validation.StringInSlice([]string{"IN", "OUT", "IN_OUT"}, false), + }, + "ip_protocol": { + Type: schema.TypeString, + Required: true, + Description: "Firewall Rule Protocol (One of 'IPV4', 'IPV6', 'IPV4_IPV6')", + ValidateFunc: validation.StringInSlice([]string{"IPV4", "IPV6", "IPV4_IPV6"}, false), + }, + "source_ids": { + Type: schema.TypeSet, + Optional: true, + Description: "A set of Source Firewall Group IDs (IP Sets or Security Groups). Leaving it empty means 'Any'", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "destination_ids": { + Type: schema.TypeSet, + Optional: true, + Description: "A set of Destination Firewall Group IDs (IP Sets or Security Groups). Leaving it empty means 'Any'", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "app_port_profile_ids": { + Type: schema.TypeSet, + Optional: true, + Description: "A set of Application Port Profile IDs. Leaving it empty means 'Any'", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "above_rule_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "ID of the rule above which this rule should be created", + }, + }, + } +} + +func resourceVcdNsxtFirewallRuleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + orgName := d.Get("org").(string) + edgeGatewayId := d.Get("edge_gateway_id").(string) + + // Confirm Edge Gateway exists and we have access + _, err := vcdClient.GetNsxtEdgeGatewayById(orgName, edgeGatewayId) + if err != nil { + return diag.Errorf("error retrieving Edge Gateway: %s", err) + } + + unlock, err := vcdClient.lockParentVdcGroupOrEdgeGateway(d) + if err != nil { + return diag.Errorf("[edge firewall rule create] %s", err) + } + + defer unlock() + + rule := getNsxtFirewallRuleFromSchema(d) + + endpoint, err := vcdClient.Client.OpenApiBuildEndpoint(fmt.Sprintf("%sedgeGateways/%s/firewall/rules/", types.OpenApiPathVersion2_0_0, edgeGatewayId)) + minimumApiVersion := "39.1" + if err != nil { + return diag.FromErr(err) + } + + // This API endpoints returns the wrong owner object. We can get the correct ID from the task details. + task, err := vcdClient.Client.OpenApiPostItemAsync(minimumApiVersion, endpoint, nil, rule) + if err != nil { + return diag.FromErr(err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return diag.FromErr(err) + } + + returnReq := &NsxtFirewallRuleV2{} + returnReq.ID = task.Task.Details + + d.SetId(returnReq.ID) + + return resourceVcdNsxtFirewallRuleRead(ctx, d, meta) +} + +func resourceVcdNsxtFirewallRuleRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + edgeGatewayId := d.Get("edge_gateway_id").(string) + ruleId := d.Id() + + if ruleId == "" { + return diag.Errorf("empty Firewall Rule ID") + } + + endpoint := fmt.Sprintf(types.OpenApiPathVersion2_0_0+types.OpenApiEndpointNsxtFirewallRules, edgeGatewayId) + minimumApiVersion := "39.1" + + urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint+"/%s", ruleId)) + if err != nil { + return diag.FromErr(err) + } + + rule := &NsxtFirewallRuleV2{} + + err = vcdClient.Client.OpenApiGetItem(minimumApiVersion, urlRef, nil, rule, nil) + if err != nil { + if govcd.ContainsNotFound(err) { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + setNsxtFirewallRuleToSchema(d, rule) + return nil +} + +func resourceVcdNsxtFirewallRuleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + orgName := d.Get("org").(string) + edgeGatewayId := d.Get("edge_gateway_id").(string) + ruleId := d.Id() + + _, err := vcdClient.GetNsxtEdgeGatewayById(orgName, edgeGatewayId) + if err != nil { + return diag.Errorf("error retrieving Edge Gateway: %s", err) + } + + unlock, err := vcdClient.lockParentVdcGroupOrEdgeGateway(d) + if err != nil { + return diag.Errorf("[edge firewall rule update] %s", err) + } + + defer unlock() + + rule := getNsxtFirewallRuleFromSchema(d) + rule.ID = ruleId + + endpoint := fmt.Sprintf(types.OpenApiPathVersion2_0_0+types.OpenApiEndpointNsxtFirewallRules, edgeGatewayId) + minimumApiVersion := "39.1" + + urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint+"/%s", ruleId)) + if err != nil { + return diag.FromErr(err) + } + + existingRule := &NsxtFirewallRuleV2{} + err = vcdClient.Client.OpenApiGetItem(minimumApiVersion, urlRef, nil, existingRule, nil) + if err != nil { + return diag.FromErr(err) + } + rule.Version = existingRule.Version + + returnReq := &NsxtFirewallRuleV2{} + err = vcdClient.Client.OpenApiPutItem(minimumApiVersion, urlRef, nil, rule, returnReq, nil) + if err != nil { + return diag.FromErr(err) + } + + return resourceVcdNsxtFirewallRuleRead(ctx, d, meta) +} + +func resourceVcdNsxtFirewallRuleDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + orgName := d.Get("org").(string) + edgeGatewayId := d.Get("edge_gateway_id").(string) + ruleId := d.Id() + + _, err := vcdClient.GetNsxtEdgeGatewayById(orgName, edgeGatewayId) + if err != nil { + if govcd.ContainsNotFound(err) { + return nil + } + return diag.Errorf("error retrieving Edge Gateway: %s", err) + } + + unlock, err := vcdClient.lockParentVdcGroupOrEdgeGateway(d) + if err != nil { + return diag.Errorf("[edge firewall rule delete] %s", err) + } + + defer unlock() + + endpoint, err := vcdClient.Client.OpenApiBuildEndpoint(fmt.Sprintf("%sedgeGateways/%s/firewall/rules/%s", types.OpenApiPathVersion2_0_0, edgeGatewayId, ruleId)) + minimumApiVersion := "39.1" + if err != nil { + return diag.FromErr(err) + } + + err = vcdClient.Client.OpenApiDeleteItem(minimumApiVersion, endpoint, nil, nil) + if err != nil { + if govcd.ContainsNotFound(err) { + return nil + } + return diag.FromErr(err) + } + + return nil +} + +func resourceVcdNsxtFirewallRuleImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + parts := splitImportId(d.Id()) + + if len(parts) == 3 { + orgName := parts[0] + edgeName := parts[1] + ruleName := parts[2] + + vcdClient := meta.(*VCDClient) + org, err := vcdClient.GetOrgByName(orgName) + if err != nil { + return nil, fmt.Errorf("error retrieving Org '%s': %s", orgName, err) + } + + edge, err := org.GetNsxtEdgeGatewayByName(edgeName) + if err != nil { + return nil, fmt.Errorf("error retrieving Edge Gateway '%s': %s", edgeName, err) + } + + endpoint, err := vcdClient.Client.OpenApiBuildEndpoint(fmt.Sprintf("%sedgeGateways/%s/firewall/rules", types.OpenApiPathVersion2_0_0, edge.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + var container *NsxtFirewallRuleContainerV2 = &NsxtFirewallRuleContainerV2{} + + err = vcdClient.Client.OpenApiGetItem("39.1", endpoint, nil, container, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Firewall Rules: %s", err) + } + + var foundRule *NsxtFirewallRuleV2 + + // Only search in UserDefinedRules as we likely only manage those + for _, rule := range container.UserDefinedRules { + if rule.Name == ruleName { + foundRule = rule + break + } + } + + if foundRule == nil { + return nil, fmt.Errorf("could not find firewall rule with name '%s' in edge gateway '%s'", ruleName, edgeName) + } + + dSet(d, "org", orgName) + dSet(d, "edge_gateway_id", edge.EdgeGateway.ID) + d.SetId(foundRule.ID) + + return []*schema.ResourceData{d}, nil + } + + if len(parts) != 2 { + return nil, fmt.Errorf("import ID must be in format 'edge_gateway_id.rule_id' or 'org.edge_name.rule_name'") + } + + dSet(d, "edge_gateway_id", parts[0]) + d.SetId(parts[1]) + + return []*schema.ResourceData{d}, nil +} + +func splitImportId(id string) []string { + return strings.Split(id, ImportSeparator) +} + +func getNsxtFirewallRuleFromSchema(d *schema.ResourceData) *NsxtFirewallRuleV2 { + rule := &NsxtFirewallRuleV2{ + Name: d.Get("name").(string), + ActionValue: d.Get("action").(string), + Active: d.Get("enabled").(bool), + Logging: d.Get("logging").(bool), + Direction: d.Get("direction").(string), + IpProtocol: d.Get("ip_protocol").(string), + } + + if v, ok := d.GetOk("source_ids"); ok { + rule.SourceFirewallGroups = convertSliceOfStringsToOpenApiReferenceIds(convertSchemaSetToSliceOfStrings(v.(*schema.Set))) + } + + if v, ok := d.GetOk("destination_ids"); ok { + rule.DestinationFirewallGroups = convertSliceOfStringsToOpenApiReferenceIds(convertSchemaSetToSliceOfStrings(v.(*schema.Set))) + } + + if v, ok := d.GetOk("app_port_profile_ids"); ok { + rule.ApplicationPortProfiles = convertSliceOfStringsToOpenApiReferenceIds(convertSchemaSetToSliceOfStrings(v.(*schema.Set))) + } + + return rule +} + +func setNsxtFirewallRuleToSchema(d *schema.ResourceData, rule *NsxtFirewallRuleV2) { + dSet(d, "name", rule.Name) + dSet(d, "action", rule.ActionValue) + dSet(d, "enabled", rule.Active) + dSet(d, "logging", rule.Logging) + dSet(d, "direction", rule.Direction) + dSet(d, "ip_protocol", rule.IpProtocol) + + if err := d.Set("source_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.SourceFirewallGroups))); err != nil { + fmt.Printf("error setting source_ids in schema: %s", err) + } + if err := d.Set("destination_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.DestinationFirewallGroups))); err != nil { + fmt.Printf("error setting destination_ids in schema: %s", err) + } + if err := d.Set("app_port_profile_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.ApplicationPortProfiles))); err != nil { + fmt.Printf("error setting app_port_profile_ids in schema: %s", err) + } +} diff --git a/vcd/resource_vcd_nsxt_firewall_rule_test.go b/vcd/resource_vcd_nsxt_firewall_rule_test.go new file mode 100644 index 000000000..c3c61c662 --- /dev/null +++ b/vcd/resource_vcd_nsxt_firewall_rule_test.go @@ -0,0 +1,239 @@ +//go:build network || nsxt || functional || ALL + +package vcd + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccVcdNsxtFirewallRule_Basic(t *testing.T) { + preTestChecks(t) + + ruleName := "test-acc-firewall-rule-basic" + resourceName := "vcd_nsxt_firewall_rule.test" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckVcdNsxtFirewallRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVcdNsxtFirewallRuleConfig(ruleName, "ALLOW", "IN_OUT"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVcdNsxtFirewallRuleExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", ruleName), + resource.TestCheckResourceAttr(resourceName, "action", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, "direction", "IN_OUT"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "logging", "false"), + resource.TestCheckResourceAttr(resourceName, "ip_protocol", "IPV4"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccVcdNsxtFirewallRuleImportStateIdFunc(resourceName), + }, + { + Config: testAccVcdNsxtFirewallRuleConfig(ruleName+"-updated", "DROP", "IN"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVcdNsxtFirewallRuleExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", ruleName+"-updated"), + resource.TestCheckResourceAttr(resourceName, "action", "DROP"), + resource.TestCheckResourceAttr(resourceName, "direction", "IN"), + ), + }, + }, + }) +} + +func TestAccVcdNsxtFirewallRule_Complete(t *testing.T) { + preTestChecks(t) + + ruleName := "test-acc-firewall-rule-complete" + resourceName := "vcd_nsxt_firewall_rule.test" + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckVcdNsxtFirewallRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVcdNsxtFirewallRuleCompleteConfig(ruleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVcdNsxtFirewallRuleExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", ruleName), + resource.TestCheckResourceAttr(resourceName, "action", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, "direction", "IN_OUT"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "logging", "true"), + resource.TestCheckResourceAttr(resourceName, "ip_protocol", "IPV4_IPV6"), + resource.TestCheckResourceAttr(resourceName, "source_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "destination_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "app_port_profile_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccVcdNsxtFirewallRuleImportStateIdFunc(resourceName), + }, + }, + }) +} + +func testAccVcdNsxtFirewallRuleCompleteConfig(name string) string { + return fmt.Sprintf(` + data "vcd_org" "test" { + name = "%s" + } + + data "vcd_nsxt_edgegateway" "test" { + org = data.vcd_org.test.name + name = "%s" + } + + resource "vcd_nsxt_ip_set" "src" { + edge_gateway_id = data.vcd_nsxt_edgegateway.test.id + name = "%s-src" + ip_addresses = ["1.1.1.1"] + } + + resource "vcd_nsxt_security_group" "dst" { + edge_gateway_id = data.vcd_nsxt_edgegateway.test.id + name = "%s-dst" + } + + resource "vcd_nsxt_app_port_profile" "app" { + org = data.vcd_org.test.name + name = "%s-app" + scope = "TENANT" + app_port { + protocol = "TCP" + port = ["443"] + } + } + + resource "vcd_nsxt_firewall_rule" "test" { + org = data.vcd_org.test.name + edge_gateway_id = data.vcd_nsxt_edgegateway.test.id + name = "%s" + action = "ALLOW" + direction = "IN_OUT" + enabled = true + logging = true + ip_protocol = "IPV4_IPV6" + + source_ids = [vcd_nsxt_ip_set.src.id] + destination_ids = [vcd_nsxt_security_group.dst.id] + app_port_profile_ids = [vcd_nsxt_app_port_profile.app.id] + } + `, testConfig.VCD.Org, testConfig.Nsxt.EdgeGateway, name, name, name, name) +} + +func testAccCheckVcdNsxtFirewallRuleDestroy(s *terraform.State) error { + vcdClient := testAccProvider.Meta().(*VCDClient) + for _, rs := range s.RootModule().Resources { + if rs.Type != "vcd_nsxt_firewall_rule" { + continue + } + + orgName := rs.Primary.Attributes["org"] + edgeGatewayId := rs.Primary.Attributes["edge_gateway_id"] + ruleId := rs.Primary.ID + + egw, err := vcdClient.GetNsxtEdgeGatewayById(orgName, edgeGatewayId) + if err != nil { + return err + } + + firewall, err := egw.GetNsxtFirewall() + if err != nil { + return err + } + + for _, rule := range firewall.NsxtFirewallRuleContainer.UserDefinedRules { + if rule.ID == ruleId { + return fmt.Errorf("NSX-T Firewall Rule %s still exists", ruleId) + } + } + } + return nil +} + +func testAccCheckVcdNsxtFirewallRuleExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no NSX-T Firewall Rule ID is set") + } + + vcdClient := testAccProvider.Meta().(*VCDClient) + orgName := rs.Primary.Attributes["org"] + edgeGatewayId := rs.Primary.Attributes["edge_gateway_id"] + + egw, err := vcdClient.GetNsxtEdgeGatewayById(orgName, edgeGatewayId) + if err != nil { + return err + } + + firewall, err := egw.GetNsxtFirewall() + if err != nil { + return err + } + + found := false + for _, rule := range firewall.NsxtFirewallRuleContainer.UserDefinedRules { + if rule.ID == rs.Primary.ID { + found = true + break + } + } + + if !found { + return fmt.Errorf("NSX-T Firewall Rule %s not found", rs.Primary.ID) + } + + return nil + } +} + +func testAccVcdNsxtFirewallRuleImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("not found: %s", resourceName) + } + return fmt.Sprintf("%s.%s", rs.Primary.Attributes["edge_gateway_id"], rs.Primary.ID), nil + } +} + +func testAccVcdNsxtFirewallRuleConfig(name, action, direction string) string { + return fmt.Sprintf(` + data "vcd_org" "test" { + name = "%s" + } + + data "vcd_nsxt_edgegateway" "test" { + org = data.vcd_org.test.name + name = "%s" + } + + resource "vcd_nsxt_firewall_rule" "test" { + org = data.vcd_org.test.name + edge_gateway_id = data.vcd_nsxt_edgegateway.test.id + name = "%s" + action = "%s" + direction = "%s" + ip_protocol = "IPV4" + } + `, testConfig.VCD.Org, testConfig.Nsxt.EdgeGateway, name, action, direction) +} diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 9605f4f3c..94fde9c2e 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -6,7 +6,7 @@ description: |- The VMware Cloud Director provider is used to interact with the resources supported by VMware Cloud Director. The provider needs to be configured with the proper credentials before it can be used. --- -# VMware Cloud Director Provider 3.14 +# VMware Cloud Director Provider 3.15 The VMware Cloud Director provider is used to interact with the resources supported by VMware Cloud Director. The provider needs to be configured with the proper credentials before it can be used.