-
Notifications
You must be signed in to change notification settings - Fork 325
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add command to
consul-k8s-control-plane
: `gossip-encryption-autogen…
…erate` (#772) * Add the initial gossip-encryption-autogen command stub * Move synopsis and help to the bottom of the command file * Add logging flags to init * Clean up error and logging messages * Add secret struct and some basic tests for it * Only require secret name to be set * Add test for flag validation * Generate a 32 byte secret value * Add kubernetes client to command * Write the secret to Kubernetes * Re-order flags * Add required namespace flag logic * Test for namespace flag and log flag errors * Delete secret * Secret creation and storage brought into command * Safe exit if secret already exists * Add context to the command * Rename k8s to k8sFlags * Add some nice tests * Inline functions * Move init to the tippy-top * Use Sprintf instead of Errorf...Error() * Move initialization of err closer to useage * Remove client check in secret exists check * Remove unneeded else * Rename SafeFail to EarlyTerminationWithSuccessCode * Add a message on success * Test secret generation from the outside * Add changelog entry * Update CHANGELOG.md Co-authored-by: Kyle Schochenmaier <kschoche@gmail.com> * Grammar fix in changelog * Update comments and synopsis for clarity * Clarify the does secret exists method * Rename doesK8sSecretExist to doesKubernetesSecretExist * Some polish to make it sing 😘 🤌 * Update control-plane/subcommand/gossip-encryption-autogenerate/command.go Co-authored-by: Nitya Dhanushkodi <nitya@hashicorp.com> Co-authored-by: Kyle Schochenmaier <kschoche@gmail.com> Co-authored-by: Nitya Dhanushkodi <nitya@hashicorp.com>
- Loading branch information
1 parent
56cbbb0
commit 00082a0
Showing
4 changed files
with
321 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
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
211 changes: 211 additions & 0 deletions
211
control-plane/subcommand/gossip-encryption-autogenerate/command.go
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,211 @@ | ||
package gossipencryptionautogenerate | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"flag" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/hashicorp/consul-k8s/control-plane/subcommand" | ||
"github.com/hashicorp/consul-k8s/control-plane/subcommand/common" | ||
"github.com/hashicorp/consul-k8s/control-plane/subcommand/flags" | ||
"github.com/hashicorp/go-hclog" | ||
"github.com/mitchellh/cli" | ||
v1 "k8s.io/api/core/v1" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
) | ||
|
||
type Command struct { | ||
UI cli.Ui | ||
|
||
flags *flag.FlagSet | ||
k8s *flags.K8SFlags | ||
|
||
// These flags determine where the Kubernetes secret will be stored. | ||
flagNamespace string | ||
flagSecretName string | ||
flagSecretKey string | ||
|
||
flagLogLevel string | ||
flagLogJSON bool | ||
|
||
k8sClient kubernetes.Interface | ||
|
||
log hclog.Logger | ||
once sync.Once | ||
ctx context.Context | ||
help string | ||
} | ||
|
||
// init is run once to set up usage documentation for flags. | ||
func (c *Command) init() { | ||
c.flags = flag.NewFlagSet("", flag.ContinueOnError) | ||
|
||
c.flags.StringVar(&c.flagLogLevel, "log-level", "info", | ||
"Log verbosity level. Supported values (in order of detail) are \"trace\", "+ | ||
"\"debug\", \"info\", \"warn\", and \"error\".") | ||
c.flags.BoolVar(&c.flagLogJSON, "log-json", false, "Enable or disable JSON output format for logging.") | ||
c.flags.StringVar(&c.flagNamespace, "namespace", "", "Name of Kubernetes namespace where Consul and consul-k8s components are deployed.") | ||
c.flags.StringVar(&c.flagSecretName, "secret-name", "", "Name of the secret to create.") | ||
c.flags.StringVar(&c.flagSecretKey, "secret-key", "key", "Name of the secret key to create.") | ||
|
||
c.k8s = &flags.K8SFlags{} | ||
flags.Merge(c.flags, c.k8s.Flags()) | ||
|
||
c.help = flags.Usage(help, c.flags) | ||
} | ||
|
||
// Run parses input and creates a gossip secret in Kubernetes if none exists at the given namespace and secret name. | ||
func (c *Command) Run(args []string) int { | ||
c.once.Do(c.init) | ||
|
||
if err := c.flags.Parse(args); err != nil { | ||
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) | ||
return 1 | ||
} | ||
|
||
if err := c.validateFlags(); err != nil { | ||
c.UI.Error(fmt.Sprintf("Failed to validate flags: %v", err)) | ||
return 1 | ||
} | ||
|
||
var err error | ||
c.log, err = common.Logger(c.flagLogLevel, c.flagLogJSON) | ||
if err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
if c.ctx == nil { | ||
c.ctx = context.Background() | ||
} | ||
|
||
if c.k8sClient == nil { | ||
if err = c.createKubernetesClient(); err != nil { | ||
c.UI.Error(fmt.Sprintf("Failed to create Kubernetes client: %v", err)) | ||
return 1 | ||
} | ||
} | ||
|
||
if exists, err := c.doesKubernetesSecretExist(); err != nil { | ||
c.UI.Error(fmt.Sprintf("Failed to check if Kubernetes secret exists: %v", err)) | ||
return 1 | ||
} else if exists { | ||
// Safe exit if secret already exists. | ||
c.UI.Info(fmt.Sprintf("A Kubernetes secret with the name `%s` already exists.", c.flagSecretName)) | ||
return 0 | ||
} | ||
|
||
gossipSecret, err := generateGossipSecret() | ||
if err != nil { | ||
c.UI.Error(fmt.Sprintf("Failed to generate gossip secret: %v", err)) | ||
return 1 | ||
} | ||
|
||
// Create the Kubernetes secret object. | ||
kubernetesSecret := v1.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: c.flagSecretName, | ||
Namespace: c.flagNamespace, | ||
}, | ||
Data: map[string][]byte{ | ||
c.flagSecretKey: []byte(gossipSecret), | ||
}, | ||
} | ||
|
||
// Write the secret to Kubernetes. | ||
_, err = c.k8sClient.CoreV1().Secrets(c.flagNamespace).Create(c.ctx, &kubernetesSecret, metav1.CreateOptions{}) | ||
if err != nil { | ||
c.UI.Error(fmt.Sprintf("Failed to create Kubernetes secret: %v", err)) | ||
return 1 | ||
} | ||
|
||
c.UI.Info(fmt.Sprintf("Successfully created Kubernetes secret `%s` in namespace `%s`.", c.flagSecretName, c.flagNamespace)) | ||
return 0 | ||
} | ||
|
||
// Help returns the command's help text. | ||
func (c *Command) Help() string { | ||
c.once.Do(c.init) | ||
return c.help | ||
} | ||
|
||
// Synopsis returns a one-line synopsis of the command. | ||
func (c *Command) Synopsis() string { | ||
return synopsis | ||
} | ||
|
||
// validateFlags ensures that all required flags are set. | ||
func (c *Command) validateFlags() error { | ||
if c.flagNamespace == "" { | ||
return fmt.Errorf("-namespace must be set") | ||
} | ||
|
||
if c.flagSecretName == "" { | ||
return fmt.Errorf("-secret-name must be set") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// createKubernetesClient creates a Kubernetes client on the command object. | ||
func (c *Command) createKubernetesClient() error { | ||
config, err := subcommand.K8SConfig(c.k8s.KubeConfig()) | ||
if err != nil { | ||
return fmt.Errorf("failed to create Kubernetes config: %v", err) | ||
} | ||
|
||
c.k8sClient, err = kubernetes.NewForConfig(config) | ||
if err != nil { | ||
return fmt.Errorf("error initializing Kubernetes client: %s", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// doesKubernetesSecretExist checks if a secret with the given name exists in the given namespace. | ||
func (c *Command) doesKubernetesSecretExist() (bool, error) { | ||
_, err := c.k8sClient.CoreV1().Secrets(c.flagNamespace).Get(c.ctx, c.flagSecretName, metav1.GetOptions{}) | ||
|
||
// If the secret does not exist, the error will be a NotFound error. | ||
if err != nil && apierrors.IsNotFound(err) { | ||
return false, nil | ||
} | ||
|
||
// If the error is not a NotFound error, return the error. | ||
if err != nil && !apierrors.IsNotFound(err) { | ||
return false, fmt.Errorf("failed to get Kubernetes secret: %v", err) | ||
} | ||
|
||
// The secret exists. | ||
return true, nil | ||
} | ||
|
||
// generateGossipSecret generates a random 32 byte secret returned as a base64 encoded string. | ||
func generateGossipSecret() (string, error) { | ||
// This code was copied from Consul's Keygen command: | ||
// https://github.com/hashicorp/consul/blob/d652cc86e3d0322102c2b5e9026c6a60f36c17a5/command/keygen/keygen.go | ||
|
||
key := make([]byte, 32) | ||
n, err := rand.Reader.Read(key) | ||
|
||
if err != nil { | ||
return "", fmt.Errorf("error reading random data: %s", err) | ||
} | ||
if n != 32 { | ||
return "", fmt.Errorf("couldn't read enough entropy") | ||
} | ||
|
||
return base64.StdEncoding.EncodeToString(key), nil | ||
} | ||
|
||
const synopsis = "Generate and store a secret for gossip encryption." | ||
const help = ` | ||
Usage: consul-k8s-control-plane gossip-encryption-autogenerate [options] | ||
Bootstraps the installation with a secret for gossip encryption. | ||
` |
103 changes: 103 additions & 0 deletions
103
control-plane/subcommand/gossip-encryption-autogenerate/command_test.go
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,103 @@ | ||
package gossipencryptionautogenerate | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/mitchellh/cli" | ||
"github.com/stretchr/testify/require" | ||
v1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes/fake" | ||
) | ||
|
||
func TestRun_FlagValidation(t *testing.T) { | ||
t.Parallel() | ||
cases := []struct { | ||
flags []string | ||
expErr string | ||
}{ | ||
{ | ||
flags: []string{}, | ||
expErr: "-namespace must be set", | ||
}, | ||
{ | ||
flags: []string{"-namespace", "default"}, | ||
expErr: "-secret-name must be set", | ||
}, | ||
{ | ||
flags: []string{"-namespace", "default", "-secret-name", "my-secret", "-log-level", "oak"}, | ||
expErr: "unknown log level", | ||
}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(c.expErr, func(t *testing.T) { | ||
ui := cli.NewMockUi() | ||
cmd := Command{ | ||
UI: ui, | ||
} | ||
code := cmd.Run(c.flags) | ||
require.Equal(t, 1, code) | ||
require.Contains(t, ui.ErrorWriter.String(), c.expErr) | ||
}) | ||
} | ||
} | ||
|
||
func TestRun_EarlyTerminationWithSuccessCodeIfSecretExists(t *testing.T) { | ||
namespace := "default" | ||
secretName := "my-secret" | ||
secretKey := "my-secret-key" | ||
|
||
ui := cli.NewMockUi() | ||
k8s := fake.NewSimpleClientset() | ||
|
||
cmd := Command{UI: ui, k8sClient: k8s} | ||
|
||
// Create a secret. | ||
secret := v1.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: secretName, | ||
Namespace: namespace, | ||
}, | ||
Data: map[string][]byte{ | ||
secretKey: []byte(secretKey), | ||
}, | ||
} | ||
_, err := k8s.CoreV1().Secrets(namespace).Create(context.Background(), &secret, metav1.CreateOptions{}) | ||
require.NoError(t, err) | ||
|
||
// Run the command. | ||
flags := []string{"-namespace", namespace, "-secret-name", secretName, "-secret-key", secretKey} | ||
code := cmd.Run(flags) | ||
|
||
require.Equal(t, 0, code) | ||
require.Contains(t, ui.OutputWriter.String(), fmt.Sprintf("A Kubernetes secret with the name `%s` already exists.", secretName)) | ||
} | ||
|
||
func TestRun_SecretIsGeneratedIfNoneExists(t *testing.T) { | ||
namespace := "default" | ||
secretName := "my-secret" | ||
secretKey := "my-secret-key" | ||
|
||
ui := cli.NewMockUi() | ||
k8s := fake.NewSimpleClientset() | ||
|
||
cmd := Command{UI: ui, k8sClient: k8s} | ||
|
||
// Run the command. | ||
flags := []string{"-namespace", namespace, "-secret-name", secretName, "-secret-key", secretKey} | ||
code := cmd.Run(flags) | ||
|
||
require.Equal(t, 0, code) | ||
require.Contains(t, ui.OutputWriter.String(), fmt.Sprintf("Successfully created Kubernetes secret `%s` in namespace `%s`.", secretName, namespace)) | ||
|
||
// Check the secret was created. | ||
secret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{}) | ||
require.NoError(t, err) | ||
gossipSecret, err := base64.StdEncoding.DecodeString(string(secret.Data[secretKey])) | ||
require.NoError(t, err) | ||
require.Len(t, gossipSecret, 32) | ||
} |