Add sys/version-history endpoint and associated command (#13766)
* store version history as utc; add self-heal logic * add sys/version-history endpoint * change version history from GET to LIST, require auth * add "vault version-history" CLI command * add vault-version CLI error message for version string parsing * adding version-history API and CLI docs * add changelog entry * some version-history command fixes * remove extraneous cmd args * fix version-history command help text * specify in docs that endpoint was added in 1.10.0 Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * enforce UTC within storeVersionTimestamp directly * fix improper use of %w in logger.Warn * remove extra err check and erroneous return from loadVersionTimestamps * add >= 1.10.0 warning to version-history cmd * move sys/version-history tests Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com>
This commit is contained in:
parent
0712ef13fc
commit
f9e9b4d327
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
core: Add support to list version history via API at `sys/version-history` and via CLI with `vault version-history`
|
||||
```
|
|
@ -653,6 +653,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
|||
BaseCommand: getBaseCommand(),
|
||||
}, nil
|
||||
},
|
||||
"version-history": func() (cli.Command, error) {
|
||||
return &VersionHistoryCommand{
|
||||
BaseCommand: getBaseCommand(),
|
||||
}, nil
|
||||
},
|
||||
"write": func() (cli.Command, error) {
|
||||
return &WriteCommand{
|
||||
BaseCommand: getBaseCommand(),
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/ryanuber/columnize"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*VersionHistoryCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*VersionHistoryCommand)(nil)
|
||||
)
|
||||
|
||||
// VersionHistoryCommand is a Command implementation prints the version.
|
||||
type VersionHistoryCommand struct {
|
||||
*BaseCommand
|
||||
}
|
||||
|
||||
func (c *VersionHistoryCommand) Synopsis() string {
|
||||
return "Prints the version history of the target Vault server"
|
||||
}
|
||||
|
||||
func (c *VersionHistoryCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault version-history
|
||||
|
||||
Prints the version history of the target Vault server.
|
||||
|
||||
Print the version history:
|
||||
|
||||
$ vault version-history
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *VersionHistoryCommand) Flags() *FlagSets {
|
||||
return c.flagSet(FlagSetOutputFormat)
|
||||
}
|
||||
|
||||
func (c *VersionHistoryCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *VersionHistoryCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
const versionTrackingWarning = `Note:
|
||||
Use of this command requires a server running Vault 1.10.0 or greater.
|
||||
Version tracking was added in 1.9.0. Earlier versions have not been tracked.
|
||||
`
|
||||
|
||||
func (c *VersionHistoryCommand) Run(args []string) int {
|
||||
f := c.Flags()
|
||||
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
resp, err := client.Logical().List("sys/version-history")
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error reading version history: %s", err))
|
||||
return 2
|
||||
}
|
||||
|
||||
if resp == nil || resp.Data == nil {
|
||||
c.UI.Error("Invalid response returned from Vault")
|
||||
return 2
|
||||
}
|
||||
|
||||
if c.flagFormat == "json" {
|
||||
c.UI.Warn("")
|
||||
c.UI.Warn(versionTrackingWarning)
|
||||
c.UI.Warn("")
|
||||
|
||||
return OutputData(c.UI, resp)
|
||||
}
|
||||
|
||||
var keyInfo map[string]interface{}
|
||||
|
||||
keys, ok := extractListData(resp)
|
||||
if !ok {
|
||||
c.UI.Error("Expected keys in response to be an array")
|
||||
return 2
|
||||
}
|
||||
|
||||
keyInfo, ok = resp.Data["key_info"].(map[string]interface{})
|
||||
if !ok {
|
||||
c.UI.Error("Expected key_info in response to be a map")
|
||||
return 2
|
||||
}
|
||||
|
||||
table := []string{"Version | Installation Time"}
|
||||
columnConfig := columnize.DefaultConfig()
|
||||
|
||||
for _, versionRaw := range keys {
|
||||
version, ok := versionRaw.(string)
|
||||
|
||||
if !ok {
|
||||
c.UI.Error("Expected version to be string")
|
||||
return 2
|
||||
}
|
||||
|
||||
versionInfoRaw := keyInfo[version]
|
||||
|
||||
versionInfo, ok := versionInfoRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
c.UI.Error(fmt.Sprintf("Expected version info for %q to be map", version))
|
||||
return 2
|
||||
}
|
||||
|
||||
table = append(table, fmt.Sprintf("%s | %s", version, versionInfo["timestamp_installed"]))
|
||||
}
|
||||
|
||||
c.UI.Warn("")
|
||||
c.UI.Warn(versionTrackingWarning)
|
||||
c.UI.Warn("")
|
||||
c.UI.Output(tableOutput(table, columnConfig))
|
||||
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/version"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func testVersionHistoryCommand(tb testing.TB) (*cli.MockUi, *VersionHistoryCommand) {
|
||||
tb.Helper()
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
return ui, &VersionHistoryCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
UI: ui,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionHistoryCommand_TableOutput(t *testing.T) {
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
ui, cmd := testVersionHistoryCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run([]string{})
|
||||
|
||||
if expectedCode := 0; code != expectedCode {
|
||||
t.Fatalf("expected %d to be %d: %s", code, expectedCode, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
if errorString := ui.ErrorWriter.String(); !strings.Contains(errorString, versionTrackingWarning) {
|
||||
t.Errorf("expected %q to contain %q", errorString, versionTrackingWarning)
|
||||
}
|
||||
|
||||
output := ui.OutputWriter.String()
|
||||
|
||||
if !strings.Contains(output, version.Version) {
|
||||
t.Errorf("expected %q to contain version %q", output, version.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionHistoryCommand_JsonOutput(t *testing.T) {
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
runOpts := &RunOptions{
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
Client: client,
|
||||
}
|
||||
|
||||
args, format, _ := setupEnv([]string{"version-history", "-format", "json"})
|
||||
if format != "json" {
|
||||
t.Fatalf("expected format to be %q, actual %q", "json", format)
|
||||
}
|
||||
|
||||
code := RunCustom(args, runOpts)
|
||||
|
||||
if expectedCode := 0; code != expectedCode {
|
||||
t.Fatalf("expected %d to be %d: %s", code, expectedCode, stderr.String())
|
||||
}
|
||||
|
||||
if stderrString := stderr.String(); !strings.Contains(stderrString, versionTrackingWarning) {
|
||||
t.Errorf("expected %q to contain %q", stderrString, versionTrackingWarning)
|
||||
}
|
||||
|
||||
stdoutBytes := stdout.Bytes()
|
||||
|
||||
if !json.Valid(stdoutBytes) {
|
||||
t.Fatalf("expected output %q to be valid JSON", stdoutBytes)
|
||||
}
|
||||
|
||||
var versionHistoryResp map[string]interface{}
|
||||
err := json.Unmarshal(stdoutBytes, &versionHistoryResp)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal json from STDOUT, err: %s", err.Error())
|
||||
}
|
||||
|
||||
var respData map[string]interface{}
|
||||
var ok bool
|
||||
var keys []interface{}
|
||||
var keyInfo map[string]interface{}
|
||||
|
||||
if respData, ok = versionHistoryResp["data"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("expected data key to be map, actual: %#v", versionHistoryResp["data"])
|
||||
}
|
||||
|
||||
if keys, ok = respData["keys"].([]interface{}); !ok {
|
||||
t.Fatalf("expected keys to be array, actual: %#v", respData["keys"])
|
||||
}
|
||||
|
||||
if keyInfo, ok = respData["key_info"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("expected key_info to be map, actual: %#v", respData["key_info"])
|
||||
}
|
||||
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("expected single version history entry for %q", version.Version)
|
||||
}
|
||||
|
||||
if keyInfo[version.Version] == nil {
|
||||
t.Fatalf("expected version %s to be present in key_info, actual: %#v", version.Version, keyInfo)
|
||||
}
|
||||
}
|
|
@ -1070,8 +1070,8 @@ func NewCore(conf *CoreConfig) (*Core, error) {
|
|||
// handleVersionTimeStamps stores the current version at the current time to
|
||||
// storage, and then loads all versions and upgrade timestamps out from storage.
|
||||
func (c *Core) handleVersionTimeStamps(ctx context.Context) error {
|
||||
currentTime := time.Now()
|
||||
isUpdated, err := c.storeVersionTimestamp(ctx, version.Version, currentTime)
|
||||
currentTime := time.Now().UTC()
|
||||
isUpdated, err := c.storeVersionTimestamp(ctx, version.Version, currentTime, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error storing vault version: %w", err)
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/version"
|
||||
)
|
||||
|
||||
// TestStoreMultipleVaultVersions writes multiple versions of 1.9.0 and verifies that only
|
||||
// the original timestamp is stored.
|
||||
func TestStoreMultipleVaultVersions(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
upgradeTimePlusEpsilon := time.Now()
|
||||
wasStored, err := c.storeVersionTimestamp(context.Background(), version.Version, upgradeTimePlusEpsilon.Add(30*time.Hour))
|
||||
if err != nil || wasStored {
|
||||
t.Fatalf("vault version was re-stored: %v, err is: %s", wasStored, err.Error())
|
||||
}
|
||||
upgradeTime, ok := c.versionTimestamps[version.Version]
|
||||
if !ok {
|
||||
t.Fatalf("no %s version timestamp found", version.Version)
|
||||
}
|
||||
if upgradeTime.After(upgradeTimePlusEpsilon) {
|
||||
t.Fatalf("upgrade time for %s is incorrect: got %+v, expected less than %+v", version.Version, upgradeTime, upgradeTimePlusEpsilon)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetOldestVersion verifies that FindOldestVersionTimestamp finds the oldest
|
||||
// (in time) vault version stored.
|
||||
func TestGetOldestVersion(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
upgradeTimePlusEpsilon := time.Now()
|
||||
|
||||
// 1.6.2 is stored before 1.6.1, so even though it is a higher number, it should be returned.
|
||||
c.storeVersionTimestamp(context.Background(), "1.6.2", upgradeTimePlusEpsilon.Add(-4*time.Hour))
|
||||
c.storeVersionTimestamp(context.Background(), "1.6.1", upgradeTimePlusEpsilon.Add(2*time.Hour))
|
||||
c.loadVersionTimestamps(c.activeContext)
|
||||
if len(c.versionTimestamps) != 3 {
|
||||
t.Fatalf("expected 3 entries in timestamps map after refresh, found: %d", len(c.versionTimestamps))
|
||||
}
|
||||
v, tm, err := c.FindOldestVersionTimestamp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v != "1.6.2" {
|
||||
t.Fatalf("expected 1.6.2, found: %s", v)
|
||||
}
|
||||
if tm.Before(upgradeTimePlusEpsilon.Add(-6*time.Hour)) || tm.After(upgradeTimePlusEpsilon.Add(-2*time.Hour)) {
|
||||
t.Fatalf("incorrect upgrade time logged: %v", tm)
|
||||
}
|
||||
}
|
|
@ -4245,6 +4245,42 @@ type HAStatusNode struct {
|
|||
LastEcho *time.Time `json:"last_echo"`
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handleVersionHistoryList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
versions := make([]VaultVersion, 0)
|
||||
respKeys := make([]string, 0)
|
||||
|
||||
for versionString, ts := range b.Core.versionTimestamps {
|
||||
versions = append(versions, VaultVersion{
|
||||
Version: versionString,
|
||||
TimestampInstalled: ts,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(versions, func(i, j int) bool {
|
||||
return versions[i].TimestampInstalled.Before(versions[j].TimestampInstalled)
|
||||
})
|
||||
|
||||
respKeyInfo := map[string]interface{}{}
|
||||
|
||||
for i, v := range versions {
|
||||
respKeys = append(respKeys, v.Version)
|
||||
|
||||
entry := map[string]interface{}{
|
||||
"timestamp_installed": v.TimestampInstalled.Format(time.RFC3339),
|
||||
"previous_version": nil,
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
entry["previous_version"] = versions[i-1].Version
|
||||
}
|
||||
|
||||
respKeyInfo[v.Version] = entry
|
||||
}
|
||||
|
||||
return logical.ListResponseWithInfo(respKeys, respKeyInfo), nil
|
||||
|
||||
}
|
||||
|
||||
func sanitizePath(path string) string {
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
|
@ -5036,4 +5072,13 @@ This path responds to the following HTTP methods.
|
|||
"List leases associated with this Vault cluster",
|
||||
"Requires sudo capability. List leases associated with this Vault cluster",
|
||||
},
|
||||
"version-history": {
|
||||
"List historical version changes sorted by installation time in ascending order.",
|
||||
`
|
||||
This path responds to the following HTTP methods.
|
||||
|
||||
LIST /
|
||||
Returns a list historical version changes sorted by installation time in ascending order.
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/hashicorp/vault/sdk/physical/inmem"
|
||||
lplugin "github.com/hashicorp/vault/sdk/plugin"
|
||||
"github.com/hashicorp/vault/sdk/plugin/mock"
|
||||
"github.com/hashicorp/vault/sdk/version"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
|
@ -896,3 +897,68 @@ func TestSystemBackend_HAStatus(t *testing.T) {
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// TestSystemBackend_VersionHistory_unauthenticated tests the sys/version-history
|
||||
// endpoint without providing a token. Requests to the endpoint must be
|
||||
// authenticated and thus a 403 response is expected.
|
||||
func TestSystemBackend_VersionHistory_unauthenticated(t *testing.T) {
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
client.SetToken("")
|
||||
resp, err := client.Logical().List("sys/version-history")
|
||||
|
||||
if resp != nil {
|
||||
t.Fatalf("expected nil response, resp: %#v", resp)
|
||||
}
|
||||
|
||||
respErr, ok := err.(*api.ResponseError)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected error type: err: %#v", err)
|
||||
}
|
||||
|
||||
if respErr.StatusCode != 403 {
|
||||
t.Fatalf("expected response status to be 403, actual: %d", respErr.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemBackend_VersionHistory_authenticated tests the sys/version-history
|
||||
// endpoint with authentication. Without synthetically altering the underlying
|
||||
// core/versions storage entries, a single version entry should exist.
|
||||
func TestSystemBackend_VersionHistory_authenticated(t *testing.T) {
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
resp, err := client.Logical().List("sys/version-history")
|
||||
if err != nil || resp == nil {
|
||||
t.Fatalf("request failed, err: %v, resp: %#v", err, resp)
|
||||
}
|
||||
|
||||
var ok bool
|
||||
var keys []interface{}
|
||||
var keyInfo map[string]interface{}
|
||||
|
||||
if keys, ok = resp.Data["keys"].([]interface{}); !ok {
|
||||
t.Fatalf("expected keys to be array, actual: %#v", resp.Data["keys"])
|
||||
}
|
||||
|
||||
if keyInfo, ok = resp.Data["key_info"].(map[string]interface{}); !ok {
|
||||
t.Fatalf("expected key_info to be map, actual: %#v", resp.Data["key_info"])
|
||||
}
|
||||
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("expected single version history entry for %q", version.Version)
|
||||
}
|
||||
|
||||
if keyInfo[version.Version] == nil {
|
||||
t.Fatalf("expected version %s to be present in key_info, actual: %#v", version.Version, keyInfo)
|
||||
}
|
||||
}
|
|
@ -489,6 +489,18 @@ func (b *SystemBackend) statusPaths() []*framework.Path {
|
|||
HelpSynopsis: strings.TrimSpace(sysHelp["ha-status"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["ha-status"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "version-history/$",
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{
|
||||
Callback: b.handleVersionHistoryList,
|
||||
Summary: "Returns map of historical version change entries",
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["version-history"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["version-history"][1]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,45 +11,68 @@ import (
|
|||
|
||||
const vaultVersionPath string = "core/versions/"
|
||||
|
||||
// storeVersionTimestamp will store the version and timestamp pair to storage only if no entry
|
||||
// for that version already exists in storage.
|
||||
func (c *Core) storeVersionTimestamp(ctx context.Context, version string, currentTime time.Time) (bool, error) {
|
||||
timeStamp, err := c.barrier.Get(ctx, vaultVersionPath+version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
// storeVersionTimestamp will store the version and timestamp pair to storage
|
||||
// only if no entry for that version already exists in storage. Version
|
||||
// timestamps were initially stored in local time. UTC should be used. Existing
|
||||
// entries can be overwritten via the force flag. A bool will be returned
|
||||
// denoting whether the entry was updated
|
||||
func (c *Core) storeVersionTimestamp(ctx context.Context, version string, timestampInstalled time.Time, force bool) (bool, error) {
|
||||
key := vaultVersionPath + version
|
||||
|
||||
vaultVersion := VaultVersion{
|
||||
TimestampInstalled: timestampInstalled.UTC(),
|
||||
Version: version,
|
||||
}
|
||||
|
||||
if timeStamp != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
vaultVersion := VaultVersion{TimestampInstalled: currentTime, Version: version}
|
||||
marshalledVaultVersion, err := json.Marshal(vaultVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = c.barrier.Put(ctx, &logical.StorageEntry{
|
||||
Key: vaultVersionPath + version,
|
||||
newEntry := &logical.StorageEntry{
|
||||
Key: key,
|
||||
Value: marshalledVaultVersion,
|
||||
})
|
||||
}
|
||||
|
||||
if force {
|
||||
// avoid storage lookup and write immediately
|
||||
err = c.barrier.Put(ctx, newEntry)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
existingEntry, err := c.barrier.Get(ctx, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if existingEntry != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = c.barrier.Put(ctx, newEntry)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FindOldestVersionTimestamp searches for the vault version with the oldest
|
||||
// upgrade timestamp from storage. The earliest version this can be (barring
|
||||
// downgrades) is 1.9.0.
|
||||
// upgrade timestamp from storage. The earliest version this can be is 1.9.0.
|
||||
func (c *Core) FindOldestVersionTimestamp() (string, time.Time, error) {
|
||||
if c.versionTimestamps == nil || len(c.versionTimestamps) == 0 {
|
||||
return "", time.Time{}, fmt.Errorf("version timestamps are not initialized")
|
||||
}
|
||||
|
||||
// initialize oldestUpgradeTime to current time
|
||||
oldestUpgradeTime := time.Now()
|
||||
oldestUpgradeTime := time.Now().UTC()
|
||||
var oldestVersion string
|
||||
|
||||
for version, upgradeTime := range c.versionTimestamps {
|
||||
if upgradeTime.Before(oldestUpgradeTime) {
|
||||
oldestVersion = version
|
||||
|
@ -59,18 +82,19 @@ func (c *Core) FindOldestVersionTimestamp() (string, time.Time, error) {
|
|||
return oldestVersion, oldestUpgradeTime, nil
|
||||
}
|
||||
|
||||
// loadVersionTimestamps loads all the vault versions and associated
|
||||
// upgrade timestamps from storage.
|
||||
func (c *Core) loadVersionTimestamps(ctx context.Context) (retErr error) {
|
||||
// loadVersionTimestamps loads all the vault versions and associated upgrade
|
||||
// timestamps from storage. Version timestamps were originally stored in local
|
||||
// time. A timestamp that is not in UTC will be rewritten to storage as UTC.
|
||||
func (c *Core) loadVersionTimestamps(ctx context.Context) error {
|
||||
vaultVersions, err := c.barrier.List(ctx, vaultVersionPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve vault versions from storage: %+w", err)
|
||||
return fmt.Errorf("unable to retrieve vault versions from storage: %w", err)
|
||||
}
|
||||
|
||||
for _, versionPath := range vaultVersions {
|
||||
version, err := c.barrier.Get(ctx, vaultVersionPath+versionPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read vault version at path %s: err %+w", versionPath, err)
|
||||
return fmt.Errorf("unable to read vault version at path %s: err %w", versionPath, err)
|
||||
}
|
||||
if version == nil {
|
||||
return fmt.Errorf("nil version stored at path %s", versionPath)
|
||||
|
@ -83,7 +107,25 @@ func (c *Core) loadVersionTimestamps(ctx context.Context) (retErr error) {
|
|||
if vaultVersion.Version == "" || vaultVersion.TimestampInstalled.IsZero() {
|
||||
return fmt.Errorf("found empty serialized vault version at path %s", versionPath)
|
||||
}
|
||||
c.versionTimestamps[vaultVersion.Version] = vaultVersion.TimestampInstalled
|
||||
|
||||
timestampInstalled := vaultVersion.TimestampInstalled
|
||||
|
||||
// self-heal entries that were not stored in UTC
|
||||
if timestampInstalled.Location() != time.UTC {
|
||||
timestampInstalled = timestampInstalled.UTC()
|
||||
isUpdated, err := c.storeVersionTimestamp(ctx, vaultVersion.Version, timestampInstalled, true)
|
||||
|
||||
if err != nil {
|
||||
c.logger.Warn("failed to rewrite vault version timestamp as UTC", "error", err)
|
||||
}
|
||||
|
||||
if isUpdated {
|
||||
c.logger.Info("self-healed pre-existing vault version in UTC",
|
||||
"vault version", vaultVersion.Version, "UTC time", timestampInstalled)
|
||||
}
|
||||
}
|
||||
|
||||
c.versionTimestamps[vaultVersion.Version] = timestampInstalled
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/version"
|
||||
)
|
||||
|
||||
// TestVersionStore_StoreMultipleVaultVersions writes multiple versions of 1.9.0 and verifies that only
|
||||
// the original timestamp is stored.
|
||||
func TestVersionStore_StoreMultipleVaultVersions(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
upgradeTimePlusEpsilon := time.Now().UTC()
|
||||
wasStored, err := c.storeVersionTimestamp(context.Background(), version.Version, upgradeTimePlusEpsilon.Add(30*time.Hour), false)
|
||||
if err != nil || wasStored {
|
||||
t.Fatalf("vault version was re-stored: %v, err is: %s", wasStored, err.Error())
|
||||
}
|
||||
upgradeTime, ok := c.versionTimestamps[version.Version]
|
||||
if !ok {
|
||||
t.Fatalf("no %s version timestamp found", version.Version)
|
||||
}
|
||||
if upgradeTime.After(upgradeTimePlusEpsilon) {
|
||||
t.Fatalf("upgrade time for %s is incorrect: got %+v, expected less than %+v", version.Version, upgradeTime, upgradeTimePlusEpsilon)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVersionStore_GetOldestVersion verifies that FindOldestVersionTimestamp finds the oldest
|
||||
// (in time) vault version stored.
|
||||
func TestVersionStore_GetOldestVersion(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
upgradeTimePlusEpsilon := time.Now().UTC()
|
||||
|
||||
// 1.6.2 is stored before 1.6.1, so even though it is a higher number, it should be returned.
|
||||
versionEntries := []struct{version string; ts time.Time}{
|
||||
{"1.6.2", upgradeTimePlusEpsilon.Add(-4*time.Hour)},
|
||||
{"1.6.1", upgradeTimePlusEpsilon.Add(2*time.Hour)},
|
||||
}
|
||||
|
||||
for _, entry := range versionEntries{
|
||||
_, err := c.storeVersionTimestamp(context.Background(), entry.version, entry.ts, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write version entry %#v, err: %s", entry, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err := c.loadVersionTimestamps(c.activeContext)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to populate version history cache, err: %s", err.Error())
|
||||
}
|
||||
|
||||
if len(c.versionTimestamps) != 3 {
|
||||
t.Fatalf("expected 3 entries in timestamps map after refresh, found: %d", len(c.versionTimestamps))
|
||||
}
|
||||
v, tm, err := c.FindOldestVersionTimestamp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v != "1.6.2" {
|
||||
t.Fatalf("expected 1.6.2, found: %s", v)
|
||||
}
|
||||
if tm.Before(upgradeTimePlusEpsilon.Add(-6*time.Hour)) || tm.After(upgradeTimePlusEpsilon.Add(-2*time.Hour)) {
|
||||
t.Fatalf("incorrect upgrade time logged: %v", tm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionStore_SelfHealUTC(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
estLoc, err := time.LoadLocation("EST")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load location, err: %s", err.Error())
|
||||
}
|
||||
|
||||
nowEST := time.Now().In(estLoc)
|
||||
|
||||
versionEntries := []struct{version string; ts time.Time}{
|
||||
{"1.9.0", nowEST.Add(24*time.Hour)},
|
||||
{"1.9.1", nowEST.Add(48*time.Hour)},
|
||||
}
|
||||
|
||||
for _, entry := range versionEntries{
|
||||
_, err := c.storeVersionTimestamp(context.Background(), entry.version, entry.ts, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write version entry %#v, err: %s", entry, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
err = c.loadVersionTimestamps(c.activeContext)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load version timestamps, err: %s", err.Error())
|
||||
}
|
||||
|
||||
for versionStr, ts := range c.versionTimestamps {
|
||||
if ts.Location() != time.UTC {
|
||||
t.Fatalf("failed to convert %s timestamp %s to UTC", versionStr, ts)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
layout: api
|
||||
page_title: /sys/version-history - HTTP API
|
||||
description: The `/sys/version-history` endpoint is used to retrieve the version history of a Vault.
|
||||
---
|
||||
|
||||
# `/sys/version-history`
|
||||
|
||||
~> **NOTE**: Tracking Vault version history was added in Vault version 1.9.0. Entries for versions installed prior to version 1.9.0 will not be available via this API.
|
||||
This API was added in 1.10.0.
|
||||
|
||||
The `/sys/version-history` endpoint is used to retrieve the version history of a Vault.
|
||||
|
||||
## Read Version History
|
||||
|
||||
This endpoint returns the version history of the Vault. The response will contain the following keys:
|
||||
|
||||
- `keys`: a list of installed versions in chronological order based on the time installed
|
||||
- `key_info`: a map indexed by the versions found in the `keys` list containing the following subkeys:
|
||||
- `previous_version`: the version installed prior to this version or `null` if no prior version exists
|
||||
- `timestamp_installed`: the time (in UTC) at which the version was installed
|
||||
|
||||
| Method | Path |
|
||||
| :----- | :--------------------- |
|
||||
| `GET` | `/sys/version-history` |
|
||||
|
||||
### Sample Request
|
||||
|
||||
```shell-session
|
||||
$ curl \
|
||||
--header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/sys/version-history
|
||||
```
|
||||
|
||||
### Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"keys": ["1.9.0", "1.9.1", "1.9.2"],
|
||||
"key_info": {
|
||||
"1.9.0": {
|
||||
"previous_version": null,
|
||||
"timestamp_installed": "2021-11-18T10:23:16Z"
|
||||
},
|
||||
"1.9.1": {
|
||||
"previous_version": "1.9.0",
|
||||
"timestamp_installed": "2021-12-13T11:09:52Z"
|
||||
},
|
||||
"1.9.2": {
|
||||
"previous_version": "1.9.1",
|
||||
"timestamp_installed": "2021-12-23T10:56:37Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: version-history - Command
|
||||
description: |-
|
||||
The "version-history" command prints the historical list of installed Vault versions in chronological order.
|
||||
---
|
||||
|
||||
# version-history
|
||||
|
||||
The `version-history` command prints the historical list of installed Vault versions in chronological order.
|
||||
|
||||
```shell-session
|
||||
Note: Version tracking was added in 1.9.0. Earlier versions have not been tracked.
|
||||
|
||||
Version Installation Time
|
||||
------- -----------------
|
||||
1.9.0 2021-11-18T10:23:16Z
|
||||
1.9.1 2022-12-13T11:09:52Z
|
||||
1.9.2 2021-12-23T10:56:37Z
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The following flags are available in addition to the [standard set of
|
||||
flags](/docs/commands) included on all commands.
|
||||
|
||||
### Output Options
|
||||
|
||||
- `-format` `(string: "table")` - Print the output in the given format. Valid
|
||||
formats are "table" or json". This can also be specified via the
|
||||
`VAULT_FORMAT` environment variable.
|
|
@ -616,6 +616,10 @@
|
|||
"title": "<code>/sys/unseal</code>",
|
||||
"path": "system/unseal"
|
||||
},
|
||||
{
|
||||
"title": "<code>/sys/version-history</code>",
|
||||
"path": "system/version-history"
|
||||
},
|
||||
{
|
||||
"title": "<code>/sys/wrapping/lookup</code>",
|
||||
"path": "system/wrapping-lookup"
|
||||
|
|
|
@ -757,6 +757,10 @@
|
|||
"title": "<code>version</code>",
|
||||
"path": "commands/version"
|
||||
},
|
||||
{
|
||||
"title": "<code>version-history</code>",
|
||||
"path": "commands/version-history"
|
||||
},
|
||||
{
|
||||
"title": "<code>write</code>",
|
||||
"path": "commands/write"
|
||||
|
|
|
@ -215,6 +215,11 @@ module.exports = [
|
|||
destination: '/api-docs/system/unseal',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/docs/http/sys-version-history',
|
||||
destination: '/api-docs/system/version-history',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/docs/install/install',
|
||||
destination: '/docs/install',
|
||||
|
|
Loading…
Reference in New Issue