Skip to content

Commit

Permalink
Allow CAPBK to generate JoinConfiguration discovery kubeconfig
Browse files Browse the repository at this point in the history
Signed-off-by: Vince Prignano <vince@prigna.com>
  • Loading branch information
vincepri committed Jun 26, 2024
1 parent 1e6896e commit 4070b0b
Show file tree
Hide file tree
Showing 21 changed files with 1,495 additions and 51 deletions.
24 changes: 24 additions & 0 deletions bootstrap/kubeadm/api/v1beta1/kubeadm_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstraputil "k8s.io/cluster-bootstrap/token/util"
)
Expand Down Expand Up @@ -512,6 +513,29 @@ type BootstrapTokenDiscovery struct {
type FileDiscovery struct {
// KubeConfigPath is used to specify the actual file path or URL to the kubeconfig file from which to load cluster information
KubeConfigPath string `json:"kubeConfigPath"`

// KubeConfig is used (optionally) to generate a KubeConfig based on the Cluster's information.
// The kubeconfig is generated with a server and context matching the Cluster's name,
// Host address (server field) information is automatically populated based on the Cluster's ControlPlaneEndpoint.
// Certificate Authority (certificate-authority-data field) is gathered from the cluster's CA secret.
// +optional
KubeConfig *FileDiscoveryKubeConfig `json:"kubeConfig,omitempty"`
}

// FileDiscoveryKubeConfig contains elements describing how to generate the kubeconfig for bootstrapping.
type FileDiscoveryKubeConfig struct {
// Cluster contains information about how to communicate with the kubernetes cluster.
//
// By default the following fields are automatically populated:
// - Name with the Cluster's name.
// - Server with the Cluster's ControlPlaneEndpoint.
// - CertificateAuthorityData with the Cluster's CA certificate.
// +optional
Cluster *clientcmdv1.Cluster `json:"cluster,omitempty"`

// User contains information that describes identity information.
// This is use to tell the kubernetes cluster who you are.
User clientcmdv1.AuthInfo `json:"user"`
}

// HostPathMount contains elements describing volumes that are mounted from the
Expand Down
29 changes: 28 additions & 1 deletion bootstrap/kubeadm/api/v1beta1/zz_generated.deepcopy.go

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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstrapsecretutil "k8s.io/cluster-bootstrap/util/secrets"
"k8s.io/klog/v2"
Expand All @@ -40,6 +41,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/yaml"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
Expand Down Expand Up @@ -741,6 +743,15 @@ func (r *KubeadmConfigReconciler) joinControlplane(ctx context.Context, scope *S
return ctrl.Result{}, err
}

if discoveryFile := scope.Config.Spec.JoinConfiguration.Discovery.File; discoveryFile != nil && discoveryFile.KubeConfig != nil {
kubeconfig, err := r.resolveDiscoveryKubeConfig(scope.Config)
if err != nil {
conditions.MarkFalse(scope.Config, bootstrapv1.DataSecretAvailableCondition, bootstrapv1.DataSecretGenerationFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
return ctrl.Result{}, err
}
files = append(files, *kubeconfig)
}

controlPlaneJoinInput := &cloudinit.ControlPlaneJoinInput{
JoinConfiguration: joinData,
Certificates: certificates,
Expand Down Expand Up @@ -842,6 +853,47 @@ func (r *KubeadmConfigReconciler) resolveUsers(ctx context.Context, cfg *bootstr
return collected, nil
}

func (r *KubeadmConfigReconciler) resolveDiscoveryKubeConfig(config *bootstrapv1.KubeadmConfig) (*bootstrapv1.File, error) {
cfg := config.Spec.JoinConfiguration.Discovery.File
if cfg == nil || cfg.KubeConfig == nil {
return nil, errors.New("no discovery configuration file to resolve")
}
kubeconfig := clientcmdv1.Config{
CurrentContext: "default",
Contexts: []clientcmdv1.NamedContext{
{
Name: "default",
Context: clientcmdv1.Context{
Cluster: "default",
AuthInfo: "default",
},
},
},
Clusters: []clientcmdv1.NamedCluster{
{
Name: "default",
Cluster: *cfg.KubeConfig.Cluster,
},
},
AuthInfos: []clientcmdv1.NamedAuthInfo{
{
Name: "default",
AuthInfo: cfg.KubeConfig.User,
},
},
}
b, err := yaml.Marshal(kubeconfig)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal kubeconfig from JoinConfiguration.Discovery.File.KubeConfig")
}
return &bootstrapv1.File{
Path: cfg.KubeConfigPath,
Owner: "root:root",
Permissions: "0640",
Content: string(b),
}, nil
}

// resolveSecretUserContent returns passwd fetched from a referenced secret object.
func (r *KubeadmConfigReconciler) resolveSecretPasswordContent(ctx context.Context, ns string, source bootstrapv1.User) ([]byte, error) {
secret := &corev1.Secret{}
Expand Down Expand Up @@ -970,7 +1022,7 @@ func (r *KubeadmConfigReconciler) reconcileDiscovery(ctx context.Context, cluste

// if config already contains a file discovery configuration, respect it without further validations
if config.Spec.JoinConfiguration.Discovery.File != nil {
return ctrl.Result{}, nil
return r.reconcileDiscoveryFile(ctx, cluster, config, certificates)
}

// otherwise it is necessary to ensure token discovery is properly configured
Expand Down Expand Up @@ -1026,6 +1078,35 @@ func (r *KubeadmConfigReconciler) reconcileDiscovery(ctx context.Context, cluste
return ctrl.Result{}, nil
}

func (r *KubeadmConfigReconciler) reconcileDiscoveryFile(ctx context.Context, cluster *clusterv1.Cluster, config *bootstrapv1.KubeadmConfig, certificates secret.Certificates) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
cfg := config.Spec.JoinConfiguration.Discovery.File.KubeConfig
if config.Spec.JoinConfiguration.Discovery.File.KubeConfig == nil {
// Nothing else to do.
return ctrl.Result{}, nil
}

if cfg.Cluster == nil {
cfg.Cluster = &clientcmdv1.Cluster{}
}

if cfg.Cluster.Server == "" {
if !cluster.Spec.ControlPlaneEndpoint.IsValid() {
log.V(1).Info("Waiting for Cluster Controller to set Cluster.Spec.ControlPlaneEndpoint")
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
}
cfg.Cluster.Server = fmt.Sprintf("https://%s", cluster.Spec.ControlPlaneEndpoint.String())
log.V(3).Info("Altering JoinConfiguration.Discovery.File.KubeConfig.Cluster.Server", "apiServerEndpoint", cfg.Cluster.Server)
}

if len(cfg.Cluster.CertificateAuthorityData) == 0 {
cfg.Cluster.CertificateAuthorityData = certificates.GetByPurpose(secret.ClusterCA).KeyPair.Cert
log.V(3).Info("Altering JoinConfiguration.Discovery.File.KubeConfig.CertificateAuthorityData")
}

return ctrl.Result{}, nil
}

// reconcileTopLevelObjectSettings injects into config.ClusterConfiguration values from top level objects like cluster and machine.
// The implementation func respect user provided config values, but in case some of them are missing, values from top level objects are used.
func (r *KubeadmConfigReconciler) reconcileTopLevelObjectSettings(ctx context.Context, cluster *clusterv1.Cluster, machine *clusterv1.Machine, config *bootstrapv1.KubeadmConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
Expand All @@ -46,9 +47,11 @@ import (
"sigs.k8s.io/cluster-api/feature"
"sigs.k8s.io/cluster-api/internal/test/builder"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/certs"
"sigs.k8s.io/cluster-api/util/conditions"
"sigs.k8s.io/cluster-api/util/patch"
"sigs.k8s.io/cluster-api/util/secret"
utilyaml "sigs.k8s.io/cluster-api/util/yaml"
)

// MachineToBootstrapMapFunc return kubeadm bootstrap configref name when configref exists.
Expand Down Expand Up @@ -1497,6 +1500,37 @@ func TestKubeadmConfigReconciler_Reconcile_DiscoveryReconcileBehaviors(t *testin
return nil
},
},
{
name: "Respect discoveryConfiguration.File.KubeConfig",
cluster: goodcluster,
config: &bootstrapv1.KubeadmConfig{
Spec: bootstrapv1.KubeadmConfigSpec{
JoinConfiguration: &bootstrapv1.JoinConfiguration{
Discovery: bootstrapv1.Discovery{
File: &bootstrapv1.FileDiscovery{
KubeConfigPath: "/bootstrap-kubeconfig.yaml",
KubeConfig: &bootstrapv1.FileDiscoveryKubeConfig{
User: clientcmdv1.AuthInfo{
Username: "admin",
Password: "admin",
},
},
},
},
},
},
},
validateDiscovery: func(g *WithT, c *bootstrapv1.KubeadmConfig) error {
d := c.Spec.JoinConfiguration.Discovery
g.Expect(d.BootstrapToken).To(BeNil())
g.Expect(d.File.KubeConfig.User.Username).To(Equal("admin"))
g.Expect(d.File.KubeConfig.User.Password).To(Equal("admin"))
g.Expect(d.File.KubeConfig.Cluster).ToNot(BeNil())
g.Expect(d.File.KubeConfig.Cluster.Server).To(Equal("https://example.com:6443"))
g.Expect(d.File.KubeConfig.Cluster.CertificateAuthorityData).To(BeEquivalentTo("ca-data"))
return nil
},
},
{
name: "Respect discoveryConfiguration.BootstrapToken.APIServerEndpoint",
cluster: goodcluster,
Expand Down Expand Up @@ -1573,7 +1607,15 @@ func TestKubeadmConfigReconciler_Reconcile_DiscoveryReconcileBehaviors(t *testin
KubeadmInitLock: &myInitLocker{},
}

res, err := k.reconcileDiscovery(ctx, tc.cluster, tc.config, secret.Certificates{})
res, err := k.reconcileDiscovery(ctx, tc.cluster, tc.config, secret.Certificates{
&secret.Certificate{
Purpose: secret.ClusterCA,
KeyPair: &certs.KeyPair{
Cert: []byte("ca-data"),
Key: []byte("ca-key"),
},
},
})
g.Expect(res.IsZero()).To(BeTrue())
g.Expect(err).ToNot(HaveOccurred())

Expand Down Expand Up @@ -2147,6 +2189,97 @@ func TestKubeadmConfigReconciler_ResolveFiles(t *testing.T) {
}
}

func TestKubeadmConfigReconciler_ResolveDiscoveryFileKubeConfig(t *testing.T) {
cases := map[string]struct {
cfg *bootstrapv1.KubeadmConfig
expect *bootstrapv1.File
err string
}{
"should generate the bootstrap kubeconfig correctly": {
cfg: &bootstrapv1.KubeadmConfig{
Spec: bootstrapv1.KubeadmConfigSpec{
JoinConfiguration: &bootstrapv1.JoinConfiguration{
Discovery: bootstrapv1.Discovery{
File: &bootstrapv1.FileDiscovery{
KubeConfigPath: "/bootstrap-kubeconfig.yaml",
KubeConfig: &bootstrapv1.FileDiscoveryKubeConfig{
User: clientcmdv1.AuthInfo{
Username: "admin",
Password: "admin",
},
},
},
},
},
},
},
expect: &bootstrapv1.File{
Path: "/bootstrap-kubeconfig.yaml",
Owner: "root:root",
Permissions: "0640",
Content: utilyaml.Raw(`
clusters:
- cluster:
certificate-authority-data: Y2EtZGF0YQ==
server: https://example.com:6443
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
preferences: {}
users:
- name: default
user:
password: admin
username: admin
`),
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
g := NewWithT(t)

myclient := fake.NewClientBuilder().Build()
k := &KubeadmConfigReconciler{
Client: myclient,
SecretCachingClient: myclient,
KubeadmInitLock: &myInitLocker{},
}

_, err := k.reconcileDiscoveryFile(ctx, &clusterv1.Cluster{
Spec: clusterv1.ClusterSpec{
ControlPlaneEndpoint: clusterv1.APIEndpoint{
Host: "example.com",
Port: 6443,
},
},
}, tc.cfg, secret.Certificates{
&secret.Certificate{
Purpose: secret.ClusterCA,
KeyPair: &certs.KeyPair{
Cert: []byte("ca-data"),
Key: []byte("ca-key"),
},
},
})
g.Expect(err).ToNot(HaveOccurred())

file, err := k.resolveDiscoveryKubeConfig(tc.cfg)
if tc.err != "" {
g.Expect(err).To(MatchError(ContainSubstring(tc.err)))
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(file).To(BeEquivalentTo(tc.expect))
})
}
}

func TestKubeadmConfigReconciler_ResolveUsers(t *testing.T) {
fakePasswd := "bar"
testSecret := &corev1.Secret{
Expand Down
Loading

0 comments on commit 4070b0b

Please sign in to comment.