internal/fleet/integration_policy/secrets.go (141 lines of code) (raw):
package integration_policy
import (
"context"
"encoding/json"
"github.com/elastic/terraform-provider-elasticstack/generated/kbapi"
"github.com/elastic/terraform-provider-elasticstack/internal/utils"
"github.com/hashicorp/terraform-plugin-framework/diag"
)
// The secret store is a map of policy secret reference IDs to the
// original value at time of creation. By replacing the ref when
// marshaling the state back to Terraform, we can prevent resource
// drift.
type secretStore map[string]any
// newSecretStore creates a new secretStore from the resource privateData.
// If the store already exists, it is filtered by any references in the resp policy.
func newSecretStore(ctx context.Context, resp *kbapi.PackagePolicy, private privateData) (store secretStore, diags diag.Diagnostics) {
bytes, nd := private.GetKey(ctx, "secrets")
diags.Append(nd...)
if diags.HasError() {
return
}
if len(bytes) == 0 {
store = secretStore{}
return
}
err := json.Unmarshal(bytes, &store)
if err != nil {
diags.AddError("could not unmarshal secret store", err.Error())
return
}
// Remove any saved secret refs not present in the API response.
refs := make(map[string]any)
for _, r := range utils.Deref(resp.SecretReferences) {
refs[r.Id] = nil
}
for id := range store {
if _, ok := refs[id]; !ok {
delete(store, id)
}
}
return
}
// Save marshals the secretStore back to the provider.
func (s secretStore) Save(ctx context.Context, private privateData) (diags diag.Diagnostics) {
bytes, err := json.Marshal(s)
if err != nil {
diags.AddError("could not marshal secret store", err.Error())
return
}
return private.SetKey(ctx, "secrets", bytes)
}
// HandleRespSecrets extracts the wrapped value from each response var, then
// replaces any secret refs with the original value from secrets if available.
func HandleRespSecrets(ctx context.Context, resp *kbapi.PackagePolicy, private privateData) (diags diag.Diagnostics) {
secrets, nd := newSecretStore(ctx, resp, private)
diags.Append(nd...)
if diags.HasError() {
return
}
handleVar := func(key string, mval map[string]any, vars map[string]any) {
refID := mval["id"].(string)
if original, ok := secrets[refID]; ok {
vars[key] = original
}
}
handleVars := func(vars map[string]any) {
for key, val := range vars {
if mval, ok := val.(map[string]any); ok {
if wrapped, ok := mval["value"]; ok {
vars[key] = wrapped
val = wrapped
} else if v, ok := mval["isSecretRef"]; ok && v == true {
handleVar(key, mval, vars)
} else {
// Don't keep null (missing) values
delete(vars, key)
continue
}
if mval, ok := val.(map[string]any); ok {
if v, ok := mval["isSecretRef"]; ok && v == true {
handleVar(key, mval, vars)
}
}
}
}
}
handleVars(utils.Deref(resp.Vars))
for _, input := range resp.Inputs {
handleVars(utils.Deref(input.Vars))
for _, stream := range utils.Deref(input.Streams) {
handleVars(utils.Deref(stream.Vars))
}
}
nd = secrets.Save(ctx, private)
diags.Append(nd...)
return
}
// HandleReqRespSecrets extracts the wrapped value from each response var, then
// maps any secret refs to the original request value.
func HandleReqRespSecrets(ctx context.Context, req kbapi.PackagePolicyRequest, resp *kbapi.PackagePolicy, private privateData) (diags diag.Diagnostics) {
secrets, nd := newSecretStore(ctx, resp, private)
diags.Append(nd...)
if diags.HasError() {
return
}
handleVar := func(key string, mval map[string]any, reqVars map[string]any, respVars map[string]any) {
if v, ok := mval["isSecretRef"]; ok && v == true {
original := reqVars[key]
respVars[key] = original
// Is the original also a secret ref?
// This should only show up during importing and pre 0.11.7 migration.
if moriginal, ok := original.(map[string]any); ok {
if v, ok := moriginal["isSecretRef"]; ok && v == true {
return
}
}
refID := mval["id"].(string)
secrets[refID] = original
}
}
handleVars := func(reqVars map[string]any, respVars map[string]any) {
for key, val := range respVars {
if mval, ok := val.(map[string]any); ok {
if wrapped, ok := mval["value"]; ok {
respVars[key] = wrapped
val = wrapped
} else if v, ok := mval["isSecretRef"]; ok && v == true {
handleVar(key, mval, reqVars, respVars)
} else {
// Don't keep null (missing) values
delete(respVars, key)
continue
}
if mval, ok := val.(map[string]any); ok {
handleVar(key, mval, reqVars, respVars)
}
}
}
}
handleVars(utils.Deref(req.Vars), utils.Deref(resp.Vars))
for inputID, inputReq := range utils.Deref(req.Inputs) {
inputResp := resp.Inputs[inputID]
handleVars(utils.Deref(inputReq.Vars), utils.Deref(inputResp.Vars))
streamsResp := utils.Deref(inputResp.Streams)
for streamID, streamReq := range utils.Deref(inputReq.Streams) {
streamResp := streamsResp[streamID]
handleVars(utils.Deref(streamReq.Vars), utils.Deref(streamResp.Vars))
}
}
nd = secrets.Save(ctx, private)
diags.Append(nd...)
return
}
// Equivalent to privatestate.ProviderData
type privateData interface {
// GetKey returns the private state data associated with the given key.
//
// If the key is reserved for framework usage, an error diagnostic
// is returned. If the key is valid, but private state data is not found,
// nil is returned.
//
// The naming of keys only matters in context of a single resource,
// however care should be taken that any historical keys are not reused
// without accounting for older resource instances that may still have
// older data at the key.
GetKey(ctx context.Context, key string) ([]byte, diag.Diagnostics)
// SetKey sets the private state data at the given key.
//
// If the key is reserved for framework usage, an error diagnostic
// is returned. The data must be valid JSON and UTF-8 safe or an error
// diagnostic is returned.
//
// The naming of keys only matters in context of a single resource,
// however care should be taken that any historical keys are not reused
// without accounting for older resource instances that may still have
// older data at the key.
SetKey(ctx context.Context, key string, value []byte) diag.Diagnostics
}