Skip to content

Commit

Permalink
VAULT-31135: add new hcp_vault_radar_source_github_cloud resource. (#…
Browse files Browse the repository at this point in the history
…1119)

* VAULT-31135: add new hcp_vault_radar_source_github_cloud resource.
  • Loading branch information
trentdibacco authored Oct 18, 2024
1 parent b52e294 commit f92d07d
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 200 deletions.
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
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.
10 changes: 10 additions & 0 deletions examples/resources/hcp_vault_radar_source_github_cloud/resource.tf
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
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
// 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")
}
96 changes: 96 additions & 0 deletions internal/provider/vaultradar/resource_radar_source_github_cloud.go
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

0 comments on commit f92d07d

Please sign in to comment.