Skip to content

Commit d00ce21

Browse files
aidyhawksight
andauthored
Add resource to validate GCP CloudProvider connections (#80)
* Add resource to validate GCP CloudProvider connections Interface for this is a bit clumsy. In order for the cloudprovider to be usable, we must issue an API call to validate the cloudprovider connection. We can only do this after we've done all of the other things to put the identity configuration in place. That means that we can't do it as part of the create call for tlspc_cloudprovider_gcp (as we must take parameters from that in order to set up the workload identity pool provider). However, this is effectively a write-once API. We can only ever validate the connection, we can't unset that status once we've set it. That's not a great mapping for terraform, but I think it's the best we can do. * chore: Add resource and paramater descriptions Signed-off-by: Peter Fiddes <[email protected]> * chore: generate cloudprovider_gcp_validate docs file with tfplugindocs Signed-off-by: Peter Fiddes <[email protected]> --------- Signed-off-by: Peter Fiddes <[email protected]> Co-authored-by: Peter Fiddes <[email protected]>
1 parent a5cf04d commit d00ce21

File tree

7 files changed

+693
-0
lines changed

7 files changed

+693
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "tlspc_cloudprovider_gcp_validate Resource - tlspc"
4+
subcategory: ""
5+
description: |-
6+
Activates a GCP Cloud Provider configuration for usage.
7+
---
8+
9+
# tlspc_cloudprovider_gcp_validate (Resource)
10+
11+
Activates a GCP Cloud Provider configuration for usage.
12+
13+
## Example Usage
14+
15+
```terraform
16+
resource "tlspc_cloudprovider_gcp" "gcp-cloudprovider" {
17+
name = "terraform-wif"
18+
team = resource.tlspc_team.team.id
19+
service_account_email = resource.google_service_account.tlspc.email
20+
project_number = data.google_project.project.number
21+
workload_identity_pool_id = resource.google_iam_workload_identity_pool.tlspc.workload_identity_pool_id
22+
workload_identity_pool_provider_id = "venafi-provider"
23+
}
24+
25+
resource "google_iam_workload_identity_pool_provider" "tlspc" {
26+
workload_identity_pool_id = resource.google_iam_workload_identity_pool.tlspc.workload_identity_pool_id
27+
workload_identity_pool_provider_id = resource.tlspc_cloudprovider_gcp.gcp-cloudprovider.workload_identity_pool_provider_id
28+
display_name = "Venafi TLSPC"
29+
description = "Venafi WIF Pool Provider"
30+
attribute_mapping = {
31+
"google.subject" = "assertion.sub"
32+
}
33+
oidc {
34+
issuer_uri = resource.tlspc_cloudprovider_gcp.gcp-cloudprovider.issuer_url
35+
}
36+
}
37+
38+
resource "tlspc_cloudprovider_gcp_validate" "gcp-cloudprovider-validation" {
39+
cloudprovider_id = resource.tlspc_cloudprovider_gcp.gcp-cloudprovider.id
40+
validate = true
41+
# pool provider required to be present for successful connection validation
42+
depends_on = [
43+
resource.google_iam_workload_identity_pool_provider.tlspc
44+
]
45+
}
46+
```
47+
48+
<!-- schema generated by tfplugindocs -->
49+
## Schema
50+
51+
### Required
52+
53+
- `cloudprovider_id` (String) Reference to the tlspc_cloudprovider_gcp resource to validate.
54+
- `validate` (Boolean) Set to true to validate the GCP Cloud Provider connection.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
resource "tlspc_cloudprovider_gcp" "gcp-cloudprovider" {
2+
name = "terraform-wif"
3+
team = resource.tlspc_team.team.id
4+
service_account_email = resource.google_service_account.tlspc.email
5+
project_number = data.google_project.project.number
6+
workload_identity_pool_id = resource.google_iam_workload_identity_pool.tlspc.workload_identity_pool_id
7+
workload_identity_pool_provider_id = "venafi-provider"
8+
}
9+
10+
resource "google_iam_workload_identity_pool_provider" "tlspc" {
11+
workload_identity_pool_id = resource.google_iam_workload_identity_pool.tlspc.workload_identity_pool_id
12+
workload_identity_pool_provider_id = resource.tlspc_cloudprovider_gcp.gcp-cloudprovider.workload_identity_pool_provider_id
13+
display_name = "Venafi TLSPC"
14+
description = "Venafi WIF Pool Provider"
15+
attribute_mapping = {
16+
"google.subject" = "assertion.sub"
17+
}
18+
oidc {
19+
issuer_uri = resource.tlspc_cloudprovider_gcp.gcp-cloudprovider.issuer_url
20+
}
21+
}
22+
23+
resource "tlspc_cloudprovider_gcp_validate" "gcp-cloudprovider-validation" {
24+
cloudprovider_id = resource.tlspc_cloudprovider_gcp.gcp-cloudprovider.id
25+
validate = true
26+
# pool provider required to be present for successful connection validation
27+
depends_on = [
28+
resource.google_iam_workload_identity_pool_provider.tlspc
29+
]
30+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) Venafi, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"terraform-provider-tlspc/internal/tlspc"
11+
12+
"github.com/hashicorp/terraform-plugin-framework/path"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
)
17+
18+
var (
19+
_ resource.Resource = &cloudProviderGCPValidateResource{}
20+
_ resource.ResourceWithConfigure = &cloudProviderGCPValidateResource{}
21+
_ resource.ResourceWithImportState = &cloudProviderGCPValidateResource{}
22+
)
23+
24+
type cloudProviderGCPValidateResource struct {
25+
client *tlspc.Client
26+
}
27+
28+
func NewCloudProviderGCPValidateResource() resource.Resource {
29+
return &cloudProviderGCPValidateResource{}
30+
}
31+
32+
func (r *cloudProviderGCPValidateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
33+
resp.TypeName = req.ProviderTypeName + "_cloudprovider_gcp_validate"
34+
}
35+
36+
func (r *cloudProviderGCPValidateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
37+
resp.Schema = schema.Schema{
38+
MarkdownDescription: "Activates a GCP Cloud Provider configuration for usage.",
39+
Attributes: map[string]schema.Attribute{
40+
"cloudprovider_id": schema.StringAttribute{
41+
Required: true,
42+
MarkdownDescription: "Reference to the tlspc_cloudprovider_gcp resource to validate.",
43+
},
44+
"validate": schema.BoolAttribute{
45+
Required: true,
46+
MarkdownDescription: "Set to true to validate the GCP Cloud Provider connection.",
47+
},
48+
},
49+
}
50+
}
51+
52+
func (r *cloudProviderGCPValidateResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
53+
if req.ProviderData == nil {
54+
return
55+
}
56+
57+
client, ok := req.ProviderData.(*tlspc.Client)
58+
59+
if !ok {
60+
resp.Diagnostics.AddError(
61+
"Unexpected Data Source Configure Type",
62+
fmt.Sprintf("Expected *tlspc.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
63+
)
64+
65+
return
66+
}
67+
68+
r.client = client
69+
}
70+
71+
type cloudProviderGCPValidateResourceModel struct {
72+
CloudProviderID types.String `tfsdk:"cloudprovider_id"`
73+
Validate types.Bool `tfsdk:"validate"`
74+
}
75+
76+
func (r *cloudProviderGCPValidateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
77+
var plan cloudProviderGCPValidateResourceModel
78+
diags := req.Plan.Get(ctx, &plan)
79+
resp.Diagnostics.Append(diags...)
80+
if resp.Diagnostics.HasError() {
81+
return
82+
}
83+
84+
if !plan.Validate.ValueBool() {
85+
resp.Diagnostics.AddError(
86+
"Error validating GCP Cloud Provider Connection",
87+
"Validate can only be set to true",
88+
)
89+
return
90+
}
91+
92+
validated, err := r.client.ValidateCloudProviderGCP(ctx, plan.CloudProviderID.ValueString())
93+
94+
if err != nil {
95+
resp.Diagnostics.AddError(
96+
"Error validating GCP Cloud Provider Connection",
97+
"Could validate GCP Cloud Provider: "+err.Error(),
98+
)
99+
return
100+
}
101+
102+
if !validated {
103+
resp.Diagnostics.AddError(
104+
"Error validating GCP Cloud Provider Connection",
105+
"Could validate GCP Cloud Provider connection",
106+
)
107+
return
108+
}
109+
110+
plan.Validate = types.BoolValue(validated)
111+
112+
diags = resp.State.Set(ctx, plan)
113+
resp.Diagnostics.Append(diags...)
114+
}
115+
116+
func (r *cloudProviderGCPValidateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
117+
var state cloudProviderGCPValidateResourceModel
118+
119+
diags := req.State.Get(ctx, &state)
120+
resp.Diagnostics.Append(diags...)
121+
if resp.Diagnostics.HasError() {
122+
return
123+
}
124+
125+
validated, err := r.client.GetCloudProviderGCPValidation(ctx, state.CloudProviderID.ValueString())
126+
// Really, we should parse the error here and conditionally either bomb out, or set the validated status to false
127+
// The api isn't really built around giving us a good way of determining whether or not it's an error with the request
128+
// or if the connection requires validation. For now, set the state to false, we can only ever attempt to set it to true,
129+
// so this should be reasonably safe and sane.
130+
_ = err
131+
/*
132+
if err != nil {
133+
resp.Diagnostics.AddError(
134+
"Error retrieving GCP Cloud Provider Connection validation state",
135+
"Could not retrieve state: "+err.Error(),
136+
)
137+
return
138+
}
139+
*/
140+
141+
state.Validate = types.BoolValue(validated)
142+
143+
diags = resp.State.Set(ctx, state)
144+
resp.Diagnostics.Append(diags...)
145+
}
146+
147+
func (r *cloudProviderGCPValidateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
148+
var state, plan cloudProviderGCPValidateResourceModel
149+
150+
diags := req.State.Get(ctx, &state)
151+
resp.Diagnostics.Append(diags...)
152+
if resp.Diagnostics.HasError() {
153+
return
154+
}
155+
diags = req.Plan.Get(ctx, &plan)
156+
resp.Diagnostics.Append(diags...)
157+
if resp.Diagnostics.HasError() {
158+
return
159+
}
160+
161+
if !plan.Validate.ValueBool() {
162+
if state.Validate.ValueBool() {
163+
resp.Diagnostics.AddError(
164+
"Error updating GCP Cloud Provider Connection validation",
165+
"Can not unvalidate connection status",
166+
)
167+
} else {
168+
resp.Diagnostics.AddError(
169+
"Error validating GCP Cloud Provider Connection",
170+
"Validate can only be set to true",
171+
)
172+
}
173+
return
174+
}
175+
176+
validated, err := r.client.ValidateCloudProviderGCP(ctx, state.CloudProviderID.ValueString())
177+
178+
if err != nil {
179+
resp.Diagnostics.AddError(
180+
"Error validating GCP Cloud Provider Connection",
181+
"Could validate GCP Cloud Provider: "+err.Error(),
182+
)
183+
return
184+
}
185+
186+
plan.Validate = types.BoolValue(validated)
187+
188+
diags = resp.State.Set(ctx, plan)
189+
resp.Diagnostics.Append(diags...)
190+
}
191+
192+
func (r *cloudProviderGCPValidateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
193+
var state cloudProviderGCPValidateResourceModel
194+
195+
diags := req.State.Get(ctx, &state)
196+
resp.Diagnostics.Append(diags...)
197+
if resp.Diagnostics.HasError() {
198+
return
199+
}
200+
201+
// Can't delete validated state. Nothing to do here.
202+
}
203+
204+
func (r *cloudProviderGCPValidateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
205+
// Retrieve import ID and save to id attribute
206+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
207+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func (p *tlspcProvider) Resources(ctx context.Context) []func() resource.Resourc
103103
NewFireflySubCAResource,
104104
NewFireflyPolicyResource,
105105
NewCloudProviderGCPResource,
106+
NewCloudProviderGCPValidateResource,
106107
}
107108
}
108109

internal/tlspc/graphql.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,40 @@ func (c *Client) DeleteCloudProviderGCP(ctx context.Context, id string) error {
219219

220220
return err
221221
}
222+
223+
func (c *Client) GetCloudProviderGCPValidation(ctx context.Context, id string) (bool, error) {
224+
gql := c.GetGraphQLClient()
225+
226+
cpId, err := uuid.Parse(id)
227+
if err != nil {
228+
return false, err
229+
}
230+
231+
resp, err := graphql.GetGCPProviderDetails(ctx, gql, cpId)
232+
if err != nil {
233+
return false, err
234+
}
235+
236+
details, ok := resp.CloudProviderDetails.(*graphql.GetGCPProviderDetailsCloudProviderDetailsGCPProviderDetails)
237+
if !ok {
238+
return false, errors.New("Error retrieving GCP CloudProvider status")
239+
}
240+
241+
return details.CloudProvider.Status == graphql.CloudProviderStatusValidated, nil
242+
}
243+
244+
func (c *Client) ValidateCloudProviderGCP(ctx context.Context, id string) (bool, error) {
245+
gql := c.GetGraphQLClient()
246+
247+
cpId, err := uuid.Parse(id)
248+
if err != nil {
249+
return false, err
250+
}
251+
252+
resp, err := graphql.ValidateGCPProvider(ctx, gql, cpId)
253+
if err != nil {
254+
return false, err
255+
}
256+
257+
return resp.ValidateCloudProvider.Result == graphql.CloudProviderStatusValidated, nil
258+
}

internal/tlspc/graphql/genqlient.graphql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,21 @@ mutation UpdateGCPProvider($Id: UUID!, $Name: String!, $Team: UUID!, $Project: S
9090
mutation DeleteGCPProvider($Id: UUID!) {
9191
deleteCloudProvider(cloudProviderId: [$Id])
9292
}
93+
94+
query GetGCPProviderDetails($Id: UUID!) {
95+
cloudProviderDetails(cloudProviderId: $Id) {
96+
... on GCPProviderDetails {
97+
cloudProvider {
98+
id
99+
status
100+
}
101+
}
102+
}
103+
}
104+
105+
mutation ValidateGCPProvider($Id: UUID!) {
106+
validateCloudProvider(cloudProviderId: $Id) {
107+
result
108+
details
109+
}
110+
}

0 commit comments

Comments
 (0)