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

Add new resource random_bytes to generate an array of random bytes #272

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20230213-213454.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'resource/random_bytes: New resource that generates an array of random bytes
intended to be used as key or secret'
time: 2023-02-13T21:34:54.806043106-05:00
custom:
Issue: "272"
51 changes: 51 additions & 0 deletions docs/resources/bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "random_bytes Resource - terraform-provider-random"
subcategory: ""
description: |-
The resource random_bytes generates random bytes that are intended to be used as secret or keys.
---

# random_bytes (Resource)

The resource `random_bytes` generates random bytes that are intended to be used as secret or keys.

## Example Usage

```terraform
resource "random_bytes" "jwt_secret" {
length = 64
}

resource "azurerm_key_vault_secret" "jwt_secret" {
key_vault_id = "some-azure-key-vault-id"
name = "JwtSecret"
value = random_bytes.jwt_secret.result_base64
}
```

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

### Required

- `length` (Number) The number of bytes requested. The minimum value for length is 1.

### Optional

- `keepers` (Map of String) Arbitrary map of values that, when changed, will trigger recreation of resource. See [the main provider documentation](../index.html) for more information.

### Read-Only

- `id` (String) A static value used internally by Terraform, this should not be referenced in configurations.
- `result_base64` (String, Sensitive) The generated bytes presented in base64 string format.
- `result_hex` (String, Sensitive) The generated bytes presented in hex string format.

## Import

Import is supported using the following syntax:

```shell
# Random bytes can be imported by specifying the value as base64 string.
terraform import random_bytes.basic 8/fu3q+2DcgSJ19i0jZ5Cw==
```
2 changes: 2 additions & 0 deletions examples/resources/random_bytes/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Random bytes can be imported by specifying the value as base64 string.
terraform import random_bytes.basic 8/fu3q+2DcgSJ19i0jZ5Cw==
9 changes: 9 additions & 0 deletions examples/resources/random_bytes/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "random_bytes" "jwt_secret" {
length = 64
}

resource "azurerm_key_vault_secret" "jwt_secret" {
key_vault_id = "some-azure-key-vault-id"
name = "JwtSecret"
value = random_bytes.jwt_secret.result_base64
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (p *randomProvider) Configure(context.Context, provider.ConfigureRequest, *
func (p *randomProvider) Resources(context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewIdResource,
NewBytesResource,
NewIntegerResource,
NewPasswordResource,
NewPetResource,
Expand Down
185 changes: 185 additions & 0 deletions internal/provider/resource_bytes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package provider

import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"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/types"
"github.com/terraform-providers/terraform-provider-random/internal/diagnostics"
mapplanmodifiers "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers/map"
)

var (
_ resource.Resource = (*bytesResource)(nil)
_ resource.ResourceWithImportState = (*bytesResource)(nil)
)

func NewBytesResource() resource.Resource {
return &bytesResource{}
}

type bytesResource struct {
}

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

func (r *bytesResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = bytesSchemaV0()
}

func (r *bytesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan bytesModelV0

diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

bytes := make([]byte, plan.Length.ValueInt64())
_, err := rand.Read(bytes)
if err != nil {
resp.Diagnostics.AddError(
"Create Random bytes error",
"There was an error during random generation.\n\n"+
diagnostics.RetryMsg+
fmt.Sprintf("Original Error: %s", err),
)
return
}

u := &bytesModelV0{
ID: types.StringValue("none"),
Length: plan.Length,
ResultBase64: types.StringValue(base64.StdEncoding.EncodeToString(bytes)),
ResultHex: types.StringValue(hex.EncodeToString(bytes)),
Keepers: plan.Keepers,
}

diags = resp.State.Set(ctx, u)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Read does not need to perform any operations as the state in ReadResourceResponse is already populated.
func (r *bytesResource) Read(context.Context, resource.ReadRequest, *resource.ReadResponse) {
}

func (r *bytesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var model bytesModelV0

resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
}

// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the
// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301).
func (r *bytesResource) Delete(context.Context, resource.DeleteRequest, *resource.DeleteResponse) {
}

func (r *bytesResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
bytes, err := base64.StdEncoding.DecodeString(req.ID)
if err != nil {
resp.Diagnostics.AddError(
"Import Random bytes Error",
"There was an error during the parsing of the base64 string.\n\n"+
diagnostics.RetryMsg+
fmt.Sprintf("Original Error: %s", err),
)
return
}

var state bytesModelV0

state.ID = types.StringValue("none")
state.Length = types.Int64Value(int64(len(bytes)))
state.ResultBase64 = types.StringValue(req.ID)
state.ResultHex = types.StringValue(hex.EncodeToString(bytes))
state.Keepers = types.MapNull(types.StringType)

diags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

type bytesModelV0 struct {
ID types.String `tfsdk:"id"`
Length types.Int64 `tfsdk:"length"`
Keepers types.Map `tfsdk:"keepers"`
ResultBase64 types.String `tfsdk:"result_base64"`
ResultHex types.String `tfsdk:"result_hex"`
}

func bytesSchemaV0() schema.Schema {
return schema.Schema{
Version: 0,
Description: "The resource `random_bytes` generates random bytes that are intended to be " +
"used as secret or keys.",
Attributes: map[string]schema.Attribute{
"keepers": schema.MapAttribute{
Description: "Arbitrary map of values that, when changed, will trigger recreation of " +
"resource. See [the main provider documentation](../index.html) for more information.",
ElementType: types.StringType,
Optional: true,
PlanModifiers: []planmodifier.Map{
// mapplanmodifiers.RequiresReplaceIfValuesNotNull() has been used for consistency with other
// resources but mapplanmodifier.RequiresReplace() could have been used as there shouldn't be any
// prior state storage from a terraform-plugin-sdk based resource which would've collapsed a map
// of null values into a null map.
mapplanmodifiers.RequiresReplaceIfValuesNotNull(),
},
},
"length": schema.Int64Attribute{
Description: "The number of bytes requested. The minimum value for length is 1.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
Validators: []validator.Int64{
int64validator.AtLeast(1),
},
},
"result_base64": schema.StringAttribute{
Description: "The generated bytes presented in base64 string format.",
Computed: true,
Sensitive: true,
Socolin marked this conversation as resolved.
Show resolved Hide resolved
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"result_hex": schema.StringAttribute{
Description: "The generated bytes presented in hex string format.",
Computed: true,
Sensitive: true,
Socolin marked this conversation as resolved.
Show resolved Hide resolved
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"id": schema.StringAttribute{
Description: "A static value used internally by Terraform, this should not be referenced in configurations.",
Computed: true,
Socolin marked this conversation as resolved.
Show resolved Hide resolved
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
Loading