Skip to content

Commit

Permalink
add config read command (#2078)
Browse files Browse the repository at this point in the history
* add config read command

* add tests

* lint

* update docs

* add changelog

* fix linting errors

* PR feedback
  • Loading branch information
hanshasselberg authored and absolutelightning committed Aug 4, 2023
1 parent cb7a266 commit 3717ded
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 0 deletions.
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

0 comments on commit 3717ded

Please sign in to comment.