Add document command and its first subcommand
This adds the "airshipctl document" tree of commands, as well as one of its leaf commands, "generate masterpassphrase". Change-Id: I2365ebe67b38ebbbe4873c6e1beb6407f0e000eb
This commit is contained in:
parent
1c999e2095
commit
9e2d6932f6
20
cmd/document/document.go
Normal file
20
cmd/document/document.go
Normal file
@ -0,0 +1,20 @@
|
||||
package document
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"opendev.org/airship/airshipctl/cmd/document/secret"
|
||||
"opendev.org/airship/airshipctl/pkg/environment"
|
||||
)
|
||||
|
||||
// NewDocumentCommand creates a new command for managing airshipctl documents
|
||||
func NewDocumentCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Command {
|
||||
documentRootCmd := &cobra.Command{
|
||||
Use: "document",
|
||||
Short: "manages deployment documents",
|
||||
}
|
||||
|
||||
documentRootCmd.AddCommand(secret.NewSecretCommand(rootSettings))
|
||||
|
||||
return documentRootCmd
|
||||
}
|
20
cmd/document/secret/generate/generate.go
Normal file
20
cmd/document/secret/generate/generate.go
Normal file
@ -0,0 +1,20 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/environment"
|
||||
)
|
||||
|
||||
// NewGenerateCommand creates a new command for generating secret information
|
||||
func NewGenerateCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Command {
|
||||
generateRootCmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
// TODO(howell): Make this more expressive
|
||||
Short: "generates various secrets",
|
||||
}
|
||||
|
||||
generateRootCmd.AddCommand(NewGenerateMasterPassphraseCommand(rootSettings))
|
||||
|
||||
return generateRootCmd
|
||||
}
|
26
cmd/document/secret/generate/masterpassphrase.go
Normal file
26
cmd/document/secret/generate/masterpassphrase.go
Normal file
@ -0,0 +1,26 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/environment"
|
||||
"opendev.org/airship/airshipctl/pkg/secret"
|
||||
)
|
||||
|
||||
// NewGenerateMasterPassphraseCommand creates a new command for generating secret information
|
||||
func NewGenerateMasterPassphraseCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Command {
|
||||
masterPassphraseCmd := &cobra.Command{
|
||||
Use: "masterpassphrase",
|
||||
// TODO(howell): Make this more expressive
|
||||
Short: "generates a secure master passphrase",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
engine := secret.NewPassphraseEngine(nil)
|
||||
masterPassphrase := engine.GeneratePassphrase()
|
||||
fmt.Fprintln(cmd.OutOrStdout(), masterPassphrase)
|
||||
},
|
||||
}
|
||||
|
||||
return masterPassphraseCmd
|
||||
}
|
21
cmd/document/secret/secret.go
Normal file
21
cmd/document/secret/secret.go
Normal file
@ -0,0 +1,21 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"opendev.org/airship/airshipctl/cmd/document/secret/generate"
|
||||
"opendev.org/airship/airshipctl/pkg/environment"
|
||||
)
|
||||
|
||||
// NewSecretCommand creates a new command for managing airshipctl secrets
|
||||
func NewSecretCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Command {
|
||||
secretRootCmd := &cobra.Command{
|
||||
Use: "secret",
|
||||
// TODO(howell): Make this more expressive
|
||||
Short: "manages secrets",
|
||||
}
|
||||
|
||||
secretRootCmd.AddCommand(generate.NewGenerateCommand(rootSettings))
|
||||
|
||||
return secretRootCmd
|
||||
}
|
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"opendev.org/airship/airshipctl/cmd/bootstrap"
|
||||
"opendev.org/airship/airshipctl/cmd/completion"
|
||||
"opendev.org/airship/airshipctl/cmd/document"
|
||||
"opendev.org/airship/airshipctl/pkg/environment"
|
||||
"opendev.org/airship/airshipctl/pkg/log"
|
||||
)
|
||||
@ -51,16 +52,10 @@ func AddDefaultAirshipCTLCommands(cmd *cobra.Command, settings *environment.Airs
|
||||
cmd.AddCommand(argo.NewCommand())
|
||||
cmd.AddCommand(bootstrap.NewBootstrapCommand(settings))
|
||||
cmd.AddCommand(completion.NewCompletionCommand())
|
||||
cmd.AddCommand(document.NewDocumentCommand(settings))
|
||||
cmd.AddCommand(kubectl.NewDefaultKubectlCommand())
|
||||
// Should we use cmd.OutOrStdout?
|
||||
cmd.AddCommand(kubeadm.NewKubeadmCommand(os.Stdin, os.Stdout, os.Stderr))
|
||||
|
||||
kustomizeCmd, _, err := cmd.Find([]string{"kubectl", "kustomize"})
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to find subcommand '%s'", err.Error())
|
||||
}
|
||||
|
||||
cmd.AddCommand(kustomizeCmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ Available Commands:
|
||||
argo argo is the command line interface to Argo
|
||||
bootstrap bootstraps airshipctl
|
||||
completion Generate autocompletions script for the specified shell (bash or zsh)
|
||||
document manages deployment documents
|
||||
help Help about any command
|
||||
kubeadm kubeadm: easily bootstrap a secure Kubernetes cluster
|
||||
kubectl kubectl controls the Kubernetes cluster manager
|
||||
kustomize Build a kustomization target from a directory or a remote url.
|
||||
version Show the version number of airshipctl
|
||||
|
||||
Flags:
|
||||
|
98
pkg/secret/passphrases.go
Normal file
98
pkg/secret/passphrases.go
Normal file
@ -0,0 +1,98 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLength = 24
|
||||
|
||||
asciiLowers = "abcdefghijklmnopqrstuvwxyz"
|
||||
asciiUppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
asciiNumbers = "0123456789"
|
||||
asciiSymbols = "@#&-+=?"
|
||||
)
|
||||
|
||||
var (
|
||||
// pool is the complete collection of characters that can be used for a
|
||||
// passphrase
|
||||
pool []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
pool = append(pool, []byte(asciiLowers)...)
|
||||
pool = append(pool, []byte(asciiUppers)...)
|
||||
pool = append(pool, []byte(asciiNumbers)...)
|
||||
pool = append(pool, []byte(asciiSymbols)...)
|
||||
}
|
||||
|
||||
// PassphraseEngine is used to generate secure random passphrases
|
||||
type PassphraseEngine struct {
|
||||
rng *rand.Rand
|
||||
pool []byte
|
||||
}
|
||||
|
||||
// NewPassphraseEngine creates an PassphraseEngine using src. If src is nil,
|
||||
// the returned PassphraseEngine will use the default Source
|
||||
func NewPassphraseEngine(src rand.Source) *PassphraseEngine {
|
||||
if src == nil {
|
||||
src = &Source{}
|
||||
}
|
||||
return &PassphraseEngine{
|
||||
rng: rand.New(src),
|
||||
pool: pool,
|
||||
}
|
||||
}
|
||||
|
||||
// GeneratePassphrase returns a secure random string of length 24,
|
||||
// containing at least one of each from the following sets:
|
||||
// [0-9]
|
||||
// [a-z]
|
||||
// [A-Z]
|
||||
// [@#&-+=?]
|
||||
func (e *PassphraseEngine) GeneratePassphrase() string {
|
||||
return e.GeneratePassphraseN(defaultLength)
|
||||
}
|
||||
|
||||
// GeneratePassphraseN returns a secure random string containing at least
|
||||
// one of each from the following sets. Its length will be max(length, 24)
|
||||
// [0-9]
|
||||
// [a-z]
|
||||
// [A-Z]
|
||||
// [@#&-+=?]
|
||||
func (e *PassphraseEngine) GeneratePassphraseN(length int) string {
|
||||
if length < defaultLength {
|
||||
length = defaultLength
|
||||
}
|
||||
var passPhrase string
|
||||
for !e.isValidPassphrase(passPhrase) {
|
||||
var sb strings.Builder
|
||||
for i := 0; i < length; i++ {
|
||||
randIndex := e.rng.Intn(len(e.pool))
|
||||
randChar := e.pool[randIndex]
|
||||
sb.WriteString(string(randChar))
|
||||
}
|
||||
passPhrase = sb.String()
|
||||
}
|
||||
return passPhrase
|
||||
}
|
||||
|
||||
func (e *PassphraseEngine) isValidPassphrase(passPhrase string) bool {
|
||||
if len(passPhrase) < defaultLength {
|
||||
return false
|
||||
}
|
||||
|
||||
charSets := []string{
|
||||
asciiLowers,
|
||||
asciiUppers,
|
||||
asciiNumbers,
|
||||
asciiSymbols,
|
||||
}
|
||||
for _, charSet := range charSets {
|
||||
if !strings.ContainsAny(passPhrase, charSet) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
109
pkg/secret/passphrases_test.go
Normal file
109
pkg/secret/passphrases_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
package secret_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/secret"
|
||||
)
|
||||
|
||||
const (
|
||||
asciiLowers = "abcdefghijklmnopqrstuvwxyz"
|
||||
asciiUppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
asciiNumbers = "0123456789"
|
||||
asciiSymbols = "@#&-+=?"
|
||||
|
||||
defaultLength = 24
|
||||
)
|
||||
|
||||
func TestDeterministicGenerateValidPassphrase(t *testing.T) {
|
||||
testSource := rand.NewSource(42)
|
||||
engine := secret.NewPassphraseEngine(testSource)
|
||||
|
||||
// pre-calculated for rand.NewSource(42)
|
||||
expectedPassphrases := []string{
|
||||
"erx&97vfqd7LN3HJ?t@oPhds",
|
||||
"##Xeuvf5Njy@hNWSaRoleFkf",
|
||||
"jB=kirg7acIt-=fx1Fb-tZ+7",
|
||||
"eOS#W8yoAljSThPL2oT&aUZu",
|
||||
"vlaQqKr-jXSCJfXYnvGik3b1",
|
||||
"rBKtHZkOmFUM75?c2UWiZjdh",
|
||||
"9g?QV?w6BCWN2EKAc+dZ-Jun",
|
||||
"X@IIyqAg7Mz@Wm8eRE6KMEf3",
|
||||
"7JpQkLd3ufhj4bLB8S=ipjNP",
|
||||
"XC?bDaHTa3mrBYLMG@#B=Q0B",
|
||||
}
|
||||
|
||||
for i, expected := range expectedPassphrases {
|
||||
actual := engine.GeneratePassphrase()
|
||||
if expected != actual {
|
||||
t.Errorf("Call #%d to engine.GeneratePassphrase() should have returned %s, got %s",
|
||||
i, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNondeterministicGenerateValidPassphrase(t *testing.T) {
|
||||
// Due to the nondeterminism of random number generators, this
|
||||
// functionality is impossible to fully test. Let's just generate
|
||||
// enough passphrases that we can be confident in the randomness.
|
||||
// Fortunately, Go is pretty fast, so we can do upward of 100,000 of
|
||||
// these without slowing down the test too much
|
||||
engine := secret.NewPassphraseEngine(nil)
|
||||
for i := 0; i < 100000; i++ {
|
||||
passphrase := engine.GeneratePassphrase()
|
||||
if !isValid(passphrase) {
|
||||
t.Errorf("The engine generated an invalid password: %s", passphrase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateValidPassphraseN(t *testing.T) {
|
||||
testSource := rand.NewSource(42)
|
||||
engine := secret.NewPassphraseEngine(testSource)
|
||||
tests := []struct {
|
||||
inputLegth int
|
||||
expectedLength int
|
||||
}{
|
||||
{
|
||||
inputLegth: 10,
|
||||
expectedLength: defaultLength,
|
||||
},
|
||||
{
|
||||
inputLegth: -5,
|
||||
expectedLength: defaultLength,
|
||||
},
|
||||
{
|
||||
inputLegth: 30,
|
||||
expectedLength: 30,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
passphrase := engine.GeneratePassphraseN(tt.inputLegth)
|
||||
if len(passphrase) != tt.expectedLength {
|
||||
t.Errorf(`Passphrase "%s" should have length %d, got %d\n`,
|
||||
passphrase, len(passphrase), tt.expectedLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isValid(passphrase string) bool {
|
||||
if len(passphrase) < defaultLength {
|
||||
return false
|
||||
}
|
||||
|
||||
charSets := []string{
|
||||
asciiLowers,
|
||||
asciiUppers,
|
||||
asciiNumbers,
|
||||
asciiSymbols,
|
||||
}
|
||||
for _, charSet := range charSets {
|
||||
if !strings.ContainsAny(passphrase, charSet) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
33
pkg/secret/rngsource.go
Normal file
33
pkg/secret/rngsource.go
Normal file
@ -0,0 +1,33 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
crypto "crypto/rand"
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
|
||||
"opendev.org/airship/airshipctl/pkg/log"
|
||||
)
|
||||
|
||||
// Source implements rand.Source
|
||||
type Source struct{}
|
||||
|
||||
var _ rand.Source = &Source{}
|
||||
|
||||
// Uint64 returns a secure random uint64 in the range [0, 1<<64]. It will fail
|
||||
// if an error is returned from the system's secure random numer generator
|
||||
func (s *Source) Uint64() uint64 {
|
||||
var value uint64
|
||||
err := binary.Read(crypto.Reader, binary.BigEndian, &value)
|
||||
if err != nil {
|
||||
log.Fatalf("could not generate a random number: %s", err.Error())
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Int63 returns a secure random int64 in the range [0, 1<<63]
|
||||
func (s *Source) Int63() int64 {
|
||||
return int64(s.Uint64() & ^(uint64(1 << 63)))
|
||||
}
|
||||
|
||||
// Seed does nothing, since Source will use the crypto library
|
||||
func (s *Source) Seed(_ int64) {}
|
Loading…
Reference in New Issue
Block a user