Browse Source

add config management commands

Anish Mukherjee 2 years ago
parent
commit
fa9b7643cb

+ 21 - 0
cli/cmd/context/delete.go

@@ -0,0 +1,21 @@
+package context
+
+import (
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+// contextDeleteCmd deletes a contex
+var contextDeleteCmd = &cobra.Command{
+	Use:   "delete [NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Delete a context",
+	Long:  `Delete a context`,
+	Run: func(cmd *cobra.Command, args []string) {
+		config.DeleteContext(args[0])
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(contextDeleteCmd)
+}

+ 21 - 0
cli/cmd/context/list.go

@@ -0,0 +1,21 @@
+package context
+
+import (
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+// contextListCmd lists all contexts
+var contextListCmd = &cobra.Command{
+	Use:   "list",
+	Args:  cobra.NoArgs,
+	Short: "List all contexts",
+	Long:  `List all contexts`,
+	Run: func(cmd *cobra.Command, args []string) {
+		config.ListAll()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(contextListCmd)
+}

+ 37 - 0
cli/cmd/context/root.go

@@ -0,0 +1,37 @@
+package context
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "context",
+	Short: "Manage various netmaker server configurations",
+	Long:  `Manage various netmaker server configurations`,
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 55 - 0
cli/cmd/context/set.go

@@ -0,0 +1,55 @@
+package context
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+const (
+	FlagEndpoint  = "endpoint"
+	FlagUsername  = "username"
+	FlagPassword  = "password"
+	FlagMasterKey = "master_key"
+)
+
+var (
+	endpoint  string
+	username  string
+	password  string
+	masterKey string
+)
+
+// contextSetCmd creates/updates a context
+var contextSetCmd = &cobra.Command{
+	Use:   fmt.Sprintf("set [NAME] [--%s=https://api.netmaker.io] [--%s=admin] [--%s=pass] [--%s=secret]", FlagEndpoint, FlagUsername, FlagPassword, FlagMasterKey),
+	Args:  cobra.ExactArgs(1),
+	Short: "Create a context or update an existing one",
+	Long:  `Create a context or update an existing one`,
+	Run: func(cmd *cobra.Command, args []string) {
+		ctx := config.Context{
+			Endpoint:  endpoint,
+			Username:  username,
+			Password:  password,
+			MasterKey: masterKey,
+		}
+		if ctx.Username == "" && ctx.MasterKey == "" {
+			cmd.Usage()
+			log.Fatal("Either username/password or master key is required")
+		}
+		config.SetContext(args[0], ctx)
+	},
+}
+
+func init() {
+	contextSetCmd.Flags().StringVar(&endpoint, FlagEndpoint, "", "Endpoint of the API Server (Required)")
+	contextSetCmd.MarkFlagRequired(FlagEndpoint)
+	contextSetCmd.Flags().StringVar(&username, FlagUsername, "", "Username")
+	contextSetCmd.Flags().StringVar(&password, FlagPassword, "", "Password")
+	contextSetCmd.MarkFlagsRequiredTogether(FlagUsername, FlagPassword)
+	contextSetCmd.Flags().StringVar(&masterKey, FlagMasterKey, "", "Master Key")
+
+	rootCmd.AddCommand(contextSetCmd)
+}

+ 21 - 0
cli/cmd/context/use.go

@@ -0,0 +1,21 @@
+package context
+
+import (
+	"github.com/gravitl/netmaker/cli/config"
+	"github.com/spf13/cobra"
+)
+
+// contextUseCmd sets the current context
+var contextUseCmd = &cobra.Command{
+	Use:   "use [NAME]",
+	Args:  cobra.ExactArgs(1),
+	Short: "Set the current context",
+	Long:  `Set the current context`,
+	Run: func(cmd *cobra.Command, args []string) {
+		config.SetCurrentContext(args[0])
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(contextUseCmd)
+}

+ 27 - 0
cli/cmd/network/create.go

@@ -0,0 +1,27 @@
+package network
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/gravitl/netmaker/cli/functions"
+	"github.com/gravitl/netmaker/models"
+	"github.com/spf13/cobra"
+)
+
+// networkCreateCmd represents the networkCreate command
+var networkCreateCmd = &cobra.Command{
+	Use:   "create [network_definition.json]",
+	Short: "Create a Network",
+	Long:  `Create a Network`,
+	Args:  cobra.ExactArgs(1),
+	Run: func(cmd *cobra.Command, args []string) {
+		network := &models.Network{}
+		resp := functions.CreateNetwork(network)
+		fmt.Fprintf(os.Stdout, "Response from `NetworksApi.CreateNetwork`: %v\n", resp)
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(networkCreateCmd)
+}

+ 39 - 0
cli/cmd/network/root.go

@@ -0,0 +1,39 @@
+package network
+
+import (
+	"os"
+
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "network",
+	Short: "Manage Netmaker Networks",
+	Long:  `Manage Netmaker Networks`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+}

+ 48 - 0
cli/cmd/root.go

@@ -0,0 +1,48 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/gravitl/netmaker/cli/cmd/context"
+	"github.com/gravitl/netmaker/cli/cmd/network"
+	"github.com/spf13/cobra"
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+	Use:   "netmaker",
+	Short: "CLI for interacting with Netmaker Server",
+	Long:  `CLI for interacting with Netmaker Server`,
+	// Uncomment the following line if your bare application
+	// has an action associated with it:
+	// Run: func(cmd *cobra.Command, args []string) { },
+}
+
+func GetRoot() *cobra.Command {
+	return rootCmd
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+	err := rootCmd.Execute()
+	if err != nil {
+		os.Exit(1)
+	}
+}
+
+func init() {
+	// Here you will define your flags and configuration settings.
+	// Cobra supports persistent flags, which, if defined here,
+	// will be global for your application.
+
+	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.tctl.yaml)")
+
+	// Cobra also supports local flags, which will only run
+	// when this action is called directly.
+	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+
+	// IMP: Bind subcommands here
+	rootCmd.AddCommand(network.GetRoot())
+	rootCmd.AddCommand(context.GetRoot())
+}

+ 130 - 0
cli/config/config.go

@@ -0,0 +1,130 @@
+package config
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+
+	"github.com/spf13/viper"
+	"gopkg.in/yaml.v3"
+)
+
+type Context struct {
+	Endpoint  string
+	Username  string
+	Password  string
+	MasterKey string
+	Current   bool `yaml:"current,omitempty"`
+}
+
+var (
+	contextMap     = map[string]Context{}
+	configFilePath string
+	filename       string
+)
+
+func createConfigPathIfNotExists() {
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		log.Fatal(err)
+	}
+	configFilePath = filepath.Join(homeDir, ".netmaker")
+	// create directory if not exists
+	if err := os.MkdirAll(configFilePath, os.ModePerm); err != nil {
+		log.Fatal(err)
+	}
+	filename = filepath.Join(configFilePath, "config.yml")
+	// create file if not exists
+	if _, err := os.Stat(filename); err != nil {
+		if os.IsNotExist(err) {
+			if _, err := os.Create(filename); err != nil {
+				log.Fatalf("Unable to create file filename: %s", err)
+			}
+		} else {
+			log.Fatal(err)
+		}
+	}
+}
+
+func loadConfig() {
+	viper.SetConfigName("config")
+	viper.AddConfigPath(configFilePath)
+	viper.SetConfigType("yml")
+	if err := viper.ReadInConfig(); err != nil {
+		log.Fatalf("Error reading config file: %s", err)
+	}
+	if err := viper.Unmarshal(&contextMap); err != nil {
+		log.Fatalf("Unable to decode into struct: %s", err)
+	}
+}
+
+func GetCurrentContext() (ret Context) {
+	for _, ctx := range contextMap {
+		if ctx.Current {
+			ret = ctx
+			return
+		}
+	}
+	log.Fatalf("No current context set, do so via `netmaker context use <name>`")
+	return
+}
+
+func SaveContext() {
+	bodyBytes, err := yaml.Marshal(&contextMap)
+	if err != nil {
+		log.Fatalf("Error marshalling into YAML %s", err)
+	}
+	file, err := os.Create(filename)
+	if err != nil {
+		log.Fatal(err)
+	}
+	if _, err := file.Write(bodyBytes); err != nil {
+		log.Fatal(err)
+	}
+	if err := file.Close(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func SetCurrentContext(ctxName string) {
+	if _, ok := contextMap[ctxName]; !ok {
+		log.Fatalf("No such context %s", ctxName)
+	}
+	for key, ctx := range contextMap {
+		ctx.Current = key == ctxName
+		contextMap[key] = ctx
+	}
+	SaveContext()
+}
+
+func SetContext(ctxName string, ctx Context) {
+	if oldCtx, ok := contextMap[ctxName]; ok && oldCtx.Current {
+		ctx.Current = true
+	}
+	contextMap[ctxName] = ctx
+	SaveContext()
+}
+
+func DeleteContext(ctxName string) {
+	if _, ok := contextMap[ctxName]; ok {
+		delete(contextMap, ctxName)
+		SaveContext()
+	} else {
+		log.Fatalf("No such context %s", ctxName)
+	}
+}
+
+func ListAll() {
+	for key, ctx := range contextMap {
+		fmt.Print("\n", key, " -> ", ctx.Endpoint)
+		if ctx.Current {
+			fmt.Print(" (current)")
+		}
+	}
+}
+
+func init() {
+	createConfigPathIfNotExists()
+	loadConfig()
+}

+ 12 - 0
cli/functions/auth.go

@@ -0,0 +1,12 @@
+package functions
+
+import (
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+func LoginWithUserAndPassword(username, password string) *models.SuccessResponse {
+	authParams := &models.UserAuthParams{UserName: username, Password: password}
+	return Request[models.SuccessResponse](http.MethodPost, "/api/users/adm/authenticate", authParams)
+}

+ 43 - 0
cli/functions/http_client.go

@@ -0,0 +1,43 @@
+package functions
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"log"
+	"net/http"
+)
+
+func Request[T any](method, route string, payload any) *T {
+	requestURL := "http://localhost:3000"
+	var (
+		req *http.Request
+		err error
+	)
+	if payload == nil {
+		req, err = http.NewRequest(method, requestURL+route, nil)
+	} else {
+		payloadBytes, jsonErr := json.Marshal(payload)
+		if jsonErr != nil {
+			log.Fatalf("Error in request JSON marshalling: %s", err)
+		}
+		req, err = http.NewRequest(method, requestURL+route, bytes.NewReader(payloadBytes))
+	}
+	if err != nil {
+		log.Fatalf("Client could not create request: %s", err)
+	}
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		log.Fatalf("Client error making http request: %s", err)
+	}
+
+	resBodyBytes, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		log.Fatalf("Client could not read response body: %s", err)
+	}
+	body := new(T)
+	if err := json.Unmarshal(resBodyBytes, body); err != nil {
+		log.Fatalf("Error unmarshalling JSON: %s", err)
+	}
+	return body
+}

+ 17 - 0
cli/functions/network.go

@@ -0,0 +1,17 @@
+package functions
+
+import (
+	"net/http"
+
+	"github.com/gravitl/netmaker/models"
+)
+
+// CreateNetwork - creates a network
+func CreateNetwork(payload *models.Network) *models.Network {
+	return Request[models.Network](http.MethodPost, "/api/networks", payload)
+}
+
+// GetNetworks - fetch all networks
+func GetNetworks() *models.Network {
+	return Request[models.Network](http.MethodGet, "/api/networks", nil)
+}

+ 11 - 0
cli/main.go

@@ -0,0 +1,11 @@
+package main
+
+import (
+	"github.com/gravitl/netmaker/cli/cmd"
+	_ "github.com/gravitl/netmaker/cli/config"
+)
+
+func main() {
+
+	cmd.Execute()
+}

+ 11 - 0
go.mod

@@ -43,6 +43,8 @@ require (
 	github.com/agnivade/levenshtein v1.1.1
 	github.com/coreos/go-oidc/v3 v3.4.0
 	github.com/gorilla/websocket v1.5.0
+	github.com/spf13/cobra v1.5.0
+	github.com/spf13/viper v1.8.1
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
 	golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
 )
@@ -74,23 +76,32 @@ require (
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.8 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/josharian/native v1.0.0 // indirect
 	github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/magiconair/properties v1.8.5 // indirect
 	github.com/mdlayher/genetlink v1.2.0 // indirect
 	github.com/mdlayher/netlink v1.6.0 // indirect
 	github.com/mdlayher/socket v0.1.1 // indirect
+	github.com/mitchellh/mapstructure v1.4.1 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.0.2 // indirect
+	github.com/pelletier/go-toml v1.9.3 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rogpeppe/go-internal v1.9.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sirupsen/logrus v1.9.0 // indirect
 	github.com/spf13/afero v1.9.2 // indirect
+	github.com/spf13/cast v1.3.1 // indirect
+	github.com/spf13/jwalterweatherman v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
 	github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
+	github.com/subosito/gotenv v1.2.0 // indirect
 	github.com/tevino/abool v1.2.0 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect

+ 12 - 0
go.sum

@@ -304,6 +304,7 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@@ -311,6 +312,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jackmordaunt/icns/v2 v2.2.1/go.mod h1:6aYIB9eSzyfHHMKqDf17Xrs1zetQPReAkiUSHzdw4cI=
 github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
@@ -340,6 +342,7 @@ github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
 github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@@ -364,6 +367,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
 github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -380,6 +384,7 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB
 github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -426,14 +431,20 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
 github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
 github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
 github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
+github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
 github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM=
 github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
@@ -453,6 +464,7 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
 github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=