-
Notifications
You must be signed in to change notification settings - Fork 324
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ec7b0ef
commit ec87da7
Showing
5 changed files
with
389 additions
and
0 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 | ||
cli: Add `consul-k8s config read` command that returns the helm configuration in yaml format. | ||
``` |
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,26 @@ | ||
package config | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/hashicorp/consul-k8s/cli/common" | ||
"github.com/mitchellh/cli" | ||
) | ||
|
||
// ConfigCommand provides a synopsis for the config subcommands (e.g. read). | ||
type ConfigCommand struct { | ||
*common.BaseCommand | ||
} | ||
|
||
// Run prints out information about the subcommands. | ||
func (c *ConfigCommand) Run([]string) int { | ||
return cli.RunResultHelp | ||
} | ||
|
||
func (c *ConfigCommand) Help() string { | ||
return fmt.Sprintf("%s\n\nUsage: consul-k8s config <subcommand>", c.Synopsis()) | ||
} | ||
|
||
func (c *ConfigCommand) Synopsis() string { | ||
return "Operate on configuration" | ||
} |
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,199 @@ | ||
package read | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/posener/complete" | ||
|
||
"github.com/hashicorp/consul-k8s/cli/common" | ||
"github.com/hashicorp/consul-k8s/cli/common/flag" | ||
"github.com/hashicorp/consul-k8s/cli/common/terminal" | ||
"github.com/hashicorp/consul-k8s/cli/helm" | ||
"helm.sh/helm/v3/pkg/action" | ||
helmCLI "helm.sh/helm/v3/pkg/cli" | ||
"k8s.io/client-go/kubernetes" | ||
"sigs.k8s.io/yaml" | ||
) | ||
|
||
const ( | ||
flagNameKubeConfig = "kubeconfig" | ||
flagNameKubeContext = "context" | ||
) | ||
|
||
type ReadCommand struct { | ||
*common.BaseCommand | ||
|
||
helmActionsRunner helm.HelmActionsRunner | ||
|
||
kubernetes kubernetes.Interface | ||
|
||
set *flag.Sets | ||
|
||
flagKubeConfig string | ||
flagKubeContext string | ||
|
||
once sync.Once | ||
help string | ||
} | ||
|
||
func (c *ReadCommand) init() { | ||
c.set = flag.NewSets() | ||
|
||
f := c.set.NewSet("Global Options") | ||
f.StringVar(&flag.StringVar{ | ||
Name: "kubeconfig", | ||
Aliases: []string{"c"}, | ||
Target: &c.flagKubeConfig, | ||
Default: "", | ||
Usage: "Path to kubeconfig file.", | ||
}) | ||
f.StringVar(&flag.StringVar{ | ||
Name: "context", | ||
Target: &c.flagKubeContext, | ||
Default: "", | ||
Usage: "Kubernetes context to use.", | ||
}) | ||
|
||
c.help = c.set.Help() | ||
} | ||
|
||
// Run checks the status of a Consul installation on Kubernetes. | ||
func (c *ReadCommand) Run(args []string) int { | ||
c.once.Do(c.init) | ||
if c.helmActionsRunner == nil { | ||
c.helmActionsRunner = &helm.ActionRunner{} | ||
} | ||
|
||
c.Log.ResetNamed("config read") | ||
defer common.CloseWithError(c.BaseCommand) | ||
|
||
if err := c.set.Parse(args); err != nil { | ||
c.UI.Output(err.Error()) | ||
return 1 | ||
} | ||
|
||
if err := c.validateFlags(); err != nil { | ||
c.UI.Output(err.Error()) | ||
return 1 | ||
} | ||
|
||
// helmCLI.New() will create a settings object which is used by the Helm Go SDK calls. | ||
settings := helmCLI.New() | ||
if c.flagKubeConfig != "" { | ||
settings.KubeConfig = c.flagKubeConfig | ||
} | ||
if c.flagKubeContext != "" { | ||
settings.KubeContext = c.flagKubeContext | ||
} | ||
|
||
if err := c.setupKubeClient(settings); err != nil { | ||
c.UI.Output(err.Error(), terminal.WithErrorStyle()) | ||
return 1 | ||
} | ||
|
||
// Setup logger to stream Helm library logs. | ||
var uiLogger = func(s string, args ...interface{}) { | ||
logMsg := fmt.Sprintf(s, args...) | ||
c.UI.Output(logMsg, terminal.WithLibraryStyle()) | ||
} | ||
|
||
_, releaseName, namespace, err := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ | ||
Settings: settings, | ||
ReleaseName: common.DefaultReleaseName, | ||
DebugLog: uiLogger, | ||
}) | ||
if err != nil { | ||
c.UI.Output(err.Error(), terminal.WithErrorStyle()) | ||
return 1 | ||
} | ||
|
||
if err := c.checkHelmInstallation(settings, uiLogger, releaseName, namespace); err != nil { | ||
c.UI.Output(err.Error(), terminal.WithErrorStyle()) | ||
return 1 | ||
} | ||
|
||
return 0 | ||
} | ||
|
||
// validateFlags checks the command line flags and values for errors. | ||
func (c *ReadCommand) validateFlags() error { | ||
if len(c.set.Args()) > 0 { | ||
return errors.New("should have no non-flag arguments") | ||
} | ||
return nil | ||
} | ||
|
||
// AutocompleteFlags returns a mapping of supported flags and autocomplete | ||
// options for this command. The map key for the Flags map should be the | ||
// complete flag such as "-foo" or "--foo". | ||
func (c *ReadCommand) AutocompleteFlags() complete.Flags { | ||
return complete.Flags{ | ||
fmt.Sprintf("-%s", flagNameKubeConfig): complete.PredictFiles("*"), | ||
fmt.Sprintf("-%s", flagNameKubeContext): complete.PredictNothing, | ||
} | ||
} | ||
|
||
// AutocompleteArgs returns the argument predictor for this command. | ||
// Since argument completion is not supported, this will return | ||
// complete.PredictNothing. | ||
func (c *ReadCommand) AutocompleteArgs() complete.Predictor { | ||
return complete.PredictNothing | ||
} | ||
|
||
// checkHelmInstallation uses the helm Go SDK to depict the status of a named release. This function then prints | ||
// the version of the release, it's status (unknown, deployed, uninstalled, ...), and the overwritten values. | ||
func (c *ReadCommand) checkHelmInstallation(settings *helmCLI.EnvSettings, uiLogger action.DebugLog, releaseName, namespace string) error { | ||
// Need a specific action config to call helm status, where namespace comes from the previous call to list. | ||
statusConfig := new(action.Configuration) | ||
statusConfig, err := helm.InitActionConfig(statusConfig, namespace, settings, uiLogger) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
statuser := action.NewStatus(statusConfig) | ||
rel, err := c.helmActionsRunner.GetStatus(statuser, releaseName) | ||
if err != nil { | ||
return fmt.Errorf("couldn't check for installations: %s", err) | ||
} | ||
|
||
valuesYaml, err := yaml.Marshal(rel.Config) | ||
if err != nil { | ||
return err | ||
} | ||
c.UI.Output(string(valuesYaml)) | ||
|
||
return nil | ||
} | ||
|
||
// setupKubeClient to use for non Helm SDK calls to the Kubernetes API The Helm SDK will use | ||
// settings.RESTClientGetter for its calls as well, so this will use a consistent method to | ||
// target the right cluster for both Helm SDK and non Helm SDK calls. | ||
func (c *ReadCommand) setupKubeClient(settings *helmCLI.EnvSettings) error { | ||
if c.kubernetes == nil { | ||
restConfig, err := settings.RESTClientGetter().ToRESTConfig() | ||
if err != nil { | ||
c.UI.Output("Error retrieving Kubernetes authentication: %v", err, terminal.WithErrorStyle()) | ||
return err | ||
} | ||
c.kubernetes, err = kubernetes.NewForConfig(restConfig) | ||
if err != nil { | ||
c.UI.Output("Error initializing Kubernetes client: %v", err, terminal.WithErrorStyle()) | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Help returns a description of the command and how it is used. | ||
func (c *ReadCommand) Help() string { | ||
c.once.Do(c.init) | ||
return c.Synopsis() + "\n\nUsage: consul-k8s config read [flags]\n\n" + c.help | ||
} | ||
|
||
// Synopsis returns a one-line command summary. | ||
func (c *ReadCommand) Synopsis() string { | ||
return "Returns the helm config of a Consul installation on Kubernetes." | ||
} |
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,149 @@ | ||
package read | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"io" | ||
"os" | ||
"testing" | ||
|
||
"github.com/hashicorp/consul-k8s/cli/common" | ||
cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" | ||
"github.com/hashicorp/consul-k8s/cli/common/terminal" | ||
"github.com/hashicorp/consul-k8s/cli/helm" | ||
"github.com/hashicorp/go-hclog" | ||
"github.com/posener/complete" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"helm.sh/helm/v3/pkg/action" | ||
"helm.sh/helm/v3/pkg/chart" | ||
helmRelease "helm.sh/helm/v3/pkg/release" | ||
helmTime "helm.sh/helm/v3/pkg/time" | ||
"k8s.io/client-go/kubernetes/fake" | ||
) | ||
|
||
func TestConfigRead(t *testing.T) { | ||
nowTime := helmTime.Now() | ||
cases := map[string]struct { | ||
messages []string | ||
helmActionsRunner *helm.MockActionRunner | ||
expectedReturnCode int | ||
}{ | ||
"empty config": { | ||
messages: []string{"\n"}, | ||
|
||
helmActionsRunner: &helm.MockActionRunner{ | ||
GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { | ||
return &helmRelease.Release{ | ||
Name: "consul", Namespace: "consul", | ||
Info: &helmRelease.Info{LastDeployed: nowTime, Status: "READY"}, | ||
Chart: &chart.Chart{Metadata: &chart.Metadata{Version: "1.0.0"}}, | ||
Config: make(map[string]interface{})}, nil | ||
}, | ||
}, | ||
expectedReturnCode: 0, | ||
}, | ||
"error": { | ||
messages: []string{"error", "\n"}, | ||
|
||
helmActionsRunner: &helm.MockActionRunner{ | ||
GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { | ||
return nil, errors.New("error") | ||
}, | ||
}, | ||
expectedReturnCode: 1, | ||
}, | ||
"some config": { | ||
messages: []string{"global: \"true\"", "\n"}, | ||
|
||
helmActionsRunner: &helm.MockActionRunner{ | ||
GetStatusFunc: func(status *action.Status, name string) (*helmRelease.Release, error) { | ||
return &helmRelease.Release{ | ||
Name: "consul", Namespace: "consul", | ||
Info: &helmRelease.Info{LastDeployed: nowTime, Status: "READY"}, | ||
Chart: &chart.Chart{ | ||
Metadata: &chart.Metadata{ | ||
Version: "1.0.0", | ||
}, | ||
}, | ||
Config: map[string]interface{}{"global": "true"}, | ||
}, nil | ||
}, | ||
}, | ||
expectedReturnCode: 0, | ||
}, | ||
} | ||
for name, tc := range cases { | ||
t.Run(name, func(t *testing.T) { | ||
buf := new(bytes.Buffer) | ||
c := getInitializedCommand(t, buf) | ||
c.kubernetes = fake.NewSimpleClientset() | ||
c.helmActionsRunner = tc.helmActionsRunner | ||
returnCode := c.Run([]string{}) | ||
require.Equal(t, tc.expectedReturnCode, returnCode) | ||
output := buf.String() | ||
for _, msg := range tc.messages { | ||
require.Contains(t, output, msg) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestTaskCreateCommand_AutocompleteFlags(t *testing.T) { | ||
t.Parallel() | ||
cmd := getInitializedCommand(t, nil) | ||
|
||
predictor := cmd.AutocompleteFlags() | ||
|
||
// Test that we get the expected number of predictions | ||
args := complete.Args{Last: "-"} | ||
res := predictor.Predict(args) | ||
|
||
// Grab the list of flags from the Flag object | ||
flags := make([]string, 0) | ||
cmd.set.VisitSets(func(name string, set *cmnFlag.Set) { | ||
set.VisitAll(func(flag *flag.Flag) { | ||
flags = append(flags, fmt.Sprintf("-%s", flag.Name)) | ||
}) | ||
}) | ||
|
||
// Verify that there is a prediction for each flag associated with the command | ||
assert.Equal(t, len(flags), len(res)) | ||
assert.ElementsMatch(t, flags, res, "flags and predictions didn't match, make sure to add "+ | ||
"new flags to the command AutoCompleteFlags function") | ||
} | ||
|
||
func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { | ||
cmd := getInitializedCommand(t, nil) | ||
c := cmd.AutocompleteArgs() | ||
assert.Equal(t, complete.PredictNothing, c) | ||
} | ||
|
||
// getInitializedCommand sets up a command struct for tests. | ||
func getInitializedCommand(t *testing.T, buf io.Writer) *ReadCommand { | ||
t.Helper() | ||
log := hclog.New(&hclog.LoggerOptions{ | ||
Name: "cli", | ||
Level: hclog.Info, | ||
Output: os.Stdout, | ||
}) | ||
var ui terminal.UI | ||
if buf != nil { | ||
ui = terminal.NewUI(context.Background(), buf) | ||
} else { | ||
ui = terminal.NewBasicUI(context.Background()) | ||
} | ||
baseCommand := &common.BaseCommand{ | ||
Log: log, | ||
UI: ui, | ||
} | ||
|
||
c := &ReadCommand{ | ||
BaseCommand: baseCommand, | ||
} | ||
c.init() | ||
return c | ||
} |
Oops, something went wrong.