VOL-3934 - TLS connection support

This is a bit of a breaking change as the current voltctl config
file defines verify as a string when it should have been a bool
from the start.

depends on merge of https://gerrit.opencord.org/c/voltha-lib-go/+/23594

Change-Id: Idb1f90a6bc827a599f2290bd276604997aab44e8
diff --git a/internal/pkg/commands/command.go b/internal/pkg/commands/command.go
index d2951a3..3e1beb4 100644
--- a/internal/pkg/commands/command.go
+++ b/internal/pkg/commands/command.go
@@ -16,12 +16,13 @@
 package commands
 
 import (
+	"context"
+	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
 	"log"
-	"net"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -32,11 +33,14 @@
 
 	"github.com/golang/protobuf/jsonpb"
 	"github.com/golang/protobuf/proto"
+	configv1 "github.com/opencord/voltctl/internal/pkg/apis/config/v1"
+	configv2 "github.com/opencord/voltctl/internal/pkg/apis/config/v2"
 	"github.com/opencord/voltctl/pkg/filter"
 	"github.com/opencord/voltctl/pkg/format"
 	"github.com/opencord/voltctl/pkg/order"
 	"google.golang.org/grpc"
-	"gopkg.in/yaml.v2"
+	"google.golang.org/grpc/credentials"
+	yaml "gopkg.in/yaml.v2"
 )
 
 type OutputType uint8
@@ -46,48 +50,9 @@
 	OUTPUT_JSON
 	OUTPUT_YAML
 
-	defaultApiHost = "localhost"
-	defaultApiPort = 55555
-
-	defaultKafkaHost = "localhost"
-	defaultKafkaPort = 9092
-
 	supportedKvStoreType = "etcd"
-	defaultKvHost        = "localhost"
-	defaultKvPort        = 2379
-	defaultKvTimeout     = time.Second * 5
-
-	defaultGrpcTimeout            = time.Minute * 5
-	defaultGrpcMaxCallRecvMsgSize = "4MB"
 )
 
-type GrpcConfigSpec struct {
-	Timeout            time.Duration `yaml:"timeout"`
-	MaxCallRecvMsgSize string        `yaml:"maxCallRecvMsgSize"`
-}
-
-type KvStoreConfigSpec struct {
-	Timeout time.Duration `yaml:"timeout"`
-}
-
-type TlsConfigSpec struct {
-	UseTls bool   `yaml:"useTls"`
-	CACert string `yaml:"caCert"`
-	Cert   string `yaml:"cert"`
-	Key    string `yaml:"key"`
-	Verify string `yaml:"verify"`
-}
-
-type GlobalConfigSpec struct {
-	Server        string            `yaml:"server"`
-	Kafka         string            `yaml:"kafka"`
-	KvStore       string            `yaml:"kvstore"`
-	Tls           TlsConfigSpec     `yaml:"tls"`
-	Grpc          GrpcConfigSpec    `yaml:"grpc"`
-	KvStoreConfig KvStoreConfigSpec `yaml:"kvstoreconfig"`
-	K8sConfig     string            `yaml:"-"`
-}
-
 var (
 	ParamNames = map[string]map[string]string{
 		"v1": {
@@ -105,21 +70,7 @@
 
 	CharReplacer = strings.NewReplacer("\\t", "\t", "\\n", "\n")
 
-	GlobalConfig = GlobalConfigSpec{
-		Server:  "localhost:55555",
-		Kafka:   "",
-		KvStore: "localhost:2379",
-		Tls: TlsConfigSpec{
-			UseTls: false,
-		},
-		Grpc: GrpcConfigSpec{
-			Timeout:            defaultGrpcTimeout,
-			MaxCallRecvMsgSize: defaultGrpcMaxCallRecvMsgSize,
-		},
-		KvStoreConfig: KvStoreConfigSpec{
-			Timeout: defaultKvTimeout,
-		},
-	}
+	GlobalConfig = configv2.NewDefaultConfig()
 
 	GlobalCommandOptions = make(map[string]map[string]string)
 
@@ -194,31 +145,6 @@
 	}
 }
 
-func splitEndpoint(ep, defaultHost string, defaultPort int) (string, int, error) {
-	port := defaultPort
-	host, sPort, err := net.SplitHostPort(ep)
-	if err != nil {
-		if addrErr, ok := err.(*net.AddrError); ok {
-			if addrErr.Err != "missing port in address" {
-				return "", 0, err
-			}
-			host = ep
-		} else {
-			return "", 0, err
-		}
-	} else if len(strings.TrimSpace(sPort)) > 0 {
-		val, err := strconv.Atoi(sPort)
-		if err != nil {
-			return "", 0, err
-		}
-		port = val
-	}
-	if len(strings.TrimSpace(host)) == 0 {
-		host = defaultHost
-	}
-	return strings.Trim(host, "]["), port, nil
-}
-
 type CommandResult struct {
 	Format    format.Format
 	Filter    string
@@ -283,9 +209,15 @@
 			Error.Fatalf("Unable to read the configuration file '%s': %s",
 				GlobalOptions.Config, err.Error())
 		}
+		// First try the latest version of the config api then work
+		// backwards
 		if err = yaml.Unmarshal(configFile, &GlobalConfig); err != nil {
-			Error.Fatalf("Unable to parse the configuration file '%s': %s",
-				GlobalOptions.Config, err.Error())
+			GlobalConfigV1 := configv1.NewDefaultConfig()
+			if err = yaml.Unmarshal(configFile, &GlobalConfigV1); err != nil {
+				Error.Fatalf("Unable to parse the configuration file '%s': %s",
+					GlobalOptions.Config, err.Error())
+			}
+			GlobalConfig = configv2.FromConfigV1(GlobalConfigV1)
 		}
 	}
 
@@ -293,32 +225,22 @@
 	if GlobalOptions.Server != "" {
 		GlobalConfig.Server = GlobalOptions.Server
 	}
-	host, port, err := splitEndpoint(GlobalConfig.Server, defaultApiHost, defaultApiPort)
-	if err != nil {
-		Error.Fatalf("voltha API endport incorrectly specified '%s':%s",
-			GlobalConfig.Server, err)
+
+	if GlobalOptions.UseTLS {
+		GlobalConfig.Tls.UseTls = true
 	}
-	GlobalConfig.Server = net.JoinHostPort(host, strconv.Itoa(port))
+
+	if GlobalOptions.Verify {
+		GlobalConfig.Tls.Verify = true
+	}
 
 	if GlobalOptions.Kafka != "" {
 		GlobalConfig.Kafka = GlobalOptions.Kafka
 	}
-	host, port, err = splitEndpoint(GlobalConfig.Kafka, defaultKafkaHost, defaultKafkaPort)
-	if err != nil {
-		Error.Fatalf("Kafka endport incorrectly specified '%s':%s",
-			GlobalConfig.Kafka, err)
-	}
-	GlobalConfig.Kafka = net.JoinHostPort(host, strconv.Itoa(port))
 
 	if GlobalOptions.KvStore != "" {
 		GlobalConfig.KvStore = GlobalOptions.KvStore
 	}
-	host, port, err = splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
-	if err != nil {
-		Error.Fatalf("KV store endport incorrectly specified '%s':%s",
-			GlobalConfig.KvStore, err)
-	}
-	GlobalConfig.KvStore = net.JoinHostPort(host, strconv.Itoa(port))
 
 	if GlobalOptions.KvStoreTimeout != "" {
 		timeout, err := time.ParseDuration(GlobalOptions.KvStoreTimeout)
@@ -384,7 +306,24 @@
 		Error.Fatalf("Cannot convert msgSize %s to bytes", GlobalConfig.Grpc.MaxCallRecvMsgSize)
 	}
 
-	return grpc.Dial(GlobalConfig.Server, grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(int(n))))
+	var opts []grpc.DialOption
+
+	opts = append(opts,
+		grpc.WithDisableRetry(),
+		grpc.WithBlock(),
+		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(int(n))))
+
+	if GlobalConfig.Tls.UseTls {
+		creds := credentials.NewTLS(&tls.Config{
+			InsecureSkipVerify: !GlobalConfig.Tls.Verify})
+		opts = append(opts, grpc.WithTransportCredentials(creds))
+	} else {
+		opts = append(opts, grpc.WithInsecure())
+	}
+	ctx, cancel := context.WithTimeout(context.TODO(),
+		GlobalConfig.Grpc.ConnectTimeout)
+	defer cancel()
+	return grpc.DialContext(ctx, GlobalConfig.Server, opts...)
 }
 
 func ConvertJsonProtobufArray(data_in interface{}) (string, error) {
diff --git a/internal/pkg/commands/command_test.go b/internal/pkg/commands/command_test.go
index 68dd0a4..e0bd4bd 100644
--- a/internal/pkg/commands/command_test.go
+++ b/internal/pkg/commands/command_test.go
@@ -16,11 +16,12 @@
 package commands
 
 import (
-	flags "github.com/jessevdk/go-flags"
-	"github.com/stretchr/testify/assert"
 	"os"
 	"path"
 	"testing"
+
+	flags "github.com/jessevdk/go-flags"
+	"github.com/stretchr/testify/assert"
 )
 
 // Test that ProcessGlobalOptions does not interfere with GlobalConfig
@@ -39,58 +40,6 @@
 	assert.Equal(t, "localhost:55555", GlobalConfig.Server, "wrong default hostname for server")
 }
 
-func TestSplitHostPort(t *testing.T) {
-	data := []struct {
-		name        string
-		endpoint    string
-		defaultHost string
-		defaultPort int
-		host        string
-		port        int
-		err         bool
-	}{
-		{"Host and port specified", "host:1234", "default", 4321, "host", 1234, false},
-		{"Host only specified", "host", "default", 4321, "host", 4321, false},
-		{"Host: only specified", "host:", "default", 4321, "host", 4321, false},
-		{"Port only specified", ":1234", "default", 4321, "default", 1234, false},
-		{"Colon only", ":", "default", 4321, "default", 4321, false},
-		{"Empty endpoint", "", "default", 4321, "default", 4321, false},
-		{"IPv4 and port specified", "1.2.3.4:1234", "4.3.2.1", 4321, "1.2.3.4", 1234, false},
-		{"IPv4 only specified", "1.2.3.4", "4.3.2.1", 4321, "1.2.3.4", 4321, false},
-		{"IPv4: only specified", "1.2.3.4:", "4.3.2.1", 4321, "1.2.3.4", 4321, false},
-		{"IPv4 Port only specified", ":1234", "4.3.2.1", 4321, "4.3.2.1", 1234, false},
-		{"IPv4 Colon only", ":", "4.3.2.1", 4321, "4.3.2.1", 4321, false},
-		{"IPv4 Empty endpoint", "", "4.3.2.1", 4321, "4.3.2.1", 4321, false},
-		{"IPv6 and port specified", "[0001:c0ff:eec0::::ffff]:1234", "0001:c0ff:eec0::::aaaa", 4321, "0001:c0ff:eec0::::ffff", 1234, false},
-		{"IPv6 only specified", "[0001:c0ff:eec0::::ffff]", "0001:c0ff:eec0::::aaaa", 4321, "0001:c0ff:eec0::::ffff", 4321, false},
-		{"IPv6: only specified", "[0001:c0ff:eec0::::ffff]:", "0001:c0ff:eec0::::aaaa", 4321, "0001:c0ff:eec0::::ffff", 4321, false},
-		{"IPv6 Port only specified", ":1234", "0001:c0ff:eec0::::aaaa", 4321, "0001:c0ff:eec0::::aaaa", 1234, false},
-		{"IPv6 Colon only", ":", "0001:c0ff:eec0::::aaaa", 4321, "0001:c0ff:eec0::::aaaa", 4321, false},
-		{"IPv6 Empty endpoint", "", "0001:c0ff:eec0::::aaaa", 4321, "0001:c0ff:eec0::::aaaa", 4321, false},
-		{"Invalid port", "host:1b", "default", 4321, "", 0, true},
-		{"Too many colons", "ho:st:1b", "default", 4321, "", 0, true},
-		{"IPv4 Invalid port", "1.2.3.4:1b", "4.3.2.1", 4321, "", 0, true},
-		{"IPv4 Too many colons", "1.2.3.4::1234", "4.3.2.1", 4321, "", 0, true},
-		{"IPv6 Invalid port", "[0001:c0ff:eec0::::ffff]:1b", "0001:c0ff:eec0::::aaaa", 4321, "", 0, true},
-		{"IPv6 Too many colons", "0001:c0ff:eec0::::ffff:1234", "0001:c0ff:eec0::::aaaa", 4321, "", 0, true},
-	}
-
-	for _, args := range data {
-		t.Run(args.name, func(t *testing.T) {
-			h, p, err := splitEndpoint(args.endpoint, args.defaultHost, args.defaultPort)
-			if args.err {
-				assert.NotNil(t, err, "unexpected non-error result")
-			} else {
-				assert.Nil(t, err, "unexpected error result")
-			}
-			if !args.err && err == nil {
-				assert.Equal(t, args.host, h, "unexpected host value")
-				assert.Equal(t, args.port, p, "unexpected port value")
-			}
-		})
-	}
-}
-
 func TestParseSize(t *testing.T) {
 	var res uint64
 	var err error
diff --git a/internal/pkg/commands/config.go b/internal/pkg/commands/config.go
index 3ba4118..59a1df1 100644
--- a/internal/pkg/commands/config.go
+++ b/internal/pkg/commands/config.go
@@ -17,12 +17,12 @@
 
 import (
 	"fmt"
+
 	flags "github.com/jessevdk/go-flags"
-	"gopkg.in/yaml.v2"
+	yaml "gopkg.in/yaml.v2"
 )
 
-const copyrightNotice = `
-# Copyright 2019-present Ciena Corporation
+const copyrightNotice = `# Copyright 2021-present Ciena Corporation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -34,9 +34,7 @@
 # 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.
-#
-`
+# limitations under the License.`
 
 type CommandOptionsDump struct{}
 
diff --git a/internal/pkg/commands/log.go b/internal/pkg/commands/log.go
index 77d97e9..a1110b4 100644
--- a/internal/pkg/commands/log.go
+++ b/internal/pkg/commands/log.go
@@ -17,11 +17,10 @@
 
 import (
 	"context"
+	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
-	"net"
-	"strconv"
 	"strings"
 
 	flags "github.com/jessevdk/go-flags"
@@ -30,6 +29,7 @@
 	"github.com/opencord/voltha-lib-go/v4/pkg/config"
 	"github.com/opencord/voltha-lib-go/v4/pkg/db/kvstore"
 	"github.com/opencord/voltha-lib-go/v4/pkg/log"
+	v3Client "go.etcd.io/etcd/clientv3"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/clientcmd"
@@ -250,14 +250,24 @@
 }
 
 func constructConfigManager(ctx context.Context) (*config.ConfigManager, func(), error) {
-	client, err := kvstore.NewEtcdClient(ctx, GlobalConfig.KvStore, GlobalConfig.KvStoreConfig.Timeout, log.FatalLevel)
+	var tlsConfig *tls.Config
+	if GlobalConfig.Tls.UseTls {
+		tlsConfig = &tls.Config{InsecureSkipVerify: !GlobalConfig.Tls.Verify}
+	}
+	logconfig := log.ConstructZapConfig(log.JSON, log.FatalLevel, log.Fields{})
+	client, err := kvstore.NewEtcdCustomClient(
+		ctx,
+		&v3Client.Config{
+			Endpoints:   []string{GlobalConfig.KvStore},
+			DialTimeout: GlobalConfig.KvStoreConfig.Timeout,
+			LogConfig:   &logconfig,
+			TLS:         tlsConfig,
+		})
 	if err != nil {
 		return nil, nil, fmt.Errorf("Unable to create kvstore client %s", err)
 	}
 
-	// Already error checked during option processing
-	host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
-	cm := config.NewConfigManager(ctx, client, supportedKvStoreType, net.JoinHostPort(host, strconv.Itoa(port)), GlobalConfig.KvStoreConfig.Timeout)
+	cm := config.NewConfigManager(ctx, client, supportedKvStoreType, GlobalConfig.KvStore, GlobalConfig.KvStoreConfig.Timeout)
 	return cm, func() { client.Close(ctx) }, nil
 }
 
@@ -685,8 +695,7 @@
 	if len(options.Args.Component) == 0 {
 		componentList, err = cm.RetrieveComponentList(ctx, config.ConfigTypeLogLevel)
 		if err != nil {
-			host, port, _ := splitEndpoint(GlobalConfig.KvStore, defaultKvHost, defaultKvPort)
-			return fmt.Errorf("Unable to retrieve list of voltha components : %s \nIs ETCD available at %s:%d?", err, host, port)
+			return fmt.Errorf("Unable to retrieve list of voltha components : %s \nIs ETCD available at %s?", err, GlobalConfig.KvStore)
 		}
 	} else {
 		componentList = toStringArray(options.Args.Component)
diff --git a/internal/pkg/commands/version.go b/internal/pkg/commands/version.go
index 8856ae1..db47ed8 100644
--- a/internal/pkg/commands/version.go
+++ b/internal/pkg/commands/version.go
@@ -18,12 +18,13 @@
 import (
 	"context"
 	"encoding/json"
+	"strings"
+
 	"github.com/golang/protobuf/ptypes/empty"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/opencord/voltctl/internal/pkg/cli/version"
 	"github.com/opencord/voltctl/pkg/format"
 	"github.com/opencord/voltha-protos/v4/go/voltha"
-	"strings"
 )
 
 type VersionDetails struct {
@@ -140,7 +141,7 @@
 
 	voltha, err := client.GetVoltha(ctx, &empty.Empty{})
 	if err != nil {
-		return nil
+		return err
 	}
 
 	info := make(map[string]interface{})