-
Notifications
You must be signed in to change notification settings - Fork 321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add config read command #2078
add config read command #2078
Changes from all commits
112d1d6
413d24b
c7f6c44
0aa2024
7472313
48c09d3
370b062
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
``` |
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" | ||
} |
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." | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious, are changes in this PR to prediction behavior? It feels like prediction tests should not he to go into ever command if predictions are global and the command does not do anything new. (not looking for a code change, but just curious if these are here because other commands had them) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah most of the other commands have this boilerplate in it. I think it could be fine to keep or remove in this PR, but probably we should have a separate task to pull out the autocomplete code elsewhere |
||
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have tests where this is used? It feels like this was set up with an idea of looking for some errors, so it would be great to see some added. but if not, this could jut be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be worth validating the output in the "some config" case, and make sure you see the config you're setting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmurret added!
@ndhanushkodi That was an oversight on my part, I added the actual config check.