Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 15 additions & 1 deletion config/samples/v1alpha1_frontproxy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions config/samples/v1alpha1_kubeconfig_rootshard.yaml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions config/samples/v1alpha1_kubeconfig_shard.yaml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions config/samples/v1alpha1_rootshard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 7 additions & 2 deletions config/samples/v1alpha1_shard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
48 changes: 22 additions & 26 deletions internal/controller/kubeconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import (
"context"
"errors"
"fmt"
"net/url"
"time"

certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
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"
Expand Down Expand Up @@ -70,45 +70,46 @@ 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)
}

ref := shard.Spec.RootShard.Reference
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
Expand All @@ -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")
Expand Down Expand Up @@ -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
}

Expand Down
124 changes: 96 additions & 28 deletions internal/resources/kubeconfig/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,117 @@ package kubeconfig

import (
"fmt"
"net/url"

"k8c.io/reconciler/pkg/reconciling"

corev1 "k8s.io/api/core/v1"
"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 {
Expand All @@ -71,5 +139,5 @@ func KubeconfigSecretReconciler(kubeconfig *operatorv1alpha1.Kubeconfig, caSecre

return secret, nil
}
}
}, nil
}