diff --git a/config/samples/v1alpha1_frontproxy.yaml b/config/samples/v1alpha1_frontproxy.yaml index 483c5c79..99d1bbc5 100644 --- a/config/samples/v1alpha1_frontproxy.yaml +++ b/config/samples/v1alpha1_frontproxy.yaml @@ -9,4 +9,18 @@ spec: rootShard: ref: name: shard-sample - externalHostname: kcp.example.com + serviceTemplate: + spec: + # hard code a specific cluster IP, e.g. for a kind setup. + clusterIP: 10.96.100.100 + certificateTemplates: + server: + spec: + dnsNames: + # add localhost to the certificate. + - localhost + ipAddresses: + # add localhost IPs to the server certificate. + # this allows easy port-forward access. + - 127.0.0.1 + - 127.0.0.2 diff --git a/config/samples/v1alpha1_kubeconfig.yaml b/config/samples/v1alpha1_kubeconfig_frontproxy.yaml similarity index 82% rename from config/samples/v1alpha1_kubeconfig.yaml rename to config/samples/v1alpha1_kubeconfig_frontproxy.yaml index 072049d6..e22ebc03 100644 --- a/config/samples/v1alpha1_kubeconfig.yaml +++ b/config/samples/v1alpha1_kubeconfig_frontproxy.yaml @@ -4,11 +4,11 @@ metadata: labels: app.kubernetes.io/name: kcp-operator app.kubernetes.io/managed-by: kustomize - name: kubeconfig-sample + name: kubeconfig-kcp-admin spec: - username: user@kcp.io + username: kcp-admin groups: - - kcp-users + - system:kcp:admin validity: 8766h secretRef: name: sample-kubeconfig diff --git a/config/samples/v1alpha1_kubeconfig_rootshard.yaml b/config/samples/v1alpha1_kubeconfig_rootshard.yaml new file mode 100644 index 00000000..12590a34 --- /dev/null +++ b/config/samples/v1alpha1_kubeconfig_rootshard.yaml @@ -0,0 +1,17 @@ +apiVersion: operator.kcp.io/v1alpha1 +kind: Kubeconfig +metadata: + labels: + app.kubernetes.io/name: kcp-operator + app.kubernetes.io/managed-by: kustomize + name: kubeconfig-shard-root-admin +spec: + username: shard-root-admin + groups: + - system:kcp:admin + validity: 8766h + secretRef: + name: kubeconfig-shard-root-admin + target: + rootShardRef: + name: shard-sample diff --git a/config/samples/v1alpha1_kubeconfig_shard.yaml b/config/samples/v1alpha1_kubeconfig_shard.yaml new file mode 100644 index 00000000..496a747f --- /dev/null +++ b/config/samples/v1alpha1_kubeconfig_shard.yaml @@ -0,0 +1,17 @@ +apiVersion: operator.kcp.io/v1alpha1 +kind: Kubeconfig +metadata: + labels: + app.kubernetes.io/name: kcp-operator + app.kubernetes.io/managed-by: kustomize + name: kubeconfig-shard-secondary-admin +spec: + username: shard-root-admin + groups: + - system:kcp:admin + validity: 8766h + secretRef: + name: kubeconfig-shard-secondary-admin + target: + shardRef: + name: secondary-shard diff --git a/config/samples/v1alpha1_rootshard.yaml b/config/samples/v1alpha1_rootshard.yaml index 59a94d7f..a25824f0 100644 --- a/config/samples/v1alpha1_rootshard.yaml +++ b/config/samples/v1alpha1_rootshard.yaml @@ -20,3 +20,12 @@ spec: etcd: endpoints: - http://etcd.default.svc.cluster.local:2379 + deploymentTemplate: + spec: + template: + spec: + hostAliases: + # add a hardcoded DNS override to the same IP as in v1alpha1_frontproxy.yaml. + - ip: "10.96.100.100" + hostnames: + - "example.operator.kcp.io" diff --git a/config/samples/v1alpha1_shard.yaml b/config/samples/v1alpha1_shard.yaml index b75cb253..3556ee51 100644 --- a/config/samples/v1alpha1_shard.yaml +++ b/config/samples/v1alpha1_shard.yaml @@ -4,6 +4,11 @@ metadata: labels: app.kubernetes.io/name: kcp-operator app.kubernetes.io/managed-by: kustomize - name: shard-sample + name: secondary-shard spec: - # TODO(user): Add fields here + etcd: + endpoints: + - http://etcd-shard.default.svc.cluster.local:2379 + rootShard: + ref: + name: shard-sample diff --git a/internal/controller/kubeconfig_controller.go b/internal/controller/kubeconfig_controller.go index b286cd9a..45cb14a5 100644 --- a/internal/controller/kubeconfig_controller.go +++ b/internal/controller/kubeconfig_controller.go @@ -20,7 +20,6 @@ import ( "context" "errors" "fmt" - "net/url" "time" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" @@ -28,6 +27,7 @@ import ( k8creconciling "k8c.io/reconciler/pkg/reconciling" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -70,28 +70,32 @@ func (r *KubeconfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) var kc operatorv1alpha1.Kubeconfig if err := r.Get(ctx, req.NamespacedName, &kc); err != nil { + // object has been deleted. + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } return ctrl.Result{}, err } + rootShard := &operatorv1alpha1.RootShard{} + shard := &operatorv1alpha1.Shard{} + var ( - clientCertIssuer, serverCA, serverURL, serverName string + clientCertIssuer string + serverCA string ) switch { case kc.Spec.Target.RootShardRef != nil: - var rootShard operatorv1alpha1.RootShard - if err := r.Get(ctx, types.NamespacedName{Name: kc.Spec.Target.RootShardRef.Name, Namespace: req.Namespace}, &rootShard); err != nil { + if err := r.Get(ctx, types.NamespacedName{Name: kc.Spec.Target.RootShardRef.Name, Namespace: req.Namespace}, rootShard); err != nil { return ctrl.Result{}, fmt.Errorf("failed to get RootShard: %w", err) } - clientCertIssuer = resources.GetRootShardCAName(&rootShard, operatorv1alpha1.ClientCA) - serverCA = resources.GetRootShardCAName(&rootShard, operatorv1alpha1.ServerCA) - serverURL = resources.GetRootShardBaseURL(&rootShard) - serverName = rootShard.Name + clientCertIssuer = resources.GetRootShardCAName(rootShard, operatorv1alpha1.ClientCA) + serverCA = resources.GetRootShardCAName(rootShard, operatorv1alpha1.ServerCA) case kc.Spec.Target.ShardRef != nil: - var shard operatorv1alpha1.Shard - if err := r.Get(ctx, types.NamespacedName{Name: kc.Spec.Target.ShardRef.Name, Namespace: req.Namespace}, &shard); err != nil { + if err := r.Get(ctx, types.NamespacedName{Name: kc.Spec.Target.ShardRef.Name, Namespace: req.Namespace}, shard); err != nil { return ctrl.Result{}, fmt.Errorf("failed to get Shard: %w", err) } @@ -99,16 +103,13 @@ func (r *KubeconfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) if ref == nil || ref.Name == "" { return ctrl.Result{}, errors.New("the Shard does not reference a (valid) RootShard") } - var rootShard operatorv1alpha1.RootShard - if err := r.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: req.Namespace}, &rootShard); err != nil { + if err := r.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: req.Namespace}, rootShard); err != nil { return ctrl.Result{}, fmt.Errorf("failed to get RootShard: %w", err) } // The client CA is shared among all shards and owned by the root shard. - clientCertIssuer = resources.GetRootShardCAName(&rootShard, operatorv1alpha1.ClientCA) - serverCA = resources.GetRootShardCAName(&rootShard, operatorv1alpha1.ServerCA) - serverURL = resources.GetShardBaseURL(&shard) - serverName = shard.Name + clientCertIssuer = resources.GetRootShardCAName(rootShard, operatorv1alpha1.ClientCA) + serverCA = resources.GetRootShardCAName(rootShard, operatorv1alpha1.ServerCA) case kc.Spec.Target.FrontProxyRef != nil: var frontProxy operatorv1alpha1.FrontProxy @@ -120,15 +121,12 @@ func (r *KubeconfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) if ref == nil || ref.Name == "" { return ctrl.Result{}, errors.New("the FrontProxy does not reference a (valid) RootShard") } - var rootShard operatorv1alpha1.RootShard - if err := r.Get(ctx, types.NamespacedName{Name: frontProxy.Spec.RootShard.Reference.Name, Namespace: req.Namespace}, &rootShard); err != nil { + if err := r.Get(ctx, types.NamespacedName{Name: frontProxy.Spec.RootShard.Reference.Name, Namespace: req.Namespace}, rootShard); err != nil { return ctrl.Result{}, fmt.Errorf("failed to get RootShard: %w", err) } - clientCertIssuer = resources.GetRootShardCAName(&rootShard, operatorv1alpha1.FrontProxyClientCA) - serverCA = resources.GetRootShardCAName(&rootShard, operatorv1alpha1.ServerCA) - serverURL = fmt.Sprintf("https://%s:6443", rootShard.Spec.External.Hostname) - serverName = rootShard.Spec.External.Hostname + clientCertIssuer = resources.GetRootShardCAName(rootShard, operatorv1alpha1.FrontProxyClientCA) + serverCA = resources.GetRootShardCAName(rootShard, operatorv1alpha1.ServerCA) default: return ctrl.Result{}, fmt.Errorf("no valid target for kubeconfig found") @@ -156,14 +154,12 @@ func (r *KubeconfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{RequeueAfter: time.Second * 5}, nil } - rootWSURL, err := url.JoinPath(serverURL, "clusters", "root") + reconciler, err := kubeconfig.KubeconfigSecretReconciler(&kc, rootShard, shard, serverCASecret, clientCertSecret) if err != nil { return ctrl.Result{}, err } - if err := k8creconciling.ReconcileSecrets(ctx, []k8creconciling.NamedSecretReconcilerFactory{ - kubeconfig.KubeconfigSecretReconciler(&kc, serverCASecret, clientCertSecret, serverName, rootWSURL), - }, req.Namespace, r.Client); err != nil { + if err := k8creconciling.ReconcileSecrets(ctx, []k8creconciling.NamedSecretReconcilerFactory{reconciler}, req.Namespace, r.Client); err != nil { return ctrl.Result{}, err } diff --git a/internal/resources/kubeconfig/secret.go b/internal/resources/kubeconfig/secret.go index 3e4f1745..6ad56a85 100644 --- a/internal/resources/kubeconfig/secret.go +++ b/internal/resources/kubeconfig/secret.go @@ -18,6 +18,7 @@ package kubeconfig import ( "fmt" + "net/url" "k8c.io/reconciler/pkg/reconciling" @@ -25,42 +26,109 @@ import ( "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "github.com/kcp-dev/kcp-operator/internal/resources" operatorv1alpha1 "github.com/kcp-dev/kcp-operator/sdk/apis/operator/v1alpha1" ) -func KubeconfigSecretReconciler(kubeconfig *operatorv1alpha1.Kubeconfig, caSecret, certSecret *corev1.Secret, serverName, serverURL string) reconciling.NamedSecretReconcilerFactory { - return func() (string, reconciling.SecretReconciler) { - return kubeconfig.Spec.SecretRef.Name, func(secret *corev1.Secret) (*corev1.Secret, error) { - var config *clientcmdapi.Config +const ( + baseContext string = "base" + shardBaseContext string = "shard-base" + defaultContext string = "default" +) - if secret.Data == nil { - secret.Data = make(map[string][]byte) - } +func KubeconfigSecretReconciler( + kubeconfig *operatorv1alpha1.Kubeconfig, + rootShard *operatorv1alpha1.RootShard, + shard *operatorv1alpha1.Shard, + caSecret, certSecret *corev1.Secret, +) (reconciling.NamedSecretReconcilerFactory, error) { + config := &clientcmdapi.Config{ + Clusters: map[string]*clientcmdapi.Cluster{}, + Contexts: map[string]*clientcmdapi.Context{}, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + kubeconfig.Spec.Username: { + ClientCertificateData: certSecret.Data["tls.crt"], + ClientKeyData: certSecret.Data["tls.key"], + }, + }, + } - config = &clientcmdapi.Config{} + addCluster := func(clusterName, url string) { + config.Clusters[clusterName] = &clientcmdapi.Cluster{ + Server: url, + CertificateAuthorityData: caSecret.Data["tls.crt"], + } + } + addContext := func(contextName, clusterName string) { + config.Contexts[contextName] = &clientcmdapi.Context{ + Cluster: clusterName, + AuthInfo: kubeconfig.Spec.Username, + } + } - config.Clusters = map[string]*clientcmdapi.Cluster{ - serverName: { - Server: serverURL, - CertificateAuthorityData: caSecret.Data["tls.crt"], - }, - } + switch { + case kubeconfig.Spec.Target.RootShardRef != nil: + if rootShard == nil { + panic("RootShard must be provided when kubeconfig targets one.") + } - contextName := fmt.Sprintf("%s:%s", serverName, kubeconfig.Spec.Username) + serverURL := resources.GetRootShardBaseURL(rootShard) + defaultURL, err := url.JoinPath(serverURL, "clusters", "root") + if err != nil { + return nil, err + } - config.Contexts = map[string]*clientcmdapi.Context{ - contextName: { - Cluster: serverName, - AuthInfo: kubeconfig.Spec.Username, - }, - } - config.AuthInfos = map[string]*clientcmdapi.AuthInfo{ - kubeconfig.Spec.Username: { - ClientCertificateData: certSecret.Data["tls.crt"], - ClientKeyData: certSecret.Data["tls.key"], - }, + addCluster(defaultContext, defaultURL) + addContext(defaultContext, defaultContext) + addCluster(baseContext, serverURL) + addContext(baseContext, baseContext) + addContext(shardBaseContext, baseContext) + config.CurrentContext = defaultContext + + case kubeconfig.Spec.Target.ShardRef != nil: + if shard == nil { + panic("Shard must be provided when kubeconfig targets one.") + } + + serverURL := resources.GetShardBaseURL(shard) + defaultURL, err := url.JoinPath(serverURL, "clusters", "root") + if err != nil { + return nil, err + } + + addCluster(defaultContext, defaultURL) + addContext(defaultContext, defaultContext) + addCluster(baseContext, serverURL) + addContext(baseContext, baseContext) + addContext(shardBaseContext, baseContext) + config.CurrentContext = defaultContext + + case kubeconfig.Spec.Target.FrontProxyRef != nil: + if rootShard == nil { + panic("RootShard must be provided when kubeconfig targets a FrontProxy.") + } + + serverURL := fmt.Sprintf("https://%s:6443", rootShard.Spec.External.Hostname) + defaultURL, err := url.JoinPath(serverURL, "clusters", "root") + if err != nil { + return nil, err + } + + addCluster(baseContext, serverURL) + addCluster(defaultContext, defaultURL) + addContext(defaultContext, defaultContext) + addContext(baseContext, baseContext) + config.CurrentContext = defaultContext + + default: + panic("Called reconciler for an invalid kubeconfig, this should not have happened.") + } + + return func() (string, reconciling.SecretReconciler) { + return kubeconfig.Spec.SecretRef.Name, func(secret *corev1.Secret) (*corev1.Secret, error) { + if secret.Data == nil { + secret.Data = make(map[string][]byte) } - config.CurrentContext = contextName data, err := clientcmd.Write(*config) if err != nil { @@ -71,5 +139,5 @@ func KubeconfigSecretReconciler(kubeconfig *operatorv1alpha1.Kubeconfig, caSecre return secret, nil } - } + }, nil }