Some of the OSS changes were clobbered when merging with quotas out of, master (#9343)

* OSS side of Global Plugin Reload
This commit is contained in:
Scott Miller 2020-06-29 16:58:51 -05:00 committed by GitHub
parent a83fe0fc6d
commit cc51427584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 39 deletions

View File

@ -257,26 +257,30 @@ func (c *Sys) ReloadPlugin(i *ReloadPluginInput) (string, error) {
}
defer resp.Body.Close()
if i.Scope == "cluster" {
if i.Scope == "global" {
// Get the reload id
secret, parseErr := ParseSecret(resp.Body)
if parseErr != nil {
return "", err
}
return secret.Data["reload_id"].(string), nil
if _, ok := secret.Data["reload_id"]; ok {
return secret.Data["reload_id"].(string), nil
}
}
return "", err
}
type PluginReloadStatus struct {
Timestamp time.Time `json:"timestamp"`
Success bool `json:"success"`
Message string `json:"message"`
// ReloadStatus is the status of an individual node's plugin reload
type ReloadStatus struct {
Timestamp time.Time `json:"timestamp" mapstructure:"timestamp"`
Success bool `json:"success" mapstructure:"success"`
Message string `json:"message" mapstructure:"message"`
}
type PluginReloadStatusResponse struct {
ReloadID string
Results map[string]interface{}
// ReloadStatusResponse is the combined response of all known completed plugin reloads
type ReloadStatusResponse struct {
ReloadID string `mapstructure:"reload_id"`
Results map[string]*ReloadStatus `mapstructure:"results"`
}
// ReloadPluginStatusInput is used as input to the ReloadStatusPlugin function.
@ -286,10 +290,10 @@ type ReloadPluginStatusInput struct {
}
// ReloadPluginStatus retrieves the status of a reload operation
func (c *Sys) ReloadPluginStatus(reloadID string) (map[string]interface{}, error) {
func (c *Sys) ReloadPluginStatus(reloadStatusInput *ReloadPluginStatusInput) (*ReloadStatusResponse, error) {
path := "/v1/sys/plugins/reload/backend/status"
req := c.c.NewRequest(http.MethodGet, path)
req.Params.Add("reload_id", reloadID)
req.Params.Add("reload_id", reloadStatusInput.ReloadID)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
@ -305,7 +309,19 @@ func (c *Sys) ReloadPluginStatus(reloadID string) (map[string]interface{}, error
return nil, err
}
return secret.Data, nil
var r ReloadStatusResponse
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339),
Result: &r,
})
if err != nil {
return nil, err
}
err = d.Decode(secret.Data)
if err != nil {
return nil, err
}
return &r, nil
}
return nil, nil

View File

@ -63,7 +63,7 @@ func (c *PluginReloadCommand) Flags() *FlagSets {
Name: "scope",
Target: &c.scope,
Completion: complete.PredictAnything,
Usage: `The scope of the reload, omitted for local, "cluster", for cluster-wide`,
Usage: "The scope of the reload, omitted for local, 'global', for replicated reloads",
})
return set
@ -92,7 +92,7 @@ func (c *PluginReloadCommand) Run(args []string) int {
case c.plugin != "" && len(c.mounts) > 0:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
case c.scope != "" && c.scope != "cluster":
case c.scope != "" && c.scope != "global":
c.UI.Error(fmt.Sprintf("Invalid reload scope: %s", c.scope))
}
@ -122,7 +122,7 @@ func (c *PluginReloadCommand) Run(args []string) int {
if rid != "" {
c.UI.Output(fmt.Sprintf("Success! Reloading plugin: %s, reload_id: %s", c.plugin, rid))
} else {
c.UI.Output(fmt.Sprintf("Success! Reloaded plugin: %s", c.mounts))
c.UI.Output(fmt.Sprintf("Success! Reloaded plugin: %s", c.plugin))
}
}

View File

@ -20,6 +20,17 @@ func testPluginReloadCommand(tb testing.TB) (*cli.MockUi, *PluginReloadCommand)
}
}
func testPluginReloadStatusCommand(tb testing.TB) (*cli.MockUi, *PluginReloadStatusCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PluginReloadStatusCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPluginReloadCommand_Run(t *testing.T) {
t.Parallel()
@ -108,3 +119,86 @@ func TestPluginReloadCommand_Run(t *testing.T) {
})
}
func TestPluginReloadStatusCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
nil,
"Not enough arguments",
1,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPluginReloadCommand(t)
cmd.client = client
args := append([]string{}, tc.args...)
code := cmd.Run(args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
t.Run("integration", func(t *testing.T) {
t.Parallel()
pluginDir, cleanup := testPluginDir(t)
defer cleanup(t)
client, _, closer := testVaultServerPluginDir(t, pluginDir)
defer closer()
pluginName := "my-plugin"
_, sha256Sum := testPluginCreateAndRegister(t, client, pluginDir, pluginName, consts.PluginTypeCredential)
ui, cmd := testPluginReloadStatusCommand(t)
cmd.client = client
if err := client.Sys().RegisterPlugin(&api.RegisterPluginInput{
Name: pluginName,
Type: consts.PluginTypeCredential,
Command: pluginName,
SHA256: sha256Sum,
}); err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
"-reload_id", pluginName,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Reloaded plugin: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
}

View File

@ -41,7 +41,7 @@ import (
)
const maxBytes = 128 * 1024
const clusterScope = "cluster"
const globalScope = "global"
func systemBackendMemDBSchema() *memdb.DBSchema {
systemSchema := &memdb.DBSchema{
@ -439,6 +439,10 @@ func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logic
pluginMounts := d.Get("mounts").([]string)
scope := d.Get("scope").(string)
if scope != "" && scope != globalScope {
return logical.ErrorResponse("reload scope must be omitted or 'global'"), nil
}
if pluginName != "" && len(pluginMounts) > 0 {
return logical.ErrorResponse("plugin and mounts cannot be set at the same time"), nil
}
@ -447,12 +451,12 @@ func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logic
}
if pluginName != "" {
err := b.Core.reloadMatchingPlugin(ctx, pluginName, time.Now())
err := b.Core.reloadMatchingPlugin(ctx, pluginName)
if err != nil {
return nil, err
}
} else if len(pluginMounts) > 0 {
err := b.Core.reloadMatchingPluginMounts(ctx, pluginMounts, time.Now())
err := b.Core.reloadMatchingPluginMounts(ctx, pluginMounts)
if err != nil {
return nil, err
}
@ -466,8 +470,11 @@ func (b *SystemBackend) handlePluginReloadUpdate(ctx context.Context, req *logic
},
}
if scope == clusterScope {
go handleClusterPluginReload(b, req.ID, pluginName, pluginMounts)
if scope == globalScope {
err := handleGlobalPluginReload(ctx, b.Core, req.ID, pluginName, pluginMounts)
if err != nil {
return nil, err
}
return logical.RespondWithStatusCode(&r, req, http.StatusAccepted)
}
return &r, nil
@ -4281,11 +4288,11 @@ This path responds to the following HTTP methods.
"",
},
"plugin-backend-reload-scope": {
`The scope of the reload`,
`Either absent or the empty string for local reload, or "cluster" for a cluster wide reload`,
`The scope of the plugin reload, either omitted or 'global'`,
"",
},
"plugin-reload-backend-status": {
`Retrieve the status of a cluster-wide plugin reload`,
`Retrieve the status of a global plugin reload`,
"",
},
"hash": {

View File

@ -84,10 +84,10 @@ var (
},
}
}
handleClusterPluginReload = func(*SystemBackend, string, string, []string) error {
handleGlobalPluginReload = func(context.Context, *Core, string, string, []string) error {
return nil
}
handleSetupPluginReload = func(*SystemBackend) error {
handleSetupPluginReload = func(*Core) error {
return nil
}

View File

@ -718,7 +718,7 @@ func (b *SystemBackend) pluginsReloadPath() *framework.Path {
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handlePluginReloadUpdate,
Summary: "Reload mounted plugin backends.",
Description: "Either the plugin name (`plugin`) or the desired plugin backend mounts (`mounts`) must be provided, but not both. In the case that the plugin name is provided, all mounted paths that use that plugin backend will be reloaded. If (`scope`) is provided and is (`cluster`), the plugin(s) are reloaded cluster wide.",
Description: "Either the plugin name (`plugin`) or the desired plugin backend mounts (`mounts`) must be provided, but not both. In the case that the plugin name is provided, all mounted paths that use that plugin backend will be reloaded. If (`scope`) is provided and is (`global`), the plugin(s) are reloaded globally.",
},
},
@ -726,6 +726,7 @@ func (b *SystemBackend) pluginsReloadPath() *framework.Path {
HelpDescription: strings.TrimSpace(sysHelp["plugin-reload"][1]),
}
}
func (b *SystemBackend) toolsPaths() []*framework.Path {
return []*framework.Path{
{

View File

@ -257,26 +257,30 @@ func (c *Sys) ReloadPlugin(i *ReloadPluginInput) (string, error) {
}
defer resp.Body.Close()
if i.Scope == "cluster" {
if i.Scope == "global" {
// Get the reload id
secret, parseErr := ParseSecret(resp.Body)
if parseErr != nil {
return "", err
}
return secret.Data["reload_id"].(string), nil
if _, ok := secret.Data["reload_id"]; ok {
return secret.Data["reload_id"].(string), nil
}
}
return "", err
}
type PluginReloadStatus struct {
Timestamp time.Time `json:"timestamp"`
Success bool `json:"success"`
Message string `json:"message"`
// ReloadStatus is the status of an individual node's plugin reload
type ReloadStatus struct {
Timestamp time.Time `json:"timestamp" mapstructure:"timestamp"`
Success bool `json:"success" mapstructure:"success"`
Message string `json:"message" mapstructure:"message"`
}
type PluginReloadStatusResponse struct {
ReloadID string
Results map[string]interface{}
// ReloadStatusResponse is the combined response of all known completed plugin reloads
type ReloadStatusResponse struct {
ReloadID string `mapstructure:"reload_id"`
Results map[string]*ReloadStatus `mapstructure:"results"`
}
// ReloadPluginStatusInput is used as input to the ReloadStatusPlugin function.
@ -286,10 +290,10 @@ type ReloadPluginStatusInput struct {
}
// ReloadPluginStatus retrieves the status of a reload operation
func (c *Sys) ReloadPluginStatus(reloadID string) (map[string]interface{}, error) {
func (c *Sys) ReloadPluginStatus(reloadStatusInput *ReloadPluginStatusInput) (*ReloadStatusResponse, error) {
path := "/v1/sys/plugins/reload/backend/status"
req := c.c.NewRequest(http.MethodGet, path)
req.Params.Add("reload_id", reloadID)
req.Params.Add("reload_id", reloadStatusInput.ReloadID)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
@ -305,8 +309,19 @@ func (c *Sys) ReloadPluginStatus(reloadID string) (map[string]interface{}, error
return nil, err
}
return secret.Data, nil
var r ReloadStatusResponse
d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339),
Result: &r,
})
if err != nil {
return nil, err
}
err = d.Decode(secret.Data)
if err != nil {
return nil, err
}
return &r, nil
}
return nil, nil