From 2fbf07f6c226876a29315ea4d4d5690123c2a9c7 Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Wed, 4 Feb 2026 15:51:20 +0100 Subject: [PATCH 01/10] feat: Add resource_vcd_nsxt_firewall_rule resource --- scripts/install-plugin.sh | 2 +- vcd/provider.go | 1 + vcd/resource_vcd_nsxt_firewall_rule.go | 391 ++++++++++++++++++++ vcd/resource_vcd_nsxt_firewall_rule_test.go | 241 ++++++++++++ 4 files changed, 634 insertions(+), 1 deletion(-) create mode 100644 vcd/resource_vcd_nsxt_firewall_rule.go create mode 100644 vcd/resource_vcd_nsxt_firewall_rule_test.go 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..ecf5755ad 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 + "resource_vcd_nsxt_firewall_rule": resourceVcdNsxtFirewallRule(), // 3.14 } // 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..b64677923 --- /dev/null +++ b/vcd/resource_vcd_nsxt_firewall_rule.go @@ -0,0 +1,391 @@ +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) + } + + 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) + } + 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) + } + + 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) + } + + d.Set("org", orgName) + d.Set("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'") + } + + d.Set("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) { + d.Set("name", rule.Name) + d.Set("action", rule.ActionValue) + d.Set("enabled", rule.Active) + d.Set("logging", rule.Logging) + d.Set("direction", rule.Direction) + d.Set("ip_protocol", rule.IpProtocol) + + d.Set("source_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.SourceFirewallGroups))) + d.Set("destination_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.DestinationFirewallGroups))) + d.Set("app_port_profile_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.ApplicationPortProfiles))) +} 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..9f1681bd1 --- /dev/null +++ b/vcd/resource_vcd_nsxt_firewall_rule_test.go @@ -0,0 +1,241 @@ +//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, + PreCheck: func() { testAccPreCheck(t) }, + 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, + PreCheck: func() { testAccPreCheck(t) }, + 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) +} From bfe9400a667ea317b383ad229f11eeecbad89ec7 Mon Sep 17 00:00:00 2001 From: Bart van der Schans Date: Thu, 5 Feb 2026 09:57:10 +0100 Subject: [PATCH 02/10] Update readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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. From 445a2082326e9d9d9986f1ef273f90db402716af Mon Sep 17 00:00:00 2001 From: Bart van der Schans Date: Thu, 5 Feb 2026 10:43:22 +0100 Subject: [PATCH 03/10] Fix resource name --- vcd/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcd/provider.go b/vcd/provider.go index ecf5755ad..226f27abb 100644 --- a/vcd/provider.go +++ b/vcd/provider.go @@ -301,7 +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 - "resource_vcd_nsxt_firewall_rule": resourceVcdNsxtFirewallRule(), // 3.14 + "vcd_nsxt_firewall_rule": resourceVcdNsxtFirewallRule(), // 3.15 } // Provider returns a terraform.ResourceProvider. From 6406db6817861d685f851890a5fa77a30a0078ac Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Thu, 5 Feb 2026 11:27:26 +0100 Subject: [PATCH 04/10] Bump version to 3.15.0 --- PREVIOUS_VERSION | 2 +- VERSION | 2 +- vcd/resource_vcd_nsxt_firewall_rule_test.go | 2 -- website/docs/index.html.markdown | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/PREVIOUS_VERSION b/PREVIOUS_VERSION index 2d35fc0bf..62b6d193d 100644 --- a/PREVIOUS_VERSION +++ b/PREVIOUS_VERSION @@ -1 +1 @@ -v3.14.1 +v3.14.2 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/vcd/resource_vcd_nsxt_firewall_rule_test.go b/vcd/resource_vcd_nsxt_firewall_rule_test.go index 9f1681bd1..c3c61c662 100644 --- a/vcd/resource_vcd_nsxt_firewall_rule_test.go +++ b/vcd/resource_vcd_nsxt_firewall_rule_test.go @@ -18,7 +18,6 @@ func TestAccVcdNsxtFirewallRule_Basic(t *testing.T) { resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviders, - PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckVcdNsxtFirewallRuleDestroy, Steps: []resource.TestStep{ { @@ -60,7 +59,6 @@ func TestAccVcdNsxtFirewallRule_Complete(t *testing.T) { resource.Test(t, resource.TestCase{ ProviderFactories: testAccProviders, - PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: testAccCheckVcdNsxtFirewallRuleDestroy, Steps: []resource.TestStep{ { 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. From 3337ba4b3c33bc0a37aed76e454ae5a5156b1468 Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Thu, 5 Feb 2026 11:56:52 +0100 Subject: [PATCH 05/10] Add pr ci and rename to schubergphilis --- .goreleaser.yml | 2 +- GNUmakefile | 5 ++--- go.mod | 2 +- main.go | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) 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..0fe8c303d 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 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() { From bed49e1cb6635f5a5003babad5b59afc775087b1 Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Thu, 5 Feb 2026 12:45:03 +0100 Subject: [PATCH 06/10] Remove runtest short --- GNUmakefile | 1 - 1 file changed, 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index 0fe8c303d..2c53eb22b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -78,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 From 1099285d012ef6b296015a968bbe78ec6c77ed76 Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Thu, 5 Feb 2026 13:17:30 +0100 Subject: [PATCH 07/10] Fix gosec issues --- vcd/resource_vcd_nsxt_firewall_rule.go | 32 +++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/vcd/resource_vcd_nsxt_firewall_rule.go b/vcd/resource_vcd_nsxt_firewall_rule.go index b64677923..5fea717bc 100644 --- a/vcd/resource_vcd_nsxt_firewall_rule.go +++ b/vcd/resource_vcd_nsxt_firewall_rule.go @@ -331,8 +331,8 @@ func resourceVcdNsxtFirewallRuleImport(ctx context.Context, d *schema.ResourceDa return nil, fmt.Errorf("could not find firewall rule with name '%s' in edge gateway '%s'", ruleName, edgeName) } - d.Set("org", orgName) - d.Set("edge_gateway_id", edge.EdgeGateway.ID) + dSet(d, "org", orgName) + dSet(d, "edge_gateway_id", edge.EdgeGateway.ID) d.SetId(foundRule.ID) return []*schema.ResourceData{d}, nil @@ -342,7 +342,7 @@ func resourceVcdNsxtFirewallRuleImport(ctx context.Context, d *schema.ResourceDa return nil, fmt.Errorf("import ID must be in format 'edge_gateway_id.rule_id' or 'org.edge_name.rule_name'") } - d.Set("edge_gateway_id", parts[0]) + dSet(d, "edge_gateway_id", parts[0]) d.SetId(parts[1]) return []*schema.ResourceData{d}, nil @@ -378,14 +378,20 @@ func getNsxtFirewallRuleFromSchema(d *schema.ResourceData) *NsxtFirewallRuleV2 { } func setNsxtFirewallRuleToSchema(d *schema.ResourceData, rule *NsxtFirewallRuleV2) { - d.Set("name", rule.Name) - d.Set("action", rule.ActionValue) - d.Set("enabled", rule.Active) - d.Set("logging", rule.Logging) - d.Set("direction", rule.Direction) - d.Set("ip_protocol", rule.IpProtocol) - - d.Set("source_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.SourceFirewallGroups))) - d.Set("destination_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.DestinationFirewallGroups))) - d.Set("app_port_profile_ids", convertStringsToTypeSet(extractIdsFromOpenApiReferences(rule.ApplicationPortProfiles))) + 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) + } } From b668811e4bafc714da9e526c4ce744f4622dadf7 Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Thu, 5 Feb 2026 13:24:49 +0100 Subject: [PATCH 08/10] Ad install tf step for hclcheck --- .github/workflows/check-docs.yml | 3 +++ 1 file changed, 3 insertions(+) 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 From 10eb232ebffa0c6273fe3b52794b93a39a8b1af8 Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Thu, 5 Feb 2026 14:01:23 +0100 Subject: [PATCH 09/10] Add edge gateway locking to support 'concurrent' creations --- vcd/resource_vcd_nsxt_firewall_rule.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vcd/resource_vcd_nsxt_firewall_rule.go b/vcd/resource_vcd_nsxt_firewall_rule.go index 5fea717bc..fdf1d9f79 100644 --- a/vcd/resource_vcd_nsxt_firewall_rule.go +++ b/vcd/resource_vcd_nsxt_firewall_rule.go @@ -159,6 +159,13 @@ func resourceVcdNsxtFirewallRuleCreate(ctx context.Context, d *schema.ResourceDa 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)) @@ -228,6 +235,14 @@ func resourceVcdNsxtFirewallRuleUpdate(ctx context.Context, d *schema.ResourceDa 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 @@ -269,6 +284,13 @@ func resourceVcdNsxtFirewallRuleDelete(_ context.Context, d *schema.ResourceData 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 { From ee0787d06bff748174fc3d3d77288e4eb003fecf Mon Sep 17 00:00:00 2001 From: Tom Snuverink Date: Fri, 6 Feb 2026 07:22:03 +0100 Subject: [PATCH 10/10] Correct previous version --- PREVIOUS_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PREVIOUS_VERSION b/PREVIOUS_VERSION index 62b6d193d..2d35fc0bf 100644 --- a/PREVIOUS_VERSION +++ b/PREVIOUS_VERSION @@ -1 +1 @@ -v3.14.2 +v3.14.1