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 random_bytes resource #494

Merged
merged 22 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
17cf9c9
Add new resource `random_bytes` to generate an array of random bytes …
Socolin Nov 30, 2022
8db8cef
Remove entry from CHANGELOG and use changie instead
Socolin Feb 14, 2023
3150651
Fix review comments
Socolin Feb 14, 2023
6f9330b
Adjust random_bytes to latest sdk version
Socolin Feb 14, 2023
b71def7
Add test to validate random_bytes it checking length
Socolin Feb 14, 2023
38e2238
Add test to validate change of length in random_bytes
Socolin Feb 14, 2023
9bec542
Remove unused link in change
Socolin Feb 14, 2023
c9f46f3
Add tests for keepers of random_bytes
Socolin Feb 14, 2023
79cbf0d
Fix discrepancy between the plan and the state in random_bytes during…
Socolin Feb 15, 2023
677f0be
Run make generate to update random_bytes doc
Socolin Feb 15, 2023
60fc8d5
Add comment explaining why random_bytes is using mapplanmodifiers.Req…
Socolin Feb 27, 2023
afb2548
Remove outdated comment
Socolin Mar 6, 2023
dce2301
Add test to validate that after import the resource does not changes.
Socolin Mar 6, 2023
4c438b2
Fix TestAccResourceBytes_ImportWithoutKeepersThenUpdateShouldNotTrigg…
Socolin Mar 13, 2023
67cdbbb
Fix acceptance test of random_bytes after rebase
Socolin Mar 13, 2023
0bebee0
Renaming result_base64 to base64, and result_hex to hex
bendbennett Nov 30, 2023
db8065c
Remove id from schema
bendbennett Nov 30, 2023
2ed443c
Updating docs
bendbennett Nov 30, 2023
ca011cc
Merge branch 'main' into random-bytes
bendbennett Nov 30, 2023
87c0abe
Quoting import value for random_bytes example
bendbennett Dec 1, 2023
8acd2b1
Using mapplanmodifier.RequiresReplace() in place of mapplanmodifiers.…
bendbennett Dec 1, 2023
d26195d
Remove redundant tests
bendbennett Dec 1, 2023
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"
50 changes: 50 additions & 0 deletions docs/resources/bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
# 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 a secret, or key. Use this in preference to random_id when the output is considered sensitive, and should not be displayed in the CLI.
---

# random_bytes (Resource)

The resource `random_bytes` generates random bytes that are intended to be used as a secret, or key. Use this in preference to `random_id` when the output is considered sensitive, and should not be displayed in the CLI.

## 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.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

- `base64` (String, Sensitive) The generated bytes presented in base64 string format.
- `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==
```
8 changes: 6 additions & 2 deletions docs/resources/id.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ page_title: "random_id Resource - terraform-provider-random"
subcategory: ""
description: |-
The resource random_id generates random numbers that are intended to be
used as unique identifiers for other resources.
used as unique identifiers for other resources. If the output is considered
sensitive, and should not be displayed in the CLI, use random_bytes
instead.
This resource does use a cryptographic random number generator in order
to minimize the chance of collisions, making the results of this resource
when a 16-byte identifier is requested of equivalent uniqueness to a
Expand All @@ -18,7 +20,9 @@ description: |-
# random_id (Resource)

The resource `random_id` generates random numbers that are intended to be
used as unique identifiers for other resources.
used as unique identifiers for other resources. If the output is considered
sensitive, and should not be displayed in the CLI, use `random_bytes`
instead.

This resource *does* use a cryptographic random number generator in order
to minimize the chance of collisions, making the results of this resource
Expand Down
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==
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Not sure if we should quote the example value since some shells may try to treat the base64 character set as special. 👍

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.base64
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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
181 changes: 181 additions & 0 deletions internal/provider/resource_bytes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

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{
Length: plan.Length,
Base64: types.StringValue(base64.StdEncoding.EncodeToString(bytes)),
Hex: 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.Length = types.Int64Value(int64(len(bytes)))
state.Base64 = types.StringValue(req.ID)
state.Hex = 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 {
Length types.Int64 `tfsdk:"length"`
Keepers types.Map `tfsdk:"keepers"`
Base64 types.String `tfsdk:"base64"`
Hex types.String `tfsdk:"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 a secret, or key. Use this in preference to `random_id` when the output is " +
"considered sensitive, and should not be displayed in the CLI.",
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(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Open question: I wonder if we should try to save the technical debt in this new implementation by using the regular validator here since there was never any prior state from terraform-plugin-sdk? I'm personally not worried about code-level consistency for this situation in new implementations. We can always "downgrade" to the special validator (or consider updating upstream) if there are real behavior issues with the regular validator.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense to me. I've replaced mapplanmodifiers.RequiresReplaceIfValuesNotNull() with mapplanmodifier.RequiresReplace(), and removed the redundant comment.

},
},
"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),
},
},
"base64": schema.StringAttribute{
Description: "The generated bytes presented in base64 string format.",
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"hex": schema.StringAttribute{
Description: "The generated bytes presented in hex string format.",
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
Loading
Loading