Skip to content
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

Backport of add config read command into release/1.1.x #2121

3 changes: 3 additions & 0 deletions .changelog/2078.txt
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.
```
26 changes: 26 additions & 0 deletions cli/cmd/config/command.go
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"
}
199 changes: 199 additions & 0 deletions cli/cmd/config/read/command.go
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."
}
149 changes: 149 additions & 0 deletions cli/cmd/config/read/command_test.go
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
}
Loading