repo_description: vCluster - Create fully functional virtual Kubernetes clusters - Each vcluster runs inside a namespace of the underlying k8s cluster. It's cheaper than creating separate full-blown clusters and it offers better multi-tenancy and isolation than regular namespaces.
repo_topics: cloud-native helm k3s k8s kubectl kubernetes multi-tenancy platform-engineering vcluster virtual-clusters
code: package etcd
  
  import (
  	"context"
  	"crypto/tls"
  	"fmt"
  	"strings"
  	"time"
  
  	clientv3 "go.etcd.io/etcd/client/v3"
  	"go.uber.org/zap"
  	"k8s.io/apimachinery/pkg/util/wait"
  	certutil "k8s.io/client-go/util/cert"
  	"k8s.io/klog/v2"
  )
  
  const (
  	waitForClientTimeout = time.Minute * 10
  )
  
  type Certificates struct {
  	CaCert     string
  	ServerCert string
  	ServerKey  string
  }
  
  func WaitForEtcd(parentCtx context.Context, certificates *Certificates, endpoints ...string) error {
  	var err error
  	waitErr := wait.PollUntilContextTimeout(parentCtx, time.Second, waitForClientTimeout, true, func(ctx context.Context) (bool, error) {
  		var etcdClient *clientv3.Client
  		etcdClient, err = GetEtcdClient(ctx, zap.L().Named("wait-for-etcd"), certificates, endpoints...)
  		if err == nil {
  			defer func() {
  				_ = etcdClient.Close()
  			}()
  
  			_, err = etcdClient.MemberList(ctx)
  			if err == nil {
  				return true, nil
  			}
  		}
  
  		klog.Infof("Couldn't connect to etcd (will retry in a second): %v", err)
  		return false, nil
  	})
  	if waitErr != nil {
  		return fmt.Errorf("error waiting for etcd: %w", err)
  	}
  
  	return nil
  }
  
  // GetEtcdClient returns an etcd client connected to the specified endpoints.
  // If no endpoints are provided, endpoints are retrieved from the provided runtime config.
  // If the runtime config does not list any endpoints, the default endpoint is used.
  // The returned client should be closed when no longer needed, in order to avoid leaking GRPC
  // client goroutines.
  func GetEtcdClient(ctx context.Context, log *zap.Logger, certificates *Certificates, endpoints ...string) (*clientv3.Client, error) {
  	cfg, err := getClientConfig(ctx, log, certificates, endpoints...)
  	if err != nil {
  		return nil, err
  	}
  
  	return clientv3.New(*cfg)
  }
  
  // getClientConfig generates an etcd client config connected to the specified endpoints.
  // If no endpoints are provided, getEndpoints is called to provide defaults.
  func getClientConfig(ctx context.Context, log *zap.Logger, certificates *Certificates, endpoints ...string) (*clientv3.Config, error) {
  	config := &clientv3.Config{
  		Endpoints:   endpoints,
  		Context:     ctx,
  		DialTimeout: 5 * time.Second,
  
  		Logger: log,
  	}
  
  	if len(endpoints) > 0 {
  		if strings.HasPrefix(endpoints[0], "https://") && certificates != nil {
  			var err error
  			if config.TLS, err = toTLSConfig(certificates); err != nil {
  				return nil, err
  			}
  		}
  	}
  
  	return config, nil
  }
  
  func toTLSConfig(certificates *Certificates) (*tls.Config, error) {
  	clientCert, err := tls.LoadX509KeyPair(
  		certificates.ServerCert,
  		certificates.ServerKey,
  	)
  	if err != nil {
  		return nil, err
  	}
  
  	pool, err := certutil.NewPool(certificates.CaCert)
  	if err != nil {
  		return nil, err
  	}
  
  	return &tls.Config{
  		RootCAs:      pool,
  		Certificates: []tls.Certificate{clientCert},
  	}, nil
  }
  