diff --git a/cmd/vclusterctl/cmd/connect.go b/cmd/vclusterctl/cmd/connect.go index 20c6270ff..7456f36c3 100644 --- a/cmd/vclusterctl/cmd/connect.go +++ b/cmd/vclusterctl/cmd/connect.go @@ -1,10 +1,15 @@ package cmd import ( + "context" "fmt" "github.com/loft-sh/vcluster/pkg/upgrade" "io/ioutil" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" "os" "os/exec" "strconv" @@ -30,7 +35,8 @@ type ConnectCmd struct { LocalPort int Server string - log log.Logger + + log log.Logger } // NewConnectCmd creates a new command @@ -132,9 +138,67 @@ func (cmd *ConnectCmd) Run(cobraCmd *cobra.Command, args []string) error { return fmt.Errorf("unexpected kube config") } + // check if the vcluster is exposed + if len(args) > 0 && cmd.Server == "" { + restConfig, err := kubeConfigLoader.ClientConfig() + if err != nil { + return errors.Wrap(err, "load kube config") + } + kubeClient, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return errors.Wrap(err, "create kube client") + } + + printedWaiting := false + err = wait.PollImmediate(time.Second*2, time.Minute*5, func() (done bool, err error) { + service, err := kubeClient.CoreV1().Services(cmd.Namespace).Get(context.TODO(), args[0], metav1.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + return true, nil + } + + return false, err + } + + // not a load balancer? Then don't wait + if service.Spec.Type != corev1.ServiceTypeLoadBalancer { + return true, nil + } + + if len(service.Status.LoadBalancer.Ingress) == 0 { + if !printedWaiting { + cmd.log.Infof("Waiting for vCluster LoadBalancer ip...") + printedWaiting = true + } + + return false, nil + } + + if service.Status.LoadBalancer.Ingress[0].Hostname != "" { + cmd.Server = service.Status.LoadBalancer.Ingress[0].Hostname + } else if service.Status.LoadBalancer.Ingress[0].IP != "" { + cmd.Server = service.Status.LoadBalancer.Ingress[0].IP + } + + if cmd.Server == "" { + return false, nil + } + + cmd.log.Infof("Using vcluster %s load balancer endpoint: %s", args[0], cmd.Server) + return true, nil + }) + if err != nil { + return errors.Wrap(err, "wait for vCluster") + } + } + port := "" for k := range kubeConfig.Clusters { if cmd.Server != "" { + if strings.HasSuffix(cmd.Server, "https://") == false { + cmd.Server = "https://" + cmd.Server + } + kubeConfig.Clusters[k].Server = cmd.Server } else { splitted := strings.Split(kubeConfig.Clusters[k].Server, ":") diff --git a/cmd/vclusterctl/cmd/create.go b/cmd/vclusterctl/cmd/create.go index 153bcb04a..ed0fec5c3 100644 --- a/cmd/vclusterctl/cmd/create.go +++ b/cmd/vclusterctl/cmd/create.go @@ -23,10 +23,10 @@ import ( ) var VersionMap = map[string]string{ - "1.21": "rancher/k3s:v1.21.0-k3s1", - "1.20": "rancher/k3s:v1.20.4-k3s1", - "1.19": "rancher/k3s:v1.19.8-k3s1", - "1.18": "rancher/k3s:v1.18.16-k3s1", + "1.21": "rancher/k3s:v1.21.2-k3s1", + "1.20": "rancher/k3s:v1.20.8-k3s1", + "1.19": "rancher/k3s:v1.19.12-k3s1", + "1.18": "rancher/k3s:v1.18.20-k3s1", "1.17": "rancher/k3s:v1.17.17-k3s1", "1.16": "rancher/k3s:v1.16.15-k3s1", } @@ -65,6 +65,7 @@ type CreateCmd struct { CreateNamespace bool DisableIngressSync bool CreateClusterRole bool + Expose bool log log.Logger } @@ -107,6 +108,7 @@ vcluster create test --namespace test cobraCmd.Flags().BoolVar(&cmd.CreateNamespace, "create-namespace", true, "If true the namespace will be created if it does not exist") cobraCmd.Flags().BoolVar(&cmd.DisableIngressSync, "disable-ingress-sync", false, "If true the virtual cluster will not sync any ingresses") cobraCmd.Flags().BoolVar(&cmd.CreateClusterRole, "create-cluster-role", false, "If true a cluster role will be created to access nodes, storageclasses and priorityclasses") + cobraCmd.Flags().BoolVar(&cmd.Expose, "expose", false, "If true will create a load balancer service to expose the vcluster endpoint") return cobraCmd } @@ -288,6 +290,12 @@ rbac: create: true` } + if cmd.Expose { + values += ` +service: + type: LoadBalancer` + } + values = strings.ReplaceAll(values, "##IMAGE##", image) values = strings.ReplaceAll(values, "##CIDR##", cidr) if cmd.K3SImage == "" { diff --git a/pkg/server/cert/syncer.go b/pkg/server/cert/syncer.go index 7b63a39d0..d58faf2fb 100644 --- a/pkg/server/cert/syncer.go +++ b/pkg/server/cert/syncer.go @@ -103,6 +103,17 @@ func (s *syncer) getSANs() ([]string, error) { } else if svc.Spec.ClusterIP == "" { return nil, fmt.Errorf("target service %s/%s is missing a clusterIP", namespace, s.serviceName) } + + // get load balancer ip + for _, ing := range svc.Status.LoadBalancer.Ingress { + if ing.IP != "" { + retSANs = append(retSANs, ing.IP) + } + if ing.Hostname != "" { + retSANs = append(retSANs, ing.Hostname) + } + } + retSANs = append(retSANs, svc.Spec.ClusterIP) // get cluster ips of node services