Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VAULT-31135: add new hcp_vault_radar_source_github_cloud resource. #1119

Merged
merged 3 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/1119.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious where 1119 comes from, I see 1116 from your last PR but don't see 1117 or 1118 on main, are they from other PRs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1119 the PR number

Add preview of vault_radar_source_github_cloud resource.
```
44 changes: 44 additions & 0 deletions docs/resources/vault_radar_source_github_cloud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
page_title: "hcp_vault_radar_source_github_cloud Resource - terraform-provider-hcp"
subcategory: ""
description: |-
This terraform resource manages a GitHub Cloud data source lifecycle in Vault Radar.
---

# hcp_vault_radar_source_github_cloud (Resource)

-> **Note:** HCP Vault Radar Terraform resources are in preview.

This terraform resource manages a GitHub Cloud data source lifecycle in Vault Radar.

## Example Usage

```terraform
variable "github_cloud_token" {
type = string
sensitive = true
}

resource "hcp_vault_radar_source_github_cloud" "example" {
github_organization = "my-github-org"
token = var.github_cloud_token
project_id = "my-project-id"
}
```


<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `github_organization` (String) GitHub organization Vault Radar will monitor. Example: type "octocat" for the org https://github.com/octocat
- `token` (String, Sensitive) GitHub personal access token.

### Optional

- `project_id` (String) The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured.

### Read-Only

- `id` (String) The ID of this resource.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
variable "github_cloud_token" {
type = string
sensitive = true
}

resource "hcp_vault_radar_source_github_cloud" "example" {
github_organization = "my-github-org"
token = var.github_cloud_token
project_id = "my-project-id"
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res
waypoint.NewTfcConfigResource,
// Radar
vaultradar.NewSourceGitHubEnterpriseResource,
vaultradar.NewSourceGitHubCloudResource,
vaultradar.NewIntegrationJiraConnectionResource,
vaultradar.NewIntegrationJiraSubscriptionResource,
vaultradar.NewIntegrationSlackConnectionResource,
Expand Down
191 changes: 191 additions & 0 deletions internal/provider/vaultradar/radar_source.go
Copy link
Contributor Author

@trentdibacco trentdibacco Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation for most part was taken from the previous implementation of resource_radar_source_github_enterprise but refactored out a few things to be shared with resource_radar_source_github_cloud.

Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package vaultradar

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-provider-hcp/internal/clients"
"github.com/hashicorp/terraform-provider-hcp/internal/provider/modifiers"

service "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-radar/preview/2023-05-01/client/data_source_registration_service"
)

var (
_ resource.Resource = &radarSourceResource{}
_ resource.ResourceWithConfigure = &radarSourceResource{}
)

// radarSourceResource is an implementation for configuring specific types Radar data sources.
// Examples: hcp_vault_radar_source_github_cloud and hcp__vault_radar_source_github_enterprise make use of

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should be one underscore instead of the two hcp__vault_radar_source_github_enterprise ?

// this implementation to define resources with specific schemas, validation, and state details related to their types.
type radarSourceResource struct {
client *clients.Client
TypeName string
SourceType string
ConnectionSchema schema.Schema
GetSourceFromPlan func(ctx context.Context, plan tfsdk.Plan) (radarSource, diag.Diagnostics)
GetSourceFromState func(ctx context.Context, state tfsdk.State) (radarSource, diag.Diagnostics)
}

func (r *radarSourceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + r.TypeName
}

func (r *radarSourceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = r.ConnectionSchema
}

// radarSource is the minimal plan/state that a Radar source must have.
type radarSource interface {
GetProjectID() types.String
SetProjectID(types.String)
GetID() types.String
SetID(types.String)
GetName() types.String
GetConnectionURL() types.String
GetToken() types.String
}

func (r *radarSourceResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*clients.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}

func (r *radarSourceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
modifiers.ModifyPlanForDefaultProjectChange(ctx, r.client.Config.ProjectID, req.State, req.Config, req.Plan, resp)
}

func (r *radarSourceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
src, diags := r.GetSourceFromPlan(ctx, req.Plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

projectID := r.client.Config.ProjectID
if !src.GetProjectID().IsUnknown() {
projectID = src.GetProjectID().ValueString()
}

body := service.OnboardDataSourceBody{
Type: r.SourceType,
Name: src.GetName().ValueString(),

Token: src.GetToken().ValueString(),
}

if !src.GetConnectionURL().IsNull() {
body.ConnectionURL = src.GetConnectionURL().ValueString()
}

res, err := clients.OnboardRadarSource(ctx, r.client, projectID, body)
if err != nil {
resp.Diagnostics.AddError("Error creating Radar source", err.Error())
return
}

src.SetID(types.StringValue(res.GetPayload().ID))
src.SetProjectID(types.StringValue(projectID))
resp.Diagnostics.Append(resp.State.Set(ctx, &src)...)
}

func (r *radarSourceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
src, diags := r.GetSourceFromState(ctx, req.State)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

projectID := r.client.Config.ProjectID
if !src.GetProjectID().IsUnknown() {
projectID = src.GetProjectID().ValueString()
}

res, err := clients.GetRadarSource(ctx, r.client, projectID, src.GetID().ValueString())
if err != nil {
if clients.IsResponseCodeNotFound(err) {
// Resource is no longer on the server.
tflog.Info(ctx, "Radar source not found, removing from state.")
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Unable to get Radar source", err.Error())
return
}

// Resource is marked as deleted on the server.
if res.GetPayload().Deleted {
// Don't update or remove the state, because its has not been fully deleted server side.
tflog.Warn(ctx, "Radar source marked for deletion.")
return
}

// The only other state that could change related to this resource is the token, and for obvious reasons we don't
// return that in the read response. So we don't need to update the state here.
}

func (r *radarSourceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
src, diags := r.GetSourceFromState(ctx, req.State)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

projectID := r.client.Config.ProjectID
if !src.GetProjectID().IsUnknown() {
projectID = src.GetProjectID().ValueString()
}

// Assert resource still exists.
res, err := clients.GetRadarSource(ctx, r.client, projectID, src.GetID().ValueString())
if err != nil {
if clients.IsResponseCodeNotFound(err) {
// Resource is no longer on the server.
tflog.Info(ctx, "Radar source not found, removing from state.")
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Unable to get Radar source", err.Error())
return
}

// Resource is already marked as being deleted on the server. Wait for it to be fully deleted.
if res.GetPayload().Deleted {
tflog.Info(ctx, "Radar resource already marked for deletion, waiting for full deletion")
if err := clients.WaitOnOffboardRadarSource(ctx, r.client, projectID, src.GetID().ValueString()); err != nil {
resp.Diagnostics.AddError("Unable to delete Radar source", err.Error())
return
}

tflog.Trace(ctx, "Deleted Radar resource")
return
}

// Offboard the Radar source.
if err := clients.OffboardRadarSource(ctx, r.client, projectID, src.GetID().ValueString()); err != nil {
resp.Diagnostics.AddError("Unable to delete Radar source", err.Error())
return
}
}

func (r *radarSourceResource) Update(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) {
// In-place update is not supported.
// Plans to support updating the token will be in a future iteration.
resp.Diagnostics.AddError("Unexpected provider error", "This is an internal error, please report this issue to the provider developers")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package vaultradar

import (
"context"
"regexp"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

func NewSourceGitHubCloudResource() resource.Resource {
return &radarSourceResource{
TypeName: "_vault_radar_source_github_cloud",
SourceType: "github_cloud",
ConnectionSchema: githubCloudSourceSchema,
GetSourceFromPlan: func(ctx context.Context, plan tfsdk.Plan) (radarSource, diag.Diagnostics) {
var data githubCloudSourceData
diags := plan.Get(ctx, &data)
return &data, diags
},
GetSourceFromState: func(ctx context.Context, state tfsdk.State) (radarSource, diag.Diagnostics) {
var data githubCloudSourceData
diags := state.Get(ctx, &data)
return &data, diags
}}
}

var githubCloudSourceSchema = schema.Schema{
MarkdownDescription: "This terraform resource manages a GitHub Cloud data source lifecycle in Vault Radar.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "The ID of this resource.",
},
"github_organization": schema.StringAttribute{
Description: `GitHub organization Vault Radar will monitor. Example: type "octocat" for the org https://github.com/octocat`,
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.RegexMatches(regexp.MustCompile(`^[a-zA-Z0-9-_.]+$`),
"must contain only letters, numbers, hyphens, underscores, or periods",
),
},
},
"token": schema.StringAttribute{
Description: "GitHub personal access token.",
Required: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},

// Optional inputs
"project_id": schema.StringAttribute{
Description: "The ID of the HCP project where Vault Radar is located. If not specified, the project specified in the HCP Provider config block will be used, if configured.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
},
},
}

type githubCloudSourceData struct {
ID types.String `tfsdk:"id"`
GitHubOrganization types.String `tfsdk:"github_organization"`
Token types.String `tfsdk:"token"`
ProjectID types.String `tfsdk:"project_id"`
}

func (d *githubCloudSourceData) GetProjectID() types.String { return d.ProjectID }

func (d *githubCloudSourceData) SetProjectID(projectID types.String) { d.ProjectID = projectID }

func (d *githubCloudSourceData) GetID() types.String { return d.ID }

func (d *githubCloudSourceData) SetID(id types.String) { d.ID = id }

func (d *githubCloudSourceData) GetName() types.String { return d.GitHubOrganization }

func (d *githubCloudSourceData) GetConnectionURL() types.String { return basetypes.NewStringNull() }

func (d *githubCloudSourceData) GetToken() types.String { return d.Token }
Loading