-
Notifications
You must be signed in to change notification settings - Fork 118
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
Changes from 19 commits
17cf9c9
8db8cef
3150651
6f9330b
b71def7
38e2238
9bec542
c9f46f3
79cbf0d
677f0be
60fc8d5
afb2548
dce2301
4c438b2
67cdbbb
0bebee0
db8065c
2ed443c
ca011cc
87c0abe
8acd2b1
d26195d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" |
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== | ||
``` |
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== | ||
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 | ||
} |
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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense to me. I've replaced |
||
}, | ||
}, | ||
"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(), | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
There was a problem hiding this comment.
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. 👍