blob: 9ad4bfa7bad42bb389d04d47ddf8e77cd2b23c49 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
"strings"
"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/types"
"github.com/terraform-providers/terraform-provider-random/internal/diagnostics"
mapplanmodifiers "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers/map"
)
var (
_ resource.Resource = (*idResource)(nil)
_ resource.ResourceWithImportState = (*idResource)(nil)
)
func NewIdResource() resource.Resource {
return &idResource{}
}
type idResource struct{}
func (r *idResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_id"
}
func (r *idResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: `
The resource ` + "`random_id`" + ` generates random numbers that are intended to be
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
type-4 UUID.
This resource can be used in conjunction with resources that have
the ` + "`create_before_destroy`" + ` lifecycle flag set to avoid conflicts with
unique names during the brief period where both the old and new resources
exist concurrently.
`,
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(),
},
},
"byte_length": schema.Int64Attribute{
Description: "The number of random bytes to produce. The minimum value is 1, which produces " +
"eight bits of randomness.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"prefix": schema.StringAttribute{
Description: "Arbitrary string to prefix the output value with. This string is supplied as-is, " +
"meaning it is not guaranteed to be URL-safe or base64 encoded.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"b64_url": schema.StringAttribute{
Description: "The generated id presented in base64, using the URL-friendly character set: " +
"case-sensitive letters, digits and the characters `_` and `-`.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"b64_std": schema.StringAttribute{
Description: "The generated id presented in base64 without additional transformations.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"hex": schema.StringAttribute{
Description: "The generated id presented in padded hexadecimal digits. This result will " +
"always be twice as long as the requested byte length.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"dec": schema.StringAttribute{
Description: "The generated id presented in non-padded decimal digits.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"id": schema.StringAttribute{
Description: "The generated id presented in base64 without additional transformations or prefix.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *idResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan idModelV0
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
byteLength := plan.ByteLength.ValueInt64()
bytes := make([]byte, byteLength)
n, err := rand.Reader.Read(bytes)
if int64(n) != byteLength {
resp.Diagnostics.Append(diagnostics.RandomnessGenerationError(err.Error())...)
return
}
if err != nil {
resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...)
return
}
id := base64.RawURLEncoding.EncodeToString(bytes)
prefix := plan.Prefix.ValueString()
b64Std := base64.StdEncoding.EncodeToString(bytes)
hexStr := hex.EncodeToString(bytes)
bigInt := big.Int{}
bigInt.SetBytes(bytes)
dec := bigInt.String()
i := idModelV0{
ID: types.StringValue(id),
Keepers: plan.Keepers,
ByteLength: types.Int64Value(plan.ByteLength.ValueInt64()),
Prefix: plan.Prefix,
B64URL: types.StringValue(prefix + id),
B64Std: types.StringValue(prefix + b64Std),
Hex: types.StringValue(prefix + hexStr),
Dec: types.StringValue(prefix + dec),
}
diags = resp.State.Set(ctx, i)
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 *idResource) Read(context.Context, resource.ReadRequest, *resource.ReadResponse) {
}
// Update ensures the plan value is copied to the state to complete the update.
func (r *idResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var model idModelV0
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 *idResource) Delete(context.Context, resource.DeleteRequest, *resource.DeleteResponse) {
}
func (r *idResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id := req.ID
var prefix string
sep := strings.LastIndex(id, ",")
if sep != -1 {
prefix = id[:sep]
id = id[sep+1:]
}
bytes, err := base64.RawURLEncoding.DecodeString(id)
if err != nil {
resp.Diagnostics.AddError(
"Import Random ID Error",
"While attempting to import a random id there was a decoding error.\n\n+"+
diagnostics.RetryMsg+
fmt.Sprintf("Original Error: %s", err),
)
return
}
b64Std := base64.StdEncoding.EncodeToString(bytes)
hexStr := hex.EncodeToString(bytes)
bigInt := big.Int{}
bigInt.SetBytes(bytes)
dec := bigInt.String()
var state idModelV0
state.ID = types.StringValue(id)
state.ByteLength = types.Int64Value(int64(len(bytes)))
// Using types.MapValueMust to ensure map is known.
state.Keepers = types.MapValueMust(types.StringType, nil)
state.B64Std = types.StringValue(prefix + b64Std)
state.B64URL = types.StringValue(prefix + id)
state.Hex = types.StringValue(prefix + hexStr)
state.Dec = types.StringValue(prefix + dec)
if prefix == "" {
state.Prefix = types.StringNull()
} else {
state.Prefix = types.StringValue(prefix)
}
diags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
type idModelV0 struct {
ID types.String `tfsdk:"id"`
Keepers types.Map `tfsdk:"keepers"`
ByteLength types.Int64 `tfsdk:"byte_length"`
Prefix types.String `tfsdk:"prefix"`
B64URL types.String `tfsdk:"b64_url"`
B64Std types.String `tfsdk:"b64_std"`
Hex types.String `tfsdk:"hex"`
Dec types.String `tfsdk:"dec"`
}