Skip to content

Commit f6ce32e

Browse files
committed
feat: added custom resource to apply sdn configurations
Signed-off-by: MacherelR <[email protected]>
1 parent 926ea8d commit f6ce32e

File tree

10 files changed

+303
-1
lines changed

10 files changed

+303
-1
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
resource "proxmox_virtual_environment_sdn_zone_simple" "test_zone_1" {
2+
id = "tZone1"
3+
nodes= [data.proxmox_virtual_environment_nodes.example.names]
4+
mtu = 1496
5+
}
6+
7+
resource "proxmox_virtual_environment_sdn_zone_simple" "test_zone_2" {
8+
id = "tZone2"
9+
nodes= [data.proxmox_virtual_environment_nodes.example.names]
10+
mtu = 1496
11+
}
12+
13+
resource "proxmox_virtual_environment_sdn_applier" "apply" {
14+
lifecycle {
15+
replace_triggered_by = [
16+
proxmox_virtual_environment_sdn_zone_simple.test_zone_1,
17+
proxmox_virtual_environment_sdn_zone_simple.test_zone_2,
18+
]
19+
}
20+
21+
depends_on = [
22+
proxmox_virtual_environment_sdn_zone_simple.test_zone_1,
23+
proxmox_virtual_environment_sdn_zone_simple.test_zone_2,
24+
]
25+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package applier
8+
9+
import (
10+
"context"
11+
"fmt"
12+
"time"
13+
14+
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
15+
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/applier"
16+
"github.com/hashicorp/terraform-plugin-framework/resource"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
18+
"github.com/hashicorp/terraform-plugin-framework/types"
19+
)
20+
21+
var (
22+
_ resource.Resource = &Resource{}
23+
_ resource.ResourceWithConfigure = &Resource{}
24+
_ resource.ResourceWithImportState = &Resource{}
25+
)
26+
27+
type model struct {
28+
// Opaque ID set timestamp at creation time.
29+
ID types.String `tfsdk:"id"`
30+
}
31+
32+
type Resource struct {
33+
client *applier.Client
34+
}
35+
36+
func NewResource() resource.Resource {
37+
return &Resource{}
38+
}
39+
40+
func (r *Resource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
41+
resp.TypeName = req.ProviderTypeName + "_sdn_applier"
42+
}
43+
44+
func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
45+
resp.Schema = schema.Schema{
46+
Description: "Applies pending Proxmox SDN configuration (cluster-wide).",
47+
MarkdownDescription: "Triggers Proxmox's SDN **Apply** (equivalent to `PUT /cluster/sdn`)." +
48+
"Intended to be used with `replace_triggered_by` so it runs after SDN objects change.",
49+
Attributes: map[string]schema.Attribute{
50+
"id": schema.StringAttribute{
51+
Computed: true,
52+
Description: "Opaque identifier set to the RFC3339 timestamp when the apply was executed.",
53+
},
54+
},
55+
}
56+
}
57+
58+
func (r *Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
59+
if req.ProviderData == nil {
60+
return
61+
}
62+
63+
cfg, ok := req.ProviderData.(config.Resource)
64+
if !ok {
65+
resp.Diagnostics.AddError(
66+
"Unexpected Resource Configure Type",
67+
fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData),
68+
)
69+
70+
return
71+
}
72+
73+
r.client = cfg.Client.Cluster().SDNApplier()
74+
}
75+
76+
func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
77+
if err := r.client.ApplyConfig(ctx); err != nil {
78+
resp.Diagnostics.AddError("Unable to Apply SDN Configuration", err.Error())
79+
return
80+
}
81+
82+
state := &model{
83+
ID: types.StringValue(time.Now().UTC().Format(time.RFC3339)),
84+
}
85+
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
86+
}
87+
88+
func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
89+
// Nothing to refresh; keep prior state to be stable in plans.
90+
var state model
91+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
92+
93+
if resp.Diagnostics.HasError() {
94+
return
95+
}
96+
97+
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
98+
}
99+
100+
func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
101+
// We expect replacements only. But if someone does in-place Update,
102+
// we just re-run apply for safety and bump the ID timestamp.
103+
if err := r.client.ApplyConfig(ctx); err != nil {
104+
resp.Diagnostics.AddError("Unable to Re-Apply SDN Configuration", err.Error())
105+
return
106+
}
107+
108+
var plan model
109+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
110+
111+
if resp.Diagnostics.HasError() {
112+
return
113+
}
114+
115+
plan.ID = types.StringValue(time.Now().UTC().Format(time.RFC3339))
116+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
117+
}
118+
119+
func (r *Resource) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) {
120+
// No remote object to delete; nothing to do.
121+
}
122+
123+
func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
124+
if req.ID == "" {
125+
resp.Diagnostics.AddError("Invalid Import ID", "Expected a non-empty ID value.")
126+
return
127+
}
128+
129+
state := &model{ID: types.StringValue(req.ID)}
130+
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
131+
}

fwprovider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/hardwaremapping"
3131
"github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/metrics"
3232
"github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/options"
33+
sdnapplier "github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/sdn/applier"
3334
sdnzone "github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/sdn/zone"
3435
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
3536
"github.com/bpg/terraform-provider-proxmox/fwprovider/nodes"
@@ -533,6 +534,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
533534
sdnzone.NewQinQResource,
534535
sdnzone.NewVXLANResource,
535536
sdnzone.NewEVPNResource,
537+
sdnapplier.NewResource,
536538
}
537539
}
538540

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/bpg/terraform-provider-proxmox
22

3-
go 1.24
3+
go 1.24.0
44

55
tool github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
66

proxmox/cluster/client.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha"
1616
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
1717
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/metrics"
18+
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/applier"
1819
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
1920
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
2021
)
@@ -70,3 +71,8 @@ func (c *Client) SDNZones() *zones.Client {
7071
// func (c *Client) SDNSubnets() *subnets.Client {
7172
// return &subnets.Client{Client: c}
7273
// }
74+
75+
// SDNApplier returns a client for applying the SDN's configuration.
76+
func (c *Client) SDNApplier() *applier.Client {
77+
return &applier.Client{Client: c}
78+
}

proxmox/cluster/sdn/applier/api.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package applier
8+
9+
import "context"
10+
11+
type API interface {
12+
ApplyConfig(ctx context.Context) error
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package applier
8+
9+
import (
10+
"context"
11+
"fmt"
12+
"net/http"
13+
)
14+
15+
// ApplyConfig triggers a cluster-wide SDN apply via PUT /cluster/sdn.
16+
// If the API returns a task UPID, it will be captured but ignored here.
17+
func (c *Client) ApplyConfig(ctx context.Context) error {
18+
resBody := &ApplyResponseBody{}
19+
20+
if err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(""), nil, resBody); err != nil {
21+
return fmt.Errorf("error applying SDN configuration: %w", err)
22+
}
23+
24+
if resBody.Data == nil || *resBody.Data == "" {
25+
return fmt.Errorf("SDN apply did not return a task UPID")
26+
}
27+
28+
return nil
29+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package applier
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"testing"
8+
"time"
9+
10+
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
11+
)
12+
13+
func getTestClient() (*Client, error) {
14+
apiToken := os.Getenv("PVE_TOKEN")
15+
if apiToken == "" {
16+
return nil, fmt.Errorf("PVE_TOKEN env variable not set")
17+
}
18+
19+
conURL := os.Getenv("PVE_URL")
20+
if conURL == "" {
21+
return nil, fmt.Errorf("PVE_URL env variable not set")
22+
}
23+
24+
conn, err := api.NewConnection(conURL, true, "")
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
creds := api.Credentials{
30+
TokenCredentials: &api.TokenCredentials{
31+
APIToken: apiToken,
32+
},
33+
}
34+
35+
client, err := api.NewClient(creds, conn)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
return &Client{Client: client}, nil
41+
}
42+
43+
func TestApplyConfig(t *testing.T) {
44+
t.Parallel()
45+
46+
client, err := getTestClient()
47+
if err != nil {
48+
t.Fatalf("failed to create test client: %v", err)
49+
}
50+
51+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
52+
defer cancel()
53+
54+
if err := client.ApplyConfig(ctx); err != nil {
55+
t.Fatalf("ApplyConfig failed: %v", err)
56+
}
57+
58+
t.Logf("SDN configuration applied successfully")
59+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package applier
8+
9+
// ApplyResponseBody represents the response of PUT /cluster/sdn.
10+
// PVE typically returns a UPID string in `data` for async tasks.
11+
// We keep it here even if ApplyConfig currently ignores it, so the
12+
// API can easily be extended later to surface it.
13+
type ApplyResponseBody struct {
14+
Data *string `json:"data"`
15+
}

proxmox/cluster/sdn/applier/client.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package applier
8+
9+
import (
10+
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
11+
)
12+
13+
// Client is a client for accessing the Proxmox SDN Apply API.
14+
type Client struct {
15+
api.Client
16+
}
17+
18+
// ExpandPath returns the API path for cluster-wide SDN apply.
19+
func (c *Client) ExpandPath(path string) string {
20+
_ = path
21+
return "cluster/sdn"
22+
}

0 commit comments

Comments
 (0)