Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export k3k cluster kubeconfig in k3kcli #36

Merged
merged 2 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions charts/k3k/crds/cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ spec:
type: array
items:
type: string
tlsSANs:
type: array
items:
type: string
expose:
type: object
properties:
Expand All @@ -54,6 +58,11 @@ spec:
properties:
enabled:
type: boolean
nodePort:
type: object
properties:
enabled:
type: boolean
status:
type: object
properties:
Expand Down
140 changes: 137 additions & 3 deletions cli/cmds/cluster/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,39 @@ package cluster
import (
"context"
"errors"
"fmt"
"net/url"
"os"
"strings"
"time"

"github.com/galal-hussein/k3k/cli/cmds"
"github.com/galal-hussein/k3k/pkg/apis/k3k.io/v1alpha1"
"github.com/galal-hussein/k3k/pkg/controller/util"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var (
Scheme = runtime.NewScheme()
Scheme = runtime.NewScheme()
backoff = wait.Backoff{
Steps: 5,
Duration: 3 * time.Second,
Factor: 2,
Jitter: 0.1,
}
)

func init() {
Expand Down Expand Up @@ -101,10 +119,11 @@ func createCluster(clx *cli.Context) error {
ctrlClient, err := client.New(restConfig, client.Options{
Scheme: Scheme,
})

if err != nil {
return err
}
logrus.Infof("creating a new cluster [%s]", name)
logrus.Infof("Creating a new cluster [%s]", name)
cluster := newCluster(
name,
token,
Expand All @@ -116,7 +135,46 @@ func createCluster(clx *cli.Context) error {
agentArgs,
)

return ctrlClient.Create(ctx, cluster)
cluster.Spec.Expose = &v1alpha1.ExposeConfig{
NodePort: &v1alpha1.NodePortConfig{
Enabled: true,
},
}

// add Host IP address as an extra TLS-SAN to expose the k3k cluster
url, err := url.Parse(restConfig.Host)
if err != nil {
return err
}
host := strings.Split(url.Host, ":")
cluster.Spec.TLSSANs = []string{
host[0],
}

if err := ctrlClient.Create(ctx, cluster); err != nil {
if apierrors.IsAlreadyExists(err) {
logrus.Infof("Cluster [%s] already exists", name)
} else {
return err
}
}

logrus.Infof("Extracting Kubeconfig for [%s] cluster", name)
var kubeconfig []byte
err = retry.OnError(backoff, apierrors.IsNotFound, func() error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally don't want to reuse the err var for a few safety reasons. it's best to scope lock it in the "if".

kubeconfig, err = extractKubeconfig(ctx, ctrlClient, cluster, host[0])
if err != nil {
return err
}
return nil
})

if err != nil {
return err
}
// export kubeconfig

return os.WriteFile(cluster.Name+"-kubeconfig.yaml", kubeconfig, 0644)
}

func validateCreateFlags(clx *cli.Context) error {
Expand Down Expand Up @@ -157,3 +215,79 @@ func newCluster(name, token string, servers, agents int32, clusterCIDR, serviceC
},
}
}

func extractKubeconfig(ctx context.Context, client client.Client, cluster *v1alpha1.Cluster, serverIP string) ([]byte, error) {
nn := types.NamespacedName{
Name: cluster.Name + "-kubeconfig",
Namespace: util.ClusterNamespace(cluster),
}
var kubeSecret v1.Secret
if err := client.Get(ctx, nn, &kubeSecret); err != nil {
return nil, err
}

kubeconfig := kubeSecret.Data["kubeconfig.yaml"]
if kubeconfig == nil {
return nil, errors.New("empty kubeconfig")
}

nn = types.NamespacedName{
Name: "k3k-server-service",
Namespace: util.ClusterNamespace(cluster),
}
var k3kService v1.Service
if err := client.Get(ctx, nn, &k3kService); err != nil {
return nil, err
}
if k3kService.Spec.Type == v1.ServiceTypeNodePort {
nodePort := k3kService.Spec.Ports[0].NodePort

restConfig, err := clientcmd.RESTConfigFromKubeConfig(kubeconfig)
if err != nil {
return nil, err
}
hostURL := fmt.Sprintf("https://%s:%d", serverIP, nodePort)
logrus.Infof(hostURL)
restConfig.Host = hostURL

clientConfig := generateKubeconfigFromRest(restConfig)

b, err := clientcmd.Write(clientConfig)
if err != nil {
return nil, err
}
kubeconfig = b
}
return kubeconfig, nil
}

func generateKubeconfigFromRest(config *rest.Config) clientcmdapi.Config {
clusters := make(map[string]*clientcmdapi.Cluster)
clusters["default-cluster"] = &clientcmdapi.Cluster{
Server: config.Host,
CertificateAuthorityData: config.CAData,
}

contexts := make(map[string]*clientcmdapi.Context)
contexts["default-context"] = &clientcmdapi.Context{
Cluster: "default-cluster",
Namespace: "default",
AuthInfo: "default",
}

authinfos := make(map[string]*clientcmdapi.AuthInfo)
authinfos["default"] = &clientcmdapi.AuthInfo{
ClientCertificateData: config.CertData,
ClientKeyData: config.KeyData,
}

clientConfig := clientcmdapi.Config{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
clientConfig := clientcmdapi.Config{
return clientcmdapi.Config{

Kind: "Config",
APIVersion: "v1",
Clusters: clusters,
Contexts: contexts,
CurrentContext: "default-context",
AuthInfos: authinfos,
}
return clientConfig
}
27 changes: 16 additions & 11 deletions pkg/apis/k3k.io/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ type Cluster struct {
}

type ClusterSpec struct {
Name string `json:"name"`
Version string `json:"version"`
Servers *int32 `json:"servers"`
Agents *int32 `json:"agents"`
Token string `json:"token"`
ClusterCIDR string `json:"clusterCIDR,omitempty"`
ServiceCIDR string `json:"serviceCIDR,omitempty"`
ClusterDNS string `json:"clusterDNS,omitempty"`

ServerArgs []string `json:"serverArgs,omitempty"`
AgentArgs []string `json:"agentArgs,omitempty"`
Name string `json:"name"`
Version string `json:"version"`
Servers *int32 `json:"servers"`
Agents *int32 `json:"agents"`
Token string `json:"token"`
ClusterCIDR string `json:"clusterCIDR,omitempty"`
ServiceCIDR string `json:"serviceCIDR,omitempty"`
ClusterDNS string `json:"clusterDNS,omitempty"`
ServerArgs []string `json:"serverArgs,omitempty"`
AgentArgs []string `json:"agentArgs,omitempty"`
TLSSANs []string `json:"tlsSANs,omitempty"`

Expose *ExposeConfig `json:"expose,omitempty"`
}
Expand All @@ -43,6 +43,7 @@ type ClusterList struct {
type ExposeConfig struct {
Ingress *IngressConfig `json:"ingress"`
LoadBalancer *LoadBalancerConfig `json:"loadbalancer"`
NodePort *NodePortConfig `json:"nodePort"`
}

type IngressConfig struct {
Expand All @@ -54,6 +55,10 @@ type LoadBalancerConfig struct {
Enabled bool `json:"enabled"`
}

type NodePortConfig struct {
Enabled bool `json:"enabled"`
}

type ClusterStatus struct {
ClusterCIDR string `json:"clusterCIDR,omitempty"`
ServiceCIDR string `json:"serviceCIDR,omitempty"`
Expand Down
26 changes: 26 additions & 0 deletions pkg/apis/k3k.io/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pkg/controller/cluster/config/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ func serverOptions(cluster *v1alpha1.Cluster) string {
if cluster.Spec.ClusterDNS != "" {
opts = opts + "cluster-dns: " + cluster.Spec.ClusterDNS + "\n"
}
if len(cluster.Spec.TLSSANs) > 0 {
opts = opts + "tls-san:\n"
for _, addr := range cluster.Spec.TLSSANs {
opts = opts + "- " + addr + "\n"
}
}
// TODO: Add extra args to the options

return opts
}
10 changes: 9 additions & 1 deletion pkg/controller/cluster/server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import (
)

func Service(cluster *v1alpha1.Cluster) *v1.Service {
serviceType := v1.ServiceTypeClusterIP
if cluster.Spec.Expose != nil {
if cluster.Spec.Expose.NodePort != nil {
if cluster.Spec.Expose.NodePort.Enabled {
serviceType = v1.ServiceTypeNodePort
}
}
}
return &v1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
Expand All @@ -18,7 +26,7 @@ func Service(cluster *v1alpha1.Cluster) *v1.Service {
Namespace: util.ClusterNamespace(cluster),
},
Spec: v1.ServiceSpec{
Type: v1.ServiceTypeClusterIP,
Type: serviceType,
Selector: map[string]string{
"cluster": cluster.Name,
"role": "server",
Expand Down