Skip to content

Commit 6f4e1f2

Browse files
authored
Merge pull request #6 from platform9/feat/compute
feat(compute): Nova keypair/flavor/servergroup/instance and data sources
2 parents 9bad709 + b6028b9 commit 6f4e1f2

16 files changed

Lines changed: 1850 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/clients/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,17 @@ func (c *Config) NetworkV2Client() (*gophercloud.ServiceClient, error) {
213213
return client, nil
214214
}
215215

216+
// ComputeV2Client returns a Nova v2 service client, honoring an endpoint_overrides
217+
// entry for the "compute" service type if present.
218+
func (c *Config) ComputeV2Client() (*gophercloud.ServiceClient, error) {
219+
client, err := openstack.NewComputeV2(c.Provider, c.endpointOpts())
220+
if err != nil {
221+
return nil, fmt.Errorf("pcd: creating compute v2 client: %w", err)
222+
}
223+
c.applyOverride(client, "compute")
224+
return client, nil
225+
}
226+
216227
// applyOverride points a service client at an operator-supplied endpoint when
217228
// endpoint_overrides names its service type.
218229
func (c *Config) applyOverride(client *gophercloud.ServiceClient, serviceType string) {

internal/provider/provider.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/hashicorp/terraform-plugin-framework/provider"
1111
"github.com/hashicorp/terraform-plugin-framework/resource"
1212

13+
"github.com/platform9/terraform-provider-pcd/internal/services/compute"
1314
"github.com/platform9/terraform-provider-pcd/internal/services/identity"
1415
"github.com/platform9/terraform-provider-pcd/internal/services/images"
1516
"github.com/platform9/terraform-provider-pcd/internal/services/networking"
@@ -51,6 +52,10 @@ func (p *pcdProvider) Resources(_ context.Context) []func() resource.Resource {
5152
networking.NewSecgroupRuleResource,
5253
networking.NewRouterResource,
5354
networking.NewRouterInterfaceResource,
55+
compute.NewKeypairResource,
56+
compute.NewInstanceResource,
57+
compute.NewFlavorResource,
58+
compute.NewServergroupResource,
5459
}
5560
}
5661

@@ -65,5 +70,8 @@ func (p *pcdProvider) DataSources(_ context.Context) []func() datasource.DataSou
6570
networking.NewNetworkDataSource,
6671
networking.NewSubnetDataSource,
6772
networking.NewSecgroupDataSource,
73+
compute.NewFlavorDataSource,
74+
compute.NewKeypairDataSource,
75+
compute.NewAvailabilityZonesDataSource,
6876
}
6977
}
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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
// Package compute implements the pcd_compute_* resources and data sources
5+
// (Nova v2), ported from terraform-provider-openstack v3.4.0.
6+
package compute
7+
8+
import (
9+
"fmt"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
13+
"github.com/platform9/terraform-provider-pcd/internal/clients"
14+
)
15+
16+
// configureClient extracts the shared *clients.Config from ProviderData.
17+
func configureClient(providerData any, diags *diag.Diagnostics) *clients.Config {
18+
if providerData == nil {
19+
return nil
20+
}
21+
config, ok := providerData.(*clients.Config)
22+
if !ok {
23+
diags.AddError(
24+
"Unexpected provider data type",
25+
fmt.Sprintf("Expected *clients.Config, got %T. This is a bug in the provider.", providerData),
26+
)
27+
return nil
28+
}
29+
return config
30+
}
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)