From 3e97414dbe135f7c6547c5e4002616f1f957b512 Mon Sep 17 00:00:00 2001 From: Elis Lulja Date: Wed, 17 Aug 2022 15:45:27 +0200 Subject: [PATCH] cli: create `etcd` command This commit introduces the `etcd` comand under `run`. It is not active yet as it will be in later commits. Signed-off-by: Elis Lulja --- go.mod | 2 + go.sum | 1 + pkg/cluster/kubernetes.go | 14 +++ pkg/command/run/etcd.go | 248 ++++++++++++++++++++++++++++++++++++++ pkg/command/run/run.go | 2 +- 5 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 pkg/command/run/etcd.go diff --git a/go.mod b/go.mod index 4c6352b..71b8f64 100644 --- a/go.mod +++ b/go.mod @@ -81,6 +81,8 @@ require ( sigs.k8s.io/yaml v1.2.0 // indirect ) +require golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 + require ( github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect diff --git a/go.sum b/go.sum index ad52d45..f869889 100644 --- a/go.sum +++ b/go.sum @@ -772,6 +772,7 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/cluster/kubernetes.go b/pkg/cluster/kubernetes.go index ad758b5..3b8d3ee 100644 --- a/pkg/cluster/kubernetes.go +++ b/pkg/cluster/kubernetes.go @@ -201,3 +201,17 @@ func GetFilesFromSecret(ctx context.Context, nsName, name string) ([][]byte, err return data, nil } + +func GetDataFromSecret(ctx context.Context, nsName, name string) (map[string][]byte, error) { + cli, err := getK8sClientSet() + if err != nil { + return nil, err + } + + secret, err := cli.CoreV1().Secrets(nsName).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + return secret.Data, nil +} diff --git a/pkg/command/run/etcd.go b/pkg/command/run/etcd.go new file mode 100644 index 0000000..30d0ef8 --- /dev/null +++ b/pkg/command/run/etcd.go @@ -0,0 +1,248 @@ +// Copyright © 2022 Cisco +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// All rights reserved. + +package run + +import ( + "context" + "fmt" + "io/ioutil" + "strings" + "syscall" + "time" + + "github.com/CloudNativeSDWAN/cnwan-operator/pkg/cluster" + "github.com/CloudNativeSDWAN/cnwan-operator/pkg/servregistry/etcd" + "github.com/davecgh/go-spew/spew" + "github.com/spf13/cobra" + clientv3 "go.etcd.io/etcd/client/v3" + "golang.org/x/term" + "gopkg.in/yaml.v3" +) + +const ( + defaultEtcdConfigMapName = "etcd-options" + defaultEtcdCredentialsSecretName = "etcd-credentials" +) + +type EtcdOptions struct { + Prefix string `yaml:"prefix"` + Endpoints []string `yaml:"endpoints"` + + Username string + Password string +} + +func getRunEtcdCommand(operatorOpts *Options) *cobra.Command { + // ----------------------------- + // Inits and defaults + // ----------------------------- + + opts := &EtcdOptions{} + var ( + optsPath string + optsConfigMap string + // This is used for the --password flag option, to signal user has + // indeed a password for authentication. + tmpPassFlag bool + credentialsSecret string + ) + + // ----------------------------- + // The command + // ----------------------------- + + cmd := &cobra.Command{ + Use: "etcd [COMMAND] [OPTIONS]", + Short: "Run the program with etcd", + PreRunE: func(cmd *cobra.Command, args []string) error { + l := log.With().Str("cmd", "etcd").Logger() + + // -- Get the options from file or ConfigMap + if optsPath != "" || optsConfigMap != "" { + var ( + fileOptions []byte + decodedFileOptions *EtcdOptions + ) + + // -- Get the options from path + if optsPath != "" { + if optsConfigMap != "" { + l.Warn().Msg("both path and configmap flags are provided: only the path will be used") + optsConfigMap = "" + } + + log.Debug().Str("path", optsPath). + Msg("getting options from file...") + byteOpts, err := ioutil.ReadFile(optsPath) + if err != nil { + return fmt.Errorf("cannot open file %s: %w", optsPath, err) + } + + fileOptions = byteOpts + } + + // -- Get options from configmap + if optsConfigMap != "" { + log.Debug(). + Str("namespace", operatorOpts.Namespace). + Str("name", optsConfigMap). + Msg("getting options from configmap...") + + ctx, canc := context.WithTimeout(context.Background(), 10*time.Second) + cfg, err := cluster.GetFilesFromConfigMap(ctx, operatorOpts.Namespace, optsConfigMap) + if err != nil { + canc() + return fmt.Errorf("cannot get configmap: %w", err) + } + canc() + + fileOptions = cfg[0] + } + + if len(fileOptions) > 0 { + if err := yaml.Unmarshal(fileOptions, &decodedFileOptions); err != nil { + return fmt.Errorf("cannot decode options %s: %w", optsPath, err) + } + } + + if !cmd.Flag("endpoints").Changed { + opts.Endpoints = decodedFileOptions.Endpoints + } + + if !cmd.Flag("prefix").Changed { + opts.Prefix = decodedFileOptions.Prefix + } + } + + // -- Get the credentials secret + if credentialsSecret != "" { + l.Debug(). + Str("namespace", operatorOpts.Namespace). + Str("name", credentialsSecret). + Msg("getting credentials from secret...") + + ctx, canc := context.WithTimeout(context.Background(), 10*time.Second) + secret, err := cluster.GetDataFromSecret(ctx, operatorOpts.Namespace, credentialsSecret) + if err != nil { + canc() + return fmt.Errorf("cannot get secret: %w", err) + } + canc() + + username, exists := secret["username"] + if !exists { + return fmt.Errorf("secret %s does not contain any username", credentialsSecret) + } else { + if opts.Username == "" { + // The user did not overwrite this via flag. + // So we're going to the value from the secret. + opts.Username = string(username) + } + } + + if password, exists := secret["password"]; exists { + opts.Password = string(password) + } + } + + // -- Ask for password + if tmpPassFlag { + if operatorOpts.RunningInK8s { + return fmt.Errorf("cannot use --password while running in Kubernetes. Please use a Secret instead.") + } + + fmt.Printf("Please enter password for %s: ", opts.Username) + bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return fmt.Errorf("cannot read password from terminal: %w", err) + } + fmt.Println() + + opts.Password = strings.TrimSpace(string(bytePassword)) + } + + return nil + }, + RunE: func(_ *cobra.Command, _ []string) error { + return runWithEtcd(operatorOpts, opts) + }, + Example: "etcd --username root -p --endpoints 10.10.10.10:2379,10.10.10.11:2379", + } + + // ----------------------------- + // Flags + // ----------------------------- + + cmd.Flags().StringVarP(&opts.Username, "username", "u", "", + "username for authentication") + cmd.Flags().BoolVarP(&tmpPassFlag, "password", "p", false, + "enter password -- will be done interactively.") + cmd.Flags().StringVar(&opts.Prefix, "prefix", "", + "prefix to insert before every key.") + cmd.Flags().StringSliceVar(&opts.Endpoints, "endpoints", []string{"localhost:2379"}, + "list of endpoints for etcd.") + cmd.Flags().StringVar(&optsPath, "options-path", "", + "path to the file containing service directory options.") + cmd.Flags().StringVar(&optsConfigMap, "options-configmap", func() string { + if operatorOpts.RunningInK8s { + return defaultEtcdConfigMapName + } + + return "" + }(), + "name of the Kubernetes config map containing settings.") + cmd.Flags().StringVar(&credentialsSecret, "credentials-secret", func() string { + if operatorOpts.RunningInK8s { + return defaultEtcdCredentialsSecretName + } + + return "" + }(), + "name of the Kubernetes secret containing the credentials.") + + return cmd +} + +func runWithEtcd(operatorOpts *Options, opts *EtcdOptions) error { + ctx, canc := context.WithTimeout(context.Background(), 15*time.Second) + defer canc() + + spew.Dump(opts) + + // TODO: support certificates + cli, err := clientv3.New(clientv3.Config{ + Endpoints: opts.Endpoints, + Username: opts.Username, + Password: opts.Password, + DialTimeout: 15 * time.Second, + }) + + if err != nil { + return fmt.Errorf("cannot get etcd client: %w", err) + } + + defer cli.Close() + + servreg := etcd.NewServiceRegistryWithEtcd(ctx, cli, &opts.Prefix) + + // TODO: use the handler + _ = servreg + + return nil +} diff --git a/pkg/command/run/run.go b/pkg/command/run/run.go index deebe22..99d921a 100644 --- a/pkg/command/run/run.go +++ b/pkg/command/run/run.go @@ -348,7 +348,7 @@ func GetRunCommand() *cobra.Command { // Sub commands // ----------------------------- - // TODO: add commands + cmd.AddCommand(getRunEtcdCommand(opts)) return cmd }