Skip to content

Commit b6028b9

Browse files
committed
feat(compute): add flavor, servergroup, and flavor/keypair/az data sources
Nova v2. All acceptance-tested against the CE lab (boot-independent). - pcd_compute_flavor: create/read/import (immutable), + pcd_compute_flavor data source. - pcd_compute_servergroup: affinity/anti-affinity group, create/read/import. - pcd_compute_keypair data source and pcd_compute_availability_zones data source. pcd_compute_instance remains code-complete but its boot acceptance test is blocked on a CE image-library issue (images created via the API don't populate the onboarded host's local library -> nova gets HTTP 204 for image data). Tracked in DECISIONS.md.
1 parent 736c557 commit b6028b9

10 files changed

Lines changed: 979 additions & 0 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ All notable changes to this project are documented here. The format is based on
3131
- Networking (Neutron v2): resources `pcd_networking_network`, `_subnet`, `_secgroup`,
3232
`_secgroup_rule`, `_router`, `_router_interface`; data sources `pcd_networking_network`,
3333
`_subnet`, `_secgroup`. (Floating IPs, ports, and remaining data sources to follow.)
34+
- Compute (Nova v2): resources `pcd_compute_keypair`, `pcd_compute_flavor`,
35+
`pcd_compute_servergroup` (acceptance-tested); `pcd_compute_instance` (code-complete —
36+
boot verification is blocked on a lab image-library issue, see DECISIONS.md); data
37+
sources `pcd_compute_flavor`, `pcd_compute_keypair`, `pcd_compute_availability_zones`.
3438

3539
### Known gaps
3640
- `cloud` (clouds.yaml) is declared but not yet implemented; it errors if set.

internal/provider/provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ func (p *pcdProvider) Resources(_ context.Context) []func() resource.Resource {
5454
networking.NewRouterInterfaceResource,
5555
compute.NewKeypairResource,
5656
compute.NewInstanceResource,
57+
compute.NewFlavorResource,
58+
compute.NewServergroupResource,
5759
}
5860
}
5961

@@ -68,5 +70,8 @@ func (p *pcdProvider) DataSources(_ context.Context) []func() datasource.DataSou
6870
networking.NewNetworkDataSource,
6971
networking.NewSubnetDataSource,
7072
networking.NewSecgroupDataSource,
73+
compute.NewFlavorDataSource,
74+
compute.NewKeypairDataSource,
75+
compute.NewAvailabilityZonesDataSource,
7176
}
7277
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
//
4+
// Ported from terraform-provider-openstack v3.4.0
5+
// (openstack/data_source_openstack_compute_availability_zones_v2.go), adapted
6+
// for the terraform-plugin-framework and PCD.
7+
8+
package compute
9+
10+
import (
11+
"context"
12+
13+
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones"
14+
"github.com/hashicorp/terraform-plugin-framework/datasource"
15+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
16+
"github.com/hashicorp/terraform-plugin-framework/types"
17+
18+
"github.com/platform9/terraform-provider-pcd/internal/clients"
19+
)
20+
21+
var (
22+
_ datasource.DataSource = (*azDataSource)(nil)
23+
_ datasource.DataSourceWithConfigure = (*azDataSource)(nil)
24+
)
25+
26+
// NewAvailabilityZonesDataSource is the factory registered with the provider.
27+
func NewAvailabilityZonesDataSource() datasource.DataSource {
28+
return &azDataSource{}
29+
}
30+
31+
type azDataSource struct {
32+
config *clients.Config
33+
}
34+
35+
type azDataSourceModel struct {
36+
ID types.String `tfsdk:"id"`
37+
State types.String `tfsdk:"state"`
38+
Names types.List `tfsdk:"names"`
39+
Region types.String `tfsdk:"region"`
40+
}
41+
42+
func (d *azDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
43+
resp.TypeName = req.ProviderTypeName + "_compute_availability_zones"
44+
}
45+
46+
func (d *azDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
47+
resp.Schema = schema.Schema{
48+
MarkdownDescription: "Return the compute availability zones.",
49+
Attributes: map[string]schema.Attribute{
50+
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "Data source identifier."},
51+
"state": schema.StringAttribute{Optional: true, MarkdownDescription: "Filter by state: available (default) or unavailable."},
52+
"names": schema.ListAttribute{Computed: true, ElementType: types.StringType, MarkdownDescription: "The matching availability zone names."},
53+
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region."},
54+
},
55+
}
56+
}
57+
58+
func (d *azDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
59+
d.config = configureClient(req.ProviderData, &resp.Diagnostics)
60+
}
61+
62+
func (d *azDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
63+
var data azDataSourceModel
64+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
65+
if resp.Diagnostics.HasError() {
66+
return
67+
}
68+
69+
client, err := d.config.ComputeV2Client()
70+
if err != nil {
71+
resp.Diagnostics.AddError("compute: building v2 client", err.Error())
72+
return
73+
}
74+
75+
pages, err := availabilityzones.List(client).AllPages(ctx)
76+
if err != nil {
77+
resp.Diagnostics.AddError("compute: listing availability zones", err.Error())
78+
return
79+
}
80+
zones, err := availabilityzones.ExtractAvailabilityZones(pages)
81+
if err != nil {
82+
resp.Diagnostics.AddError("compute: extracting availability zones", err.Error())
83+
return
84+
}
85+
86+
wantAvailable := data.State.ValueString() != "unavailable"
87+
names := make([]string, 0, len(zones))
88+
for _, z := range zones {
89+
if z.ZoneState.Available == wantAvailable {
90+
names = append(names, z.ZoneName)
91+
}
92+
}
93+
94+
nameList, diags := types.ListValueFrom(ctx, types.StringType, names)
95+
resp.Diagnostics.Append(diags...)
96+
data.Names = nameList
97+
if data.Region.IsNull() || data.Region.IsUnknown() {
98+
data.Region = types.StringValue(d.config.Region)
99+
}
100+
data.ID = types.StringValue(d.config.Region + ":azs")
101+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
102+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package compute_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
10+
11+
"github.com/platform9/terraform-provider-pcd/internal/acctest"
12+
)
13+
14+
func TestAccComputeAvailabilityZones_basic(t *testing.T) {
15+
resource.Test(t, resource.TestCase{
16+
PreCheck: func() { acctest.PreCheck(t) },
17+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
18+
Steps: []resource.TestStep{
19+
{
20+
Config: `data "pcd_compute_availability_zones" "zones" {}`,
21+
Check: resource.ComposeAggregateTestCheckFunc(
22+
resource.TestCheckResourceAttrSet("data.pcd_compute_availability_zones.zones", "names.0"),
23+
),
24+
},
25+
},
26+
})
27+
}
28+
29+
func TestAccComputeKeypairDataSource_basic(t *testing.T) {
30+
resource.Test(t, resource.TestCase{
31+
PreCheck: func() { acctest.PreCheck(t) },
32+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
33+
CheckDestroy: testAccCheckKeypairDestroy(t),
34+
Steps: []resource.TestStep{
35+
{
36+
Config: `
37+
resource "pcd_compute_keypair" "test" {
38+
name = "tf-acc-kp-ds"
39+
}
40+
41+
data "pcd_compute_keypair" "by_name" {
42+
name = pcd_compute_keypair.test.name
43+
}
44+
`,
45+
Check: resource.ComposeAggregateTestCheckFunc(
46+
resource.TestCheckResourceAttrPair("data.pcd_compute_keypair.by_name", "id", "pcd_compute_keypair.test", "name"),
47+
resource.TestCheckResourceAttrSet("data.pcd_compute_keypair.by_name", "public_key"),
48+
resource.TestCheckResourceAttrSet("data.pcd_compute_keypair.by_name", "fingerprint"),
49+
),
50+
},
51+
},
52+
})
53+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
//
4+
// Ported from terraform-provider-openstack v3.4.0
5+
// (openstack/data_source_openstack_compute_flavor_v2.go), adapted for the
6+
// terraform-plugin-framework and PCD.
7+
8+
package compute
9+
10+
import (
11+
"context"
12+
"fmt"
13+
14+
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors"
15+
"github.com/hashicorp/terraform-plugin-framework/datasource"
16+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
18+
19+
"github.com/platform9/terraform-provider-pcd/internal/clients"
20+
)
21+
22+
var (
23+
_ datasource.DataSource = (*flavorDataSource)(nil)
24+
_ datasource.DataSourceWithConfigure = (*flavorDataSource)(nil)
25+
)
26+
27+
// NewFlavorDataSource is the factory registered with the provider.
28+
func NewFlavorDataSource() datasource.DataSource {
29+
return &flavorDataSource{}
30+
}
31+
32+
type flavorDataSource struct {
33+
config *clients.Config
34+
}
35+
36+
type flavorDataSourceModel struct {
37+
ID types.String `tfsdk:"id"`
38+
FlavorID types.String `tfsdk:"flavor_id"`
39+
Name types.String `tfsdk:"name"`
40+
RAM types.Int64 `tfsdk:"ram"`
41+
VCPUs types.Int64 `tfsdk:"vcpus"`
42+
Disk types.Int64 `tfsdk:"disk"`
43+
Swap types.Int64 `tfsdk:"swap"`
44+
RxTxFactor types.Float64 `tfsdk:"rx_tx_factor"`
45+
IsPublic types.Bool `tfsdk:"is_public"`
46+
Region types.String `tfsdk:"region"`
47+
}
48+
49+
func (d *flavorDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
50+
resp.TypeName = req.ProviderTypeName + "_compute_flavor"
51+
}
52+
53+
func (d *flavorDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
54+
resp.Schema = schema.Schema{
55+
MarkdownDescription: "Look up a compute flavor by name or ID.",
56+
Attributes: map[string]schema.Attribute{
57+
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "The flavor ID."},
58+
"flavor_id": schema.StringAttribute{Optional: true, MarkdownDescription: "Look up by flavor ID (takes precedence over name)."},
59+
"name": schema.StringAttribute{Optional: true, MarkdownDescription: "Look up by exact name."},
60+
"ram": schema.Int64Attribute{Computed: true, MarkdownDescription: "Memory in MB."},
61+
"vcpus": schema.Int64Attribute{Computed: true, MarkdownDescription: "Number of vCPUs."},
62+
"disk": schema.Int64Attribute{Computed: true, MarkdownDescription: "Root disk in GB."},
63+
"swap": schema.Int64Attribute{Computed: true, MarkdownDescription: "Swap in MB."},
64+
"rx_tx_factor": schema.Float64Attribute{Computed: true, MarkdownDescription: "RX/TX factor."},
65+
"is_public": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the flavor is public."},
66+
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region."},
67+
},
68+
}
69+
}
70+
71+
func (d *flavorDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
72+
d.config = configureClient(req.ProviderData, &resp.Diagnostics)
73+
}
74+
75+
func (d *flavorDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
76+
var data flavorDataSourceModel
77+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
78+
if resp.Diagnostics.HasError() {
79+
return
80+
}
81+
82+
client, err := d.config.ComputeV2Client()
83+
if err != nil {
84+
resp.Diagnostics.AddError("compute: building v2 client", err.Error())
85+
return
86+
}
87+
88+
var flavor *flavors.Flavor
89+
if v := data.FlavorID.ValueString(); v != "" {
90+
flavor, err = flavors.Get(ctx, client, v).Extract()
91+
if err != nil {
92+
resp.Diagnostics.AddError("compute: getting flavor by id", err.Error())
93+
return
94+
}
95+
} else {
96+
pages, err := flavors.ListDetail(client, flavors.ListOpts{}).AllPages(ctx)
97+
if err != nil {
98+
resp.Diagnostics.AddError("compute: listing flavors", err.Error())
99+
return
100+
}
101+
all, err := flavors.ExtractFlavors(pages)
102+
if err != nil {
103+
resp.Diagnostics.AddError("compute: extracting flavors", err.Error())
104+
return
105+
}
106+
name := data.Name.ValueString()
107+
var matches []flavors.Flavor
108+
for _, f := range all {
109+
if f.Name == name {
110+
matches = append(matches, f)
111+
}
112+
}
113+
switch len(matches) {
114+
case 0:
115+
resp.Diagnostics.AddError("No flavor found", fmt.Sprintf("No flavor named %q.", name))
116+
return
117+
case 1:
118+
flavor = &matches[0]
119+
default:
120+
resp.Diagnostics.AddError("Multiple flavors found", fmt.Sprintf("%d flavors named %q.", len(matches), name))
121+
return
122+
}
123+
}
124+
125+
data.ID = types.StringValue(flavor.ID)
126+
data.FlavorID = types.StringValue(flavor.ID)
127+
data.Name = types.StringValue(flavor.Name)
128+
data.RAM = types.Int64Value(int64(flavor.RAM))
129+
data.VCPUs = types.Int64Value(int64(flavor.VCPUs))
130+
data.Disk = types.Int64Value(int64(flavor.Disk))
131+
data.Swap = types.Int64Value(int64(flavor.Swap))
132+
data.RxTxFactor = types.Float64Value(flavor.RxTxFactor)
133+
data.IsPublic = types.BoolValue(flavor.IsPublic)
134+
if data.Region.IsNull() || data.Region.IsUnknown() {
135+
data.Region = types.StringValue(d.config.Region)
136+
}
137+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
138+
}

0 commit comments

Comments
 (0)