Skip to content

Commit 8087ee2

Browse files
authored
Merge pull request #25 from platform9/fix/live-validation
fix(lb,floatingip): live-validation fixes from the CE lab
2 parents 95bf7dc + 15aa49e commit 8087ee2

11 files changed

Lines changed: 132 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ All notable changes to this project are documented here. The format is based on
6161
changes per load balancer). PCD ships the **OVN** provider only, which is L4
6262
(TCP/UDP/SCTP); use L4 listener protocols and OVN-supported pool algorithms. L7
6363
policy/rule resources are omitted because the OVN provider does not support L7.
64+
`pcd_lb_loadbalancer` exposes `loadbalancer_provider` (defaults to `ovn`) — required
65+
because Octavia's server-side default provider is `amphora`, which PCD does not enable.
6466
- DNS (Designate v2) — Phase 3: `pcd_dns_zone` and `pcd_dns_recordset` resources plus a
6567
`pcd_dns_zone` data source. Zone and recordset create/update/delete are asynchronous,
6668
so applies wait for the object to reach `ACTIVE` (and to disappear after delete).

examples/resources/pcd_lb_listener/resource.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ resource "pcd_lb_loadbalancer" "example" {
1515
resource "pcd_lb_listener" "example" {
1616
name = "tf-example-listener"
1717
loadbalancer_id = pcd_lb_loadbalancer.example.id
18-
protocol = "HTTP"
18+
protocol = "TCP" # OVN provider is L4: TCP/UDP/SCTP
1919
protocol_port = 80
2020
}

examples/resources/pcd_lb_loadbalancer/resource.tf

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ resource "pcd_networking_subnet" "example" {
88
}
99

1010
resource "pcd_lb_loadbalancer" "example" {
11-
name = "tf-example-lb"
12-
vip_subnet_id = pcd_networking_subnet.example.id
11+
name = "tf-example-lb"
12+
vip_subnet_id = pcd_networking_subnet.example.id
13+
loadbalancer_provider = "ovn" # PCD ships only the OVN (L4) provider; this is the default
1314
}

examples/resources/pcd_lb_monitor/resource.tf

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@ resource "pcd_lb_loadbalancer" "example" {
1515
resource "pcd_lb_pool" "example" {
1616
name = "tf-example-pool"
1717
loadbalancer_id = pcd_lb_loadbalancer.example.id
18-
protocol = "HTTP"
19-
lb_method = "ROUND_ROBIN"
18+
protocol = "TCP" # OVN provider is L4
19+
lb_method = "SOURCE_IP_PORT"
2020
}
2121

2222
resource "pcd_lb_monitor" "example" {
2323
pool_id = pcd_lb_pool.example.id
24-
type = "HTTP"
24+
type = "TCP"
2525
delay = 10
2626
timeout = 5
2727
max_retries = 3
28-
url_path = "/healthz"
2928
}

examples/resources/pcd_lb_pool/resource.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ resource "pcd_lb_loadbalancer" "example" {
1414

1515
resource "pcd_lb_listener" "example" {
1616
loadbalancer_id = pcd_lb_loadbalancer.example.id
17-
protocol = "HTTP"
17+
protocol = "TCP"
1818
protocol_port = 80
1919
}
2020

2121
resource "pcd_lb_pool" "example" {
2222
name = "tf-example-pool"
2323
listener_id = pcd_lb_listener.example.id
24-
protocol = "HTTP"
25-
lb_method = "ROUND_ROBIN"
24+
protocol = "TCP" # OVN provider is L4
25+
lb_method = "SOURCE_IP_PORT"
2626
}

internal/services/loadbalancer/loadbalancer_resource.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
2222
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
2323
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
24+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
2425
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
2526
"github.com/hashicorp/terraform-plugin-framework/types"
2627

@@ -52,6 +53,7 @@ type loadBalancerModel struct {
5253
VipAddress types.String `tfsdk:"vip_address"`
5354
VipPortID types.String `tfsdk:"vip_port_id"`
5455
FlavorID types.String `tfsdk:"flavor_id"`
56+
Provider types.String `tfsdk:"loadbalancer_provider"`
5557
Tags types.Set `tfsdk:"tags"`
5658
ProvisioningStatus types.String `tfsdk:"provisioning_status"`
5759
OperatingStatus types.String `tfsdk:"operating_status"`
@@ -72,19 +74,20 @@ func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaReques
7274
"Layer 4 (TCP/UDP/SCTP): use L4 listener protocols and OVN-supported pool algorithms; HTTP/L7 features " +
7375
"are not available.",
7476
Attributes: map[string]schema.Attribute{
75-
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "The load balancer ID.", PlanModifiers: useState},
76-
"name": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The name of the load balancer.", PlanModifiers: useState},
77-
"description": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "A description of the load balancer.", PlanModifiers: useState},
78-
"admin_state_up": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "The administrative state of the load balancer."},
79-
"vip_subnet_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Subnet on which to allocate the VIP (mutually exclusive with vip_network_id). Changing this forces a new resource.", PlanModifiers: forceNew},
80-
"vip_network_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Network on which to allocate the VIP (mutually exclusive with vip_subnet_id). Changing this forces a new resource.", PlanModifiers: forceNew},
81-
"vip_address": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The VIP address. Requesting a specific address changing it forces a new resource.", PlanModifiers: forceNew},
82-
"vip_port_id": schema.StringAttribute{Computed: true, MarkdownDescription: "The ID of the VIP port.", PlanModifiers: useState},
83-
"flavor_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The Octavia flavor to use. Changing this forces a new resource.", PlanModifiers: forceNew},
84-
"tags": schema.SetAttribute{Optional: true, Computed: true, ElementType: types.StringType, MarkdownDescription: "Tags applied to the load balancer.", PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}},
85-
"provisioning_status": schema.StringAttribute{Computed: true, MarkdownDescription: "The provisioning status (e.g. ACTIVE).", PlanModifiers: useState},
86-
"operating_status": schema.StringAttribute{Computed: true, MarkdownDescription: "The operating status (e.g. ONLINE).", PlanModifiers: useState},
87-
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region.", PlanModifiers: useState},
77+
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "The load balancer ID.", PlanModifiers: useState},
78+
"name": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The name of the load balancer.", PlanModifiers: useState},
79+
"description": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "A description of the load balancer.", PlanModifiers: useState},
80+
"admin_state_up": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true), MarkdownDescription: "The administrative state of the load balancer."},
81+
"vip_subnet_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Subnet on which to allocate the VIP (mutually exclusive with vip_network_id). Changing this forces a new resource.", PlanModifiers: forceNew},
82+
"vip_network_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Network on which to allocate the VIP (mutually exclusive with vip_subnet_id). Changing this forces a new resource.", PlanModifiers: forceNew},
83+
"vip_address": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The VIP address. Requesting a specific address changing it forces a new resource.", PlanModifiers: forceNew},
84+
"vip_port_id": schema.StringAttribute{Computed: true, MarkdownDescription: "The ID of the VIP port.", PlanModifiers: useState},
85+
"flavor_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The Octavia flavor to use. Changing this forces a new resource.", PlanModifiers: forceNew},
86+
"loadbalancer_provider": schema.StringAttribute{Optional: true, Computed: true, Default: stringdefault.StaticString("ovn"), MarkdownDescription: "The Octavia provider driver. PCD ships only `ovn`, which is the default; changing this forces a new resource.", PlanModifiers: forceNew},
87+
"tags": schema.SetAttribute{Optional: true, Computed: true, ElementType: types.StringType, MarkdownDescription: "Tags applied to the load balancer.", PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}},
88+
"provisioning_status": schema.StringAttribute{Computed: true, MarkdownDescription: "The provisioning status (e.g. ACTIVE).", PlanModifiers: useState},
89+
"operating_status": schema.StringAttribute{Computed: true, MarkdownDescription: "The operating status (e.g. ONLINE).", PlanModifiers: useState},
90+
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region.", PlanModifiers: useState},
8891
},
8992
}
9093
}
@@ -121,6 +124,7 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe
121124
VipNetworkID: plan.VipNetworkID.ValueString(),
122125
VipAddress: plan.VipAddress.ValueString(),
123126
FlavorID: plan.FlavorID.ValueString(),
127+
Provider: plan.Provider.ValueString(),
124128
AdminStateUp: &adminUp,
125129
}
126130
if !plan.Tags.IsNull() && !plan.Tags.IsUnknown() {
@@ -286,6 +290,7 @@ func (r *loadBalancerResource) readInto(ctx context.Context, client *gophercloud
286290
m.VipAddress = types.StringValue(lb.VipAddress)
287291
m.VipPortID = types.StringValue(lb.VipPortID)
288292
m.FlavorID = types.StringValue(lb.FlavorID)
293+
m.Provider = types.StringValue(lb.Provider)
289294
m.ProvisioningStatus = types.StringValue(lb.ProvisioningStatus)
290295
m.OperatingStatus = types.StringValue(lb.OperatingStatus)
291296

internal/services/loadbalancer/loadbalancer_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ func TestAccLBLoadBalancer_tree(t *testing.T) {
3838
resource.TestCheckResourceAttr(lbName, "provisioning_status", "ACTIVE"),
3939
resource.TestCheckResourceAttrSet(lbName, "vip_address"),
4040
resource.TestCheckResourceAttrSet(lbName, "vip_port_id"),
41+
resource.TestCheckResourceAttr(lbName, "loadbalancer_provider", "ovn"),
4142
resource.TestCheckResourceAttrPair("pcd_lb_listener.test", "loadbalancer_id", lbName, "id"),
42-
resource.TestCheckResourceAttr("pcd_lb_pool.test", "lb_method", "ROUND_ROBIN"),
43+
resource.TestCheckResourceAttr("pcd_lb_pool.test", "lb_method", "SOURCE_IP_PORT"),
4344
resource.TestCheckResourceAttr("pcd_lb_member.test", "protocol_port", "8080"),
44-
resource.TestCheckResourceAttr("pcd_lb_monitor.test", "type", "HTTP"),
45+
resource.TestCheckResourceAttr("pcd_lb_monitor.test", "type", "TCP"),
4546
resource.TestCheckResourceAttrPair("data.pcd_lb_loadbalancer.by_name", "id", lbName, "id"),
4647
),
4748
},
@@ -67,22 +68,23 @@ resource "pcd_networking_subnet" "test" {
6768
}
6869
6970
resource "pcd_lb_loadbalancer" "test" {
70-
name = %q
71-
vip_subnet_id = pcd_networking_subnet.test.id
71+
name = %q
72+
vip_subnet_id = pcd_networking_subnet.test.id
73+
loadbalancer_provider = "ovn"
7274
}
7375
7476
resource "pcd_lb_listener" "test" {
7577
name = "tf-acc-lb-listener"
7678
loadbalancer_id = pcd_lb_loadbalancer.test.id
77-
protocol = "HTTP"
79+
protocol = "TCP"
7880
protocol_port = 80
7981
}
8082
8183
resource "pcd_lb_pool" "test" {
8284
name = "tf-acc-lb-pool"
8385
listener_id = pcd_lb_listener.test.id
84-
protocol = "HTTP"
85-
lb_method = "ROUND_ROBIN"
86+
protocol = "TCP"
87+
lb_method = "SOURCE_IP_PORT"
8688
}
8789
8890
resource "pcd_lb_member" "test" {
@@ -94,11 +96,10 @@ resource "pcd_lb_member" "test" {
9496
9597
resource "pcd_lb_monitor" "test" {
9698
pool_id = pcd_lb_pool.test.id
97-
type = "HTTP"
99+
type = "TCP"
98100
delay = 10
99101
timeout = 5
100102
max_retries = 3
101-
url_path = "/healthz"
102103
}
103104
104105
data "pcd_lb_loadbalancer" "by_name" {

internal/services/networking/floatingip_associate_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ func TestAccNetworkingFloatingIPAssociate_basic(t *testing.T) {
4747

4848
func testAccFloatingIPAssociateConfig(pool string) string {
4949
return fmt.Sprintf(`
50+
data "pcd_networking_network" "ext" {
51+
name = %[1]q
52+
}
53+
5054
resource "pcd_networking_network" "test" {
5155
name = "tf-acc-fipa-net"
5256
}
@@ -57,6 +61,18 @@ resource "pcd_networking_subnet" "test" {
5761
cidr = "10.107.0.0/24"
5862
}
5963
64+
# A floating IP can only be bound to a port whose subnet reaches the external
65+
# network through a router (external gateway + interface on the subnet).
66+
resource "pcd_networking_router" "test" {
67+
name = "tf-acc-fipa-router"
68+
external_network_id = data.pcd_networking_network.ext.id
69+
}
70+
71+
resource "pcd_networking_router_interface" "test" {
72+
router_id = pcd_networking_router.test.id
73+
subnet_id = pcd_networking_subnet.test.id
74+
}
75+
6076
resource "pcd_networking_port" "test" {
6177
name = "tf-acc-fipa-port"
6278
network_id = pcd_networking_network.test.id
@@ -67,12 +83,13 @@ resource "pcd_networking_port" "test" {
6783
}
6884
6985
resource "pcd_networking_floatingip" "test" {
70-
pool = %q
86+
pool = %[1]q
7187
}
7288
7389
resource "pcd_networking_floatingip_associate" "test" {
7490
floating_ip_id = pcd_networking_floatingip.test.id
7591
port_id = pcd_networking_port.test.id
92+
depends_on = [pcd_networking_router_interface.test]
7693
}
7794
`, pool)
7895
}

internal/services/networking/floatingip_resource.go

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var (
3232
_ resource.Resource = (*floatingIPResource)(nil)
3333
_ resource.ResourceWithConfigure = (*floatingIPResource)(nil)
3434
_ resource.ResourceWithImportState = (*floatingIPResource)(nil)
35+
_ resource.ResourceWithModifyPlan = (*floatingIPResource)(nil)
3536
)
3637

3738
// NewFloatingIPResource is the factory registered with the provider.
@@ -74,13 +75,18 @@ func (r *floatingIPResource) Schema(_ context.Context, _ resource.SchemaRequest,
7475
"floating_network_id": schema.StringAttribute{Computed: true, MarkdownDescription: "The ID of the external network `pool` resolved to.", PlanModifiers: useState},
7576
"description": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "A description of the floating IP.", PlanModifiers: useState},
7677
"address": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The floating IP address. Request a specific address by setting this; changing it forces a new resource.", PlanModifiers: forceNewStr},
77-
"port_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The port to associate the floating IP with. Set to associate, remove to disassociate.", PlanModifiers: useState},
78-
"fixed_ip": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The specific fixed IP on the associated port to map to. Defaults to the port's first address.", PlanModifiers: useState},
79-
"tenant_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The owning project. Changing this forces a new resource.", PlanModifiers: forceNewStr},
80-
"status": schema.StringAttribute{Computed: true, MarkdownDescription: "The operational status of the floating IP."},
81-
"router_id": schema.StringAttribute{Computed: true, MarkdownDescription: "The router through which the floating IP is routed."},
82-
"tags": schema.SetAttribute{Optional: true, Computed: true, ElementType: types.StringType, MarkdownDescription: "Tags applied to the floating IP.", PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}},
83-
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region.", PlanModifiers: useState},
78+
// Set port_id to a port to associate, or to "" to disassociate. Leave
79+
// it unset to let a separate pcd_networking_floatingip_associate
80+
// resource manage the association (UseStateForUnknown keeps the
81+
// server-managed value stable in that case). ModifyPlan marks the
82+
// server-derived fields unknown when the association actually changes.
83+
"port_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The port to associate the floating IP with. Set to a port ID to associate, or to an empty string to disassociate. Leave unset to manage the association with a separate `pcd_networking_floatingip_associate` resource.", PlanModifiers: useState},
84+
"fixed_ip": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The specific fixed IP on the associated port to map to. Defaults to the port's first address.", PlanModifiers: useState},
85+
"tenant_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The owning project. Changing this forces a new resource.", PlanModifiers: forceNewStr},
86+
"status": schema.StringAttribute{Computed: true, MarkdownDescription: "The operational status of the floating IP."},
87+
"router_id": schema.StringAttribute{Computed: true, MarkdownDescription: "The router through which the floating IP is routed."},
88+
"tags": schema.SetAttribute{Optional: true, Computed: true, ElementType: types.StringType, MarkdownDescription: "Tags applied to the floating IP.", PlanModifiers: []planmodifier.Set{setplanmodifier.UseStateForUnknown()}},
89+
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region.", PlanModifiers: useState},
8490
},
8591
}
8692
}
@@ -167,6 +173,40 @@ func (r *floatingIPResource) Read(ctx context.Context, req resource.ReadRequest,
167173
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
168174
}
169175

176+
// ModifyPlan keeps the server-derived fields consistent when the association
177+
// changes. When the user manages port_id inline (sets it to a port or to "") and
178+
// that value differs from state, the server recomputes fixed_ip, router_id, and
179+
// status — so they must be planned unknown, or Terraform reports an inconsistent
180+
// result. When port_id is left unset (config null), the association is managed
181+
// elsewhere (e.g. pcd_networking_floatingip_associate); UseStateForUnknown keeps
182+
// the values stable and we make no changes.
183+
func (r *floatingIPResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
184+
if req.Plan.Raw.IsNull() {
185+
return // destroy
186+
}
187+
var config floatingIPModel
188+
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
189+
if resp.Diagnostics.HasError() || config.PortID.IsNull() {
190+
return
191+
}
192+
193+
changing := true
194+
if !req.State.Raw.IsNull() {
195+
var state floatingIPModel
196+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
197+
changing = state.PortID.ValueString() != config.PortID.ValueString()
198+
}
199+
if !changing {
200+
return
201+
}
202+
203+
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("router_id"), types.StringUnknown())...)
204+
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("status"), types.StringUnknown())...)
205+
if config.FixedIP.IsNull() {
206+
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("fixed_ip"), types.StringUnknown())...)
207+
}
208+
}
209+
170210
func (r *floatingIPResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
171211
var plan, state floatingIPModel
172212
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

0 commit comments

Comments
 (0)