Skip to content

Commit

Permalink
feat: sys-444 add credential resource support
Browse files Browse the repository at this point in the history
  • Loading branch information
gigovich committed Dec 3, 2024
1 parent 07f8257 commit f61d69c
Show file tree
Hide file tree
Showing 8 changed files with 492 additions and 8 deletions.
44 changes: 44 additions & 0 deletions docs/resources/credential.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "uptime_credential Resource - terraform-provider-uptime"
subcategory: ""
description: |-
---

# uptime_credential (Resource)





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

### Required

- `credential_type` (String)
- `display_name` (String)
- `secret` (Attributes) (see [below for nested schema](#nestedatt--secret))

### Optional

- `description` (String)
- `username` (String)

### Read-Only

- `id` (Number) The ID of this resource.

<a id="nestedatt--secret"></a>
### Nested Schema for `secret`

Optional:

- `certificate` (String, Sensitive)
- `key` (String, Sensitive)
- `passphrase` (String, Sensitive)
- `password` (String, Sensitive)
- `secret` (String, Sensitive)


2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.9.0
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241125121723-7c0ae58b90f0
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241203124913-62cfc1744b9a
)

require (
Expand Down
8 changes: 2 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,8 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241003114205-5aa12ad3c97a h1:bMIbbcsD3mnLzVD0fxnJk1sW+fRiT/yFw9gW2Rz1rsw=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241003114205-5aa12ad3c97a/go.mod h1:jtWeB/tQ00fLX2r9OwKfTnxQ/PMR0YjmhTuc9RZH2h0=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241121151606-4174b6056aa2 h1:qPVLJNbG1+BshnIsykJGzAxYfQaBuPe3MFi7h5yNDbg=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241121151606-4174b6056aa2/go.mod h1:jtWeB/tQ00fLX2r9OwKfTnxQ/PMR0YjmhTuc9RZH2h0=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241125121723-7c0ae58b90f0 h1:NorS7ES2ekYYZQjSpePzYj/QrTen/P7Cc1E1rGsnBQ0=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241125121723-7c0ae58b90f0/go.mod h1:jtWeB/tQ00fLX2r9OwKfTnxQ/PMR0YjmhTuc9RZH2h0=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241203124913-62cfc1744b9a h1:SUXIUdqiAvW42db1yTE+aIuYAdVyMuv7o3VcsCoInsM=
github.com/uptime-com/uptime-client-go/v2 v2.0.0-20241203124913-62cfc1744b9a/go.mod h1:jtWeB/tQ00fLX2r9OwKfTnxQ/PMR0YjmhTuc9RZH2h0=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ func (p *providerImpl) Resources(ctx context.Context) []func() resource.Resource
func() resource.Resource { return NewCheckWHOISResource(ctx, p) },
func() resource.Resource { return NewCheckWebhookResource(ctx, p) },
func() resource.Resource { return NewCheckMaintenanceResource(ctx, p) },

func() resource.Resource { return NewContactResource(ctx, p) },
func() resource.Resource { return NewCredentialResource(ctx, p) },
func() resource.Resource { return NewStatusPageResource(ctx, p) },
func() resource.Resource { return NewStatusPageComponentResource(ctx, p) },
func() resource.Resource { return NewStatusPageIncidentResource(ctx, p) },
Expand Down
311 changes: 311 additions & 0 deletions internal/provider/resource_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
package provider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/uptime-com/uptime-client-go/v2/pkg/upapi"
)

func NewCredentialResource(_ context.Context, p *providerImpl) resource.Resource {
return APIResource[CredentialResourceModel, upapi.Credential, upapi.Credential]{
api: CredentialResourceAPI{provider: p},
mod: CredentialResourceModelAdapter{},
meta: APIResourceMetadata{
TypeNameSuffix: "credential",
Schema: schema.Schema{
Attributes: map[string]schema.Attribute{
"id": IDSchemaAttribute(),
"display_name": schema.StringAttribute{
Required: true,
},
"description": schema.StringAttribute{
Computed: true,
Optional: true,
},
"credential_type": schema.StringAttribute{
Required: true,
Validators: []validator.String{
OneOfStringValidator([]string{"BASIC", "CERTIFICATE", "TOKEN"}),
},
},
"username": schema.StringAttribute{
Computed: true,
Optional: true,
},
"secret": schema.SingleNestedAttribute{
Required: true,
Attributes: map[string]schema.Attribute{
"certificate": schema.StringAttribute{
Computed: true,
Optional: true,
Sensitive: true,
},
"key": schema.StringAttribute{
Computed: true,
Optional: true,
Sensitive: true,
},
"password": schema.StringAttribute{
Computed: true,
Optional: true,
Sensitive: true,
},
"passphrase": schema.StringAttribute{
Computed: true,
Optional: true,
Sensitive: true,
},
"secret": schema.StringAttribute{
Computed: true,
Optional: true,
Sensitive: true,
},
},
},
},
},
ConfigValidators: func(context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{NewCredentialTypeValidator()}
},
},
}
}

type CredentialResourceModel struct {
ID types.Int64 `tfsdk:"id" ref:"PK,opt"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
CredentialType types.String `tfsdk:"credential_type"`
Username types.String `tfsdk:"username"`
Secret types.Object `tfsdk:"secret"`

secret *CredentialSecretAttribute
}

func (m CredentialResourceModel) PrimaryKey() upapi.PrimaryKey {
return upapi.PrimaryKey(m.ID.ValueInt64())
}

type CredentialSecretAttribute struct {
Certificate types.String `tfsdk:"certificate"`
Key types.String `tfsdk:"key"`
Password types.String `tfsdk:"password"`
Passphrase types.String `tfsdk:"passphrase"`
Secret types.String `tfsdk:"secret"`
}

type CredentialResourceModelAdapter struct {
SetAttributeAdapter[string]
}

func (a CredentialResourceModelAdapter) Get(ctx context.Context, sg StateGetter) (*CredentialResourceModel, diag.Diagnostics) {
model := *new(CredentialResourceModel)
diags := sg.Get(ctx, &model)
if diags.HasError() {
return nil, diags
}
model.secret, diags = a.SecretAttributeContext(ctx, model.Secret)
if diags.HasError() {
return nil, diags
}

return &model, nil
}

func (a CredentialResourceModelAdapter) ToAPIArgument(model CredentialResourceModel) (*upapi.Credential, error) {
api := upapi.Credential{
PK: model.ID.ValueInt64(),
DisplayName: model.DisplayName.ValueString(),
Description: model.Description.ValueString(),
CredentialType: model.CredentialType.ValueString(),
Username: model.Username.ValueString(),
Secret: upapi.CredentialSecret{
Certificate: model.secret.Certificate.ValueString(),
Key: model.secret.Key.ValueString(),
Password: model.secret.Password.ValueString(),
Passphrase: model.secret.Passphrase.ValueString(),
Secret: model.secret.Secret.ValueString(),
},
}
return &api, nil
}

func (a CredentialResourceModelAdapter) FromAPIResult(api upapi.Credential) (*CredentialResourceModel, error) {
model := CredentialResourceModel{
ID: types.Int64Value(api.PK),
DisplayName: types.StringValue(api.DisplayName),
Description: types.StringValue(api.Description),
CredentialType: types.StringValue(api.CredentialType),
Username: types.StringValue(api.Username),
Secret: a.SecretAttributeValue(CredentialSecretAttribute{
Certificate: types.StringValue(api.Secret.Certificate),
Key: types.StringValue(api.Secret.Key),
Passphrase: types.StringValue(api.Secret.Passphrase),
Password: types.StringValue(api.Secret.Password),
Secret: types.StringValue(api.Secret.Secret),
}),
}
return &model, nil
}

func (a CredentialResourceModelAdapter) secretAttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"certificate": types.StringType,
"key": types.StringType,
"password": types.StringType,
"passphrase": types.StringType,
"secret": types.StringType,
}
}

func (a CredentialResourceModelAdapter) secretAttributeValues(m CredentialSecretAttribute) map[string]attr.Value {
return map[string]attr.Value{
"certificate": m.Certificate,
"key": m.Key,
"password": m.Password,
"passphrase": m.Passphrase,
"secret": m.Secret,
}
}

func (a CredentialResourceModelAdapter) SecretAttributeContext(ctx context.Context, v types.Object) (*CredentialSecretAttribute, diag.Diagnostics) {
if v.IsNull() || v.IsUnknown() {
return nil, nil
}
m := new(CredentialSecretAttribute)
diags := v.As(ctx, m, basetypes.ObjectAsOptions{})
if diags.HasError() {
return nil, diags
}
return m, nil
}

func (a CredentialResourceModelAdapter) SecretAttributeValue(m CredentialSecretAttribute) types.Object {
return types.ObjectValueMust(a.secretAttributeTypes(), a.secretAttributeValues(m))
}

type CredentialResourceAPI struct {
provider *providerImpl
}

func (c CredentialResourceAPI) Create(ctx context.Context, arg upapi.Credential) (*upapi.Credential, error) {
obj, err := c.provider.api.Credentials().Create(ctx, arg)
obj.Secret = arg.Secret
return obj, err
}

func (c CredentialResourceAPI) Read(ctx context.Context, pk upapi.PrimaryKeyable) (*upapi.Credential, error) {
obj, err := c.provider.api.Credentials().Get(ctx, pk)
secret := pk.(CredentialResourceModel).secret
obj.Secret = upapi.CredentialSecret{
Certificate: secret.Certificate.ValueString(),
Key: secret.Key.ValueString(),
Passphrase: secret.Passphrase.ValueString(),
Password: secret.Password.ValueString(),
Secret: secret.Secret.ValueString(),
}
return obj, err
}

func (c CredentialResourceAPI) Update(ctx context.Context, pk upapi.PrimaryKeyable, arg upapi.Credential) (*upapi.Credential, error) {
if err := c.Delete(ctx, pk); err != nil {
return nil, err
}
return c.Create(ctx, arg)
}

func (c CredentialResourceAPI) Delete(ctx context.Context, pk upapi.PrimaryKeyable) error {
return c.provider.api.Credentials().Delete(ctx, pk)
}

type credentialTypeValidator struct{}

func NewCredentialTypeValidator() resource.ConfigValidator {
return &credentialTypeValidator{}
}

func (v *credentialTypeValidator) Description(ctx context.Context) string {
return "Validates that the credential_type field has valid values and corresponding secret fields are set correctly."
}

func (v *credentialTypeValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

func (v *credentialTypeValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var model CredentialResourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

if model.CredentialType.IsNull() || model.CredentialType.IsUnknown() {
return
}

// var a attr.Value
var secretAttr CredentialSecretAttribute
p := path.Root("secret")
diags = req.Config.GetAttribute(ctx, p, &secretAttr)
if diags.HasError() {
resp.Diagnostics = diags
return
}

switch model.CredentialType.ValueString() {
case "BASIC":
if secretAttr.Password.IsNull() {
resp.Diagnostics.AddError(
"Invalid Configuration",
"When credential_type is BASIC, the password field must be set.",
)
}
if !secretAttr.Certificate.IsNull() || !secretAttr.Key.IsNull() || !secretAttr.Passphrase.IsNull() || !secretAttr.Secret.IsNull() {
resp.Diagnostics.AddError(
"Invalid Configuration",
"When credential_type is BASIC, only the password field should be set.",
)
}
case "CERTIFICATE":
if secretAttr.Certificate.IsNull() || secretAttr.Key.IsNull() || secretAttr.Passphrase.IsNull() {
resp.Diagnostics.AddError(
"Invalid Configuration",
"When credential_type is CERTIFICATE, the certificate, key, and passphrase fields must be set.",
)
}
if !secretAttr.Password.IsNull() || !secretAttr.Secret.IsNull() {
resp.Diagnostics.AddError(
"Invalid Configuration",
"When credential_type is CERTIFICATE, only the certificate, key, and passphrase fields should be set.",
)
}
case "TOKEN":
if model.Secret.IsNull() {
resp.Diagnostics.AddError(
"Invalid Configuration",
"When credential_type is TOKEN, the secret field must be set.",
)
}
if !secretAttr.Password.IsNull() || !secretAttr.Certificate.IsNull() || !secretAttr.Key.IsNull() || !secretAttr.Passphrase.IsNull() {
resp.Diagnostics.AddError(
"Invalid Configuration",
"When credential_type is TOKEN, only the secret field should be set.",
)
}
default:
resp.Diagnostics.AddError(
"Invalid Configuration",
fmt.Sprintf("Invalid credential_type: %s", model.CredentialType.ValueString()),
)
}
}
Loading

0 comments on commit f61d69c

Please sign in to comment.