-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Port activation flags with dynamic registration (#29237)
- Loading branch information
1 parent
357b294
commit ab4e8da
Showing
8 changed files
with
394 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:improvement | ||
core: Add activation flags. A mechanism for users to opt in to new functionality at a convenient time. Previously used only in Enterprise for SecretSync, activation flags are now available in CE for future features to use. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package activationflags | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"maps" | ||
"sync" | ||
|
||
"github.com/hashicorp/vault/sdk/logical" | ||
) | ||
|
||
const ( | ||
storagePathActivationFlags = "activation-flags" | ||
) | ||
|
||
type FeatureActivationFlags struct { | ||
activationFlagsLock sync.RWMutex | ||
storage logical.Storage | ||
activationFlags map[string]bool | ||
} | ||
|
||
func NewFeatureActivationFlags() *FeatureActivationFlags { | ||
return &FeatureActivationFlags{ | ||
activationFlags: map[string]bool{}, | ||
} | ||
} | ||
|
||
func (f *FeatureActivationFlags) Initialize(ctx context.Context, storage logical.Storage) error { | ||
f.activationFlagsLock.Lock() | ||
defer f.activationFlagsLock.Unlock() | ||
|
||
if storage == nil { | ||
return fmt.Errorf("unable to access storage") | ||
} | ||
|
||
f.storage = storage | ||
|
||
entry, err := f.storage.Get(ctx, storagePathActivationFlags) | ||
if err != nil { | ||
return fmt.Errorf("failed to get activation flags from storage: %w", err) | ||
} | ||
if entry == nil { | ||
f.activationFlags = map[string]bool{} | ||
return nil | ||
} | ||
|
||
var activationFlags map[string]bool | ||
if err := entry.DecodeJSON(&activationFlags); err != nil { | ||
return fmt.Errorf("failed to decode activation flags from storage: %w", err) | ||
} | ||
|
||
f.activationFlags = activationFlags | ||
|
||
return nil | ||
} | ||
|
||
// Get is the helper function called by the activation-flags API read endpoint. This reads the | ||
// actual values from storage, then updates the in-memory cache of the activation-flags. It | ||
// returns a slice of the feature names which have already been activated. | ||
func (f *FeatureActivationFlags) Get(ctx context.Context) ([]string, error) { | ||
f.activationFlagsLock.Lock() | ||
defer f.activationFlagsLock.Unlock() | ||
|
||
// Don't use nil slice declaration, we want the JSON to show "[]" instead of null | ||
activated := []string{} | ||
|
||
if f.storage == nil { | ||
return activated, nil | ||
} | ||
|
||
entry, err := f.storage.Get(ctx, storagePathActivationFlags) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get activation flags from storage: %w", err) | ||
} | ||
if entry == nil { | ||
return activated, nil | ||
} | ||
|
||
var activationFlags map[string]bool | ||
if err := entry.DecodeJSON(&activationFlags); err != nil { | ||
return nil, fmt.Errorf("failed to decode activation flags from storage: %w", err) | ||
} | ||
|
||
// Update the in-memory flags after loading the latest values from storage | ||
f.activationFlags = activationFlags | ||
|
||
for flag, set := range activationFlags { | ||
if set { | ||
activated = append(activated, flag) | ||
} | ||
} | ||
|
||
return activated, nil | ||
} | ||
|
||
// Write is the helper function called by the activation-flags API write endpoint. This stores | ||
// the boolean value for the activation-flag feature name into Vault storage across the cluster | ||
// and updates the in-memory cache upon success. | ||
func (f *FeatureActivationFlags) Write(ctx context.Context, featureName string, activate bool) (err error) { | ||
f.activationFlagsLock.Lock() | ||
defer f.activationFlagsLock.Unlock() | ||
|
||
if f.storage == nil { | ||
return fmt.Errorf("unable to access storage") | ||
} | ||
|
||
activationFlags := f.activationFlags | ||
|
||
clonedFlags := maps.Clone(f.activationFlags) | ||
clonedFlags[featureName] = activate | ||
// The cloned flags are updated but the in-memory state is only updated on success of the storage update. | ||
defer func() { | ||
if err == nil { | ||
activationFlags[featureName] = activate | ||
} | ||
}() | ||
|
||
entry, err := logical.StorageEntryJSON(storagePathActivationFlags, clonedFlags) | ||
if err != nil { | ||
return fmt.Errorf("failed to marshal object to JSON: %w", err) | ||
} | ||
|
||
err = f.storage.Put(ctx, entry) | ||
if err != nil { | ||
return fmt.Errorf("failed to save object in storage: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// IsActivationFlagEnabled is true if the specified flag is enabled in the core. | ||
func (f *FeatureActivationFlags) IsActivationFlagEnabled(featureName string) bool { | ||
f.activationFlagsLock.RLock() | ||
defer f.activationFlagsLock.RUnlock() | ||
|
||
activated, ok := f.activationFlags[featureName] | ||
|
||
return ok && activated | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package vault | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"slices" | ||
"strings" | ||
|
||
"github.com/hashicorp/vault/sdk/framework" | ||
"github.com/hashicorp/vault/sdk/logical" | ||
) | ||
|
||
const ( | ||
paramFeatureName = "feature_name" | ||
descFeatureName = "The name of the feature to be activated." | ||
summaryList = "Returns the available and activated activation-flagged features." | ||
summaryUpdate = "Activate a flagged feature." | ||
|
||
prefixActivationFlags = "activation-flags" | ||
verbActivationFlagsActivate = "activate" | ||
verbActivationFlagsDeactivate = "deactivate" | ||
|
||
fieldActivated = "activated" | ||
fieldUnactivated = "unactivated" | ||
|
||
helpSynopsis = "Returns information about Vault's features that require a one-time activation step." | ||
helpDescription = ` | ||
This path responds to the following HTTP methods. | ||
GET / | ||
Returns the available and activated activation-flags. | ||
PUT|POST /<feature-name>/activate | ||
Activates the specified feature. Cannot be undone.` | ||
) | ||
|
||
// Register CRUD functions dynamically. | ||
// These variables should only be mutated during initialization or server construction. | ||
// It is unsafe to modify them once the Vault core is running. | ||
var ( | ||
readActivationFlag = func(ctx context.Context, b *SystemBackend, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { | ||
return b.readActivationFlag(ctx, req, fd) | ||
} | ||
|
||
writeActivationFlag = func(ctx context.Context, b *SystemBackend, req *logical.Request, fd *framework.FieldData, isActivate bool) (*logical.Response, error) { | ||
return b.writeActivationFlagWrite(ctx, req, fd, isActivate) | ||
} | ||
) | ||
|
||
func (b *SystemBackend) activationFlagsPaths() []*framework.Path { | ||
return []*framework.Path{ | ||
{ | ||
Pattern: fmt.Sprintf("%s$", prefixActivationFlags), | ||
DisplayAttrs: &framework.DisplayAttributes{ | ||
OperationVerb: "read", | ||
OperationSuffix: prefixActivationFlags, | ||
}, | ||
Operations: map[logical.Operation]framework.OperationHandler{ | ||
logical.ReadOperation: &framework.PathOperation{ | ||
Callback: b.handleActivationFlagRead, | ||
Summary: summaryList, | ||
}, | ||
}, | ||
HelpSynopsis: helpSynopsis, | ||
HelpDescription: helpDescription, | ||
}, | ||
{ | ||
Pattern: fmt.Sprintf("%s/%s/%s", prefixActivationFlags, "activation-test", verbActivationFlagsActivate), | ||
DisplayAttrs: &framework.DisplayAttributes{ | ||
OperationPrefix: prefixActivationFlags, | ||
OperationVerb: verbActivationFlagsActivate, | ||
}, | ||
Operations: map[logical.Operation]framework.OperationHandler{ | ||
logical.UpdateOperation: &framework.PathOperation{ | ||
Callback: b.handleActivationFlagsActivate, | ||
ForwardPerformanceSecondary: true, | ||
ForwardPerformanceStandby: true, | ||
Summary: summaryUpdate, | ||
}, | ||
}, | ||
HelpSynopsis: helpSynopsis, | ||
HelpDescription: helpDescription, | ||
}, | ||
} | ||
} | ||
|
||
func (b *SystemBackend) handleActivationFlagRead(ctx context.Context, req *logical.Request, fd *framework.FieldData) (*logical.Response, error) { | ||
return readActivationFlag(ctx, b, req, fd) | ||
} | ||
|
||
func (b *SystemBackend) handleActivationFlagsActivate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||
return writeActivationFlag(ctx, b, req, data, true) | ||
} | ||
|
||
func (b *SystemBackend) readActivationFlag(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { | ||
activationFlags, err := b.Core.FeatureActivationFlags.Get(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return b.activationFlagsToResponse(activationFlags), nil | ||
} | ||
|
||
func (b *SystemBackend) writeActivationFlagWrite(ctx context.Context, req *logical.Request, _ *framework.FieldData, isActivate bool) (*logical.Response, error) { | ||
// We need to manually parse out the feature_name from the path because we can't use FieldSchema parameters | ||
// in the path to make generic endpoints. We need each activation-flag path to be a separate endpoint. | ||
// Path starts out as activation-flags/<feature_name>/verb | ||
// Removes activation-flags/ from the path | ||
trimPrefix := strings.TrimPrefix(req.Path, prefixActivationFlags+"/") | ||
// Removes /verb from the path | ||
featureName := trimPrefix[:strings.LastIndex(trimPrefix, "/")] | ||
|
||
err := b.Core.FeatureActivationFlags.Write(ctx, featureName, isActivate) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to write new activation flags: %w", err) | ||
} | ||
|
||
// We read back the value after writing it to storage so that we can try forcing a cache update right away. | ||
// If this fails, it's still okay to proceed as the write has been successful and the cache will get updated | ||
// at the time of an endpoint getting called. However, we can only return the one feature name we just activated | ||
// in the response since the read to retrieve any others did not succeed. | ||
activationFlags, err := b.Core.FeatureActivationFlags.Get(ctx) | ||
if err != nil { | ||
resp := b.activationFlagsToResponse([]string{featureName}) | ||
return resp, fmt.Errorf("failed to read activation-flags back after write: %w", err) | ||
} | ||
|
||
return b.activationFlagsToResponse(activationFlags), nil | ||
} | ||
|
||
func (b *SystemBackend) activationFlagsToResponse(activationFlags []string) *logical.Response { | ||
slices.Sort(activationFlags) | ||
return &logical.Response{ | ||
Data: map[string]interface{}{ | ||
fieldActivated: activationFlags, | ||
}, | ||
} | ||
} |
Oops, something went wrong.