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

cli: support install command #713

Merged
merged 4 commits into from
Sep 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
## UNRELEASED
FEATURES:
* CLI
* The `consul-k8s` CLI enables users to deploy and operate Consul on Kubernetes.
* Support `consul-k8s install` command. [[GH-713](https://github.com/hashicorp/consul-k8s/pull/713)]

IMPROVEMENTS:
* Helm Chart
Expand Down
83 changes: 83 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Consul Kubernetes CLI
This repository contains a CLI tool for installing and operating [Consul](https://www.consul.io/) on Kubernetes.
**Warning** this tool is currently experimental. Do not use it on Consul clusters you care about.

## Installation & Setup
Currently the tool is not available on any releases page. Instead clone the repository and run `go build -o bin/consul-k8s`
and proceed to run the binary.

## Commands
* [consul-k8s install](#consul-k8s-install)

### consul-k8s install
This command installs Consul on a Kubernetes cluster. It allows `demo` and `secure` installations via preset configurations
using the `-preset` flag. The `demo` installation installs just a single replica server with sidecar injection enabled and
is useful to test out service mesh functionality. The `secure` installation is minimal like `demo` but also enables ACLs and TLS.

Get started with:
```bash
consul-k8s install -preset=demo
```

Note that when configuring an installation, the precedence order is as follows from lowest to highest precedence:
1. `-preset`
2. `-f`
3. `-set`
4. `-set-string`
5. `-set-file`

For example, `-set-file` will override a value provided via `-set`. Additionally, within each of these groups the
rightmost flag value has the highest precedence, i.e `-set foo=bar -set foo=baz` will result in `foo: baz` being set.

```
Usage: consul-k8s install [flags]
Install Consul onto a Kubernetes cluster.

Command Options:

-auto-approve
Skip confirmation prompt. The default is false.

-config-file=<string>
Path to a file to customize the installation, such as Consul Helm chart
values file. Can be specified multiple times. This is aliased as "-f".

-dry-run
Run pre-install checks and display summary of installation. The default
is false.

-namespace=<string>
Namespace for the Consul installation. The default is consul.

-preset=<string>
Use an installation preset, one of demo, secure. Defaults to none

-set=<string>
Set a value to customize. Can be specified multiple times. Supports
Consul Helm chart values.

-set-file=<string>
Set a value to customize via a file. The contents of the file will be
set as the value. Can be specified multiple times. Supports Consul Helm
chart values.

-set-string=<string>
Set a string value to customize. Can be specified multiple times.
Supports Consul Helm chart values.

-timeout=<string>
Timeout to wait for installation to be ready. The default is 10m.

-wait
Determines whether to wait for resources in installation to be ready
before exiting command. The default is true.

Global Options:

ndhanushkodi marked this conversation as resolved.
Show resolved Hide resolved
-context=<string>
Kubernetes context to use.

-kubeconfig=<string>
Path to kubeconfig file. This is aliased as "-c".

```
44 changes: 44 additions & 0 deletions cli/cmd/common/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package common

import (
"context"
"io"

"github.com/hashicorp/consul-k8s/cli/cmd/common/terminal"
"github.com/hashicorp/go-hclog"
)

// BaseCommand is embedded in all commands to provide common logic and data.
type BaseCommand struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this pattern SO much!!

// Ctx is the base context for the command. It is up to commands to
// utilize this context so that cancellation works in a timely manner.
Ctx context.Context

// Log is the logger to use.
Log hclog.Logger

// UI is used to write to the CLI.
UI terminal.UI
}

// Close cleans up any resources that the command created. This should be
// defered by any CLI command that embeds baseCommand in the Run command.
func (c *BaseCommand) Close() error {
// Close our UI if it implements it. The glint-based UI does for example
// to finish up all the CLI output.
var err error
if closer, ok := c.UI.(io.Closer); ok && closer != nil {
err = closer.Close()
}
if err != nil {
return err
}

return nil
}

// Init should be called FIRST within the Run function implementation.
func (c *BaseCommand) Init() {
ui := terminal.NewBasicUI(c.Ctx)
c.UI = ui
}
5 changes: 5 additions & 0 deletions cli/cmd/common/flag/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Package flag is a thin layer over the stdlib flag package that provides
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ reviewers: you don't need to review all of the files in this package unless you want to see how it works since it is from another package

// some minimal features such as aliasing, autocompletion handling, improved
// defaults, etc. It was created for mitchellh/cli but can work as a standalone
// package. Source: https://github.com/hashicorp/waypoint/tree/348d2c77fce199952618ccef6433c8844b22583b/internal/pkg/flag, or release tag 0.5.1.
package flag
74 changes: 74 additions & 0 deletions cli/cmd/common/flag/flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package flag

import (
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/kr/text"
)

// maxLineLength is the maximum width of any line.
const maxLineLength int = 78

// reRemoveWhitespace is a regular expression for stripping whitespace from
// a string.
var reRemoveWhitespace = regexp.MustCompile(`[\s]+`)

// FlagExample is an interface which declares an example value. This is
// used in help generation to provide better help text.
type FlagExample interface {
Example() string
}

// FlagVisibility is an interface which declares whether a flag should be
// hidden from help and completions. This is usually used for deprecations
// on "internal-only" flags.
type FlagVisibility interface {
Hidden() bool
}

// helpers

func envDefault(key, def string) string {
if v, exist := os.LookupEnv(key); exist {
return v
}
return def
}

func envBoolDefault(key string, def bool) bool {
if v, exist := os.LookupEnv(key); exist {
b, err := strconv.ParseBool(v)
if err != nil {
panic(err)
}
return b
}
return def
}

func envDurationDefault(key string, def time.Duration) time.Duration {
if v, exist := os.LookupEnv(key); exist {
d, err := time.ParseDuration(v)
if err != nil {
panic(err)
}
return d
}
return def
}

// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking
// into account any provided left padding.
func wrapAtLengthWithPadding(s string, pad int) string {
wrapped := text.Wrap(s, maxLineLength-pad)
lines := strings.Split(wrapped, "\n")
for i, line := range lines {
lines[i] = strings.Repeat(" ", pad) + line
}

return strings.Join(lines, "\n")
}
77 changes: 77 additions & 0 deletions cli/cmd/common/flag/flag_bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package flag

import (
"os"
"strconv"

"github.com/posener/complete"
)

// -- BoolVar and boolValue
type BoolVar struct {
Name string
Aliases []string
Usage string
Default bool
Hidden bool
EnvVar string
Target *bool
Completion complete.Predictor
SetHook func(val bool)
}

func (f *Set) BoolVar(i *BoolVar) {
def := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
if b, err := strconv.ParseBool(v); err == nil {
def = b
}
}

f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: strconv.FormatBool(i.Default),
EnvVar: i.EnvVar,
Value: newBoolValue(i, def, i.Target, i.Hidden),
Completion: i.Completion,
})
}

type boolValue struct {
v *BoolVar
hidden bool
target *bool
}

func newBoolValue(v *BoolVar, def bool, target *bool, hidden bool) *boolValue {
*target = def

return &boolValue{
v: v,
hidden: hidden,
target: target,
}
}

func (b *boolValue) Set(s string) error {
v, err := strconv.ParseBool(s)
if err != nil {
return err
}

*b.target = v

if b.v.SetHook != nil {
b.v.SetHook(v)
}

return nil
}

func (b *boolValue) Get() interface{} { return *b.target }
func (b *boolValue) String() string { return strconv.FormatBool(*b.target) }
func (b *boolValue) Example() string { return "" }
func (b *boolValue) Hidden() bool { return b.hidden }
func (b *boolValue) IsBoolFlag() bool { return true }
90 changes: 90 additions & 0 deletions cli/cmd/common/flag/flag_enum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package flag

import (
"fmt"
"os"
"strings"

"github.com/posener/complete"
)

// -- EnumVar and enumValue
type EnumVar struct {
Name string
Aliases []string
Usage string
Values []string
Default []string
Hidden bool
EnvVar string
Target *[]string
Completion complete.Predictor
}

func (f *Set) EnumVar(i *EnumVar) {
initial := i.Default
if v, exist := os.LookupEnv(i.EnvVar); exist {
parts := strings.Split(v, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
initial = parts
}

def := ""
if i.Default != nil {
def = strings.Join(i.Default, ",")
}

possible := strings.Join(i.Values, ", ")

f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: strings.TrimRight(i.Usage, ". \t") + ". One possible value from: " + possible + ".",
Default: def,
EnvVar: i.EnvVar,
Value: newEnumValue(i, initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}

type enumValue struct {
ev *EnumVar
hidden bool
target *[]string
}

func newEnumValue(ev *EnumVar, def []string, target *[]string, hidden bool) *enumValue {
*target = def
return &enumValue{
ev: ev,
hidden: hidden,
target: target,
}
}

func (s *enumValue) Set(vals string) error {
parts := strings.Split(vals, ",")

parts:
for _, val := range parts {
val = strings.TrimSpace(val)

for _, p := range s.ev.Values {
if p == val {
*s.target = append(*s.target, strings.TrimSpace(val))
continue parts
}
}

return fmt.Errorf("'%s' not valid. Must be one of: %s", val, strings.Join(s.ev.Values, ", "))
}

return nil
}

func (s *enumValue) Get() interface{} { return *s.target }
func (s *enumValue) String() string { return strings.Join(*s.target, ",") }
func (s *enumValue) Example() string { return "string" }
func (s *enumValue) Hidden() bool { return s.hidden }
Loading