Skip to content

Commit

Permalink
feat: member clusters can join the control cluster through token
Browse files Browse the repository at this point in the history
  • Loading branch information
qclc committed Dec 5, 2023
1 parent bd2d0f4 commit 931128f
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 49 deletions.
1 change: 1 addition & 0 deletions pkg/controllers/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const (
ClusterClientKeyKey = "client-key-data"
ClusterCertificateAuthorityKey = "certificate-authority-data"
ClusterServiceAccountTokenKey = "service-account-token-data"
ClusterBootstrapTokenKey = "bootstrap-token-data"
)

const (
Expand Down
2 changes: 1 addition & 1 deletion pkg/controllers/federatedcluster/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ func (c *FederatedClusterController) getClusterClient(
return nil, nil, fmt.Errorf("failed to get cluster secret: %w", err)
}

if err := clusterutil.PopulateAuthDetailsFromSecret(restConfig, cluster.Spec.Insecure, clusterSecret, false); err != nil {
if err := clusterutil.PopulateAuthDetailsFromSecret(restConfig, cluster.Spec.Insecure, clusterSecret, true); err != nil {

Check warning on line 478 in pkg/controllers/federatedcluster/controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/federatedcluster/controller.go#L478

Added line #L478 was not covered by tests
return nil, nil, fmt.Errorf("cluster secret malformed: %w", err)
}

Expand Down
79 changes: 31 additions & 48 deletions pkg/util/cluster/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,7 @@ import (
"k8s.io/client-go/tools/clientcmd"

fedcorev1a1 "github.com/kubewharf/kubeadmiral/pkg/apis/core/v1alpha1"
)

// User account keys
const (
ClientCertificateKey = "client-certificate-data"
ClientKeyKey = "client-key-data"
CertificateAuthorityKey = "certificate-authority-data"
)

// Service account keys
const (
ServiceAccountTokenKey = "service-account-token-data"
"github.com/kubewharf/kubeadmiral/pkg/controllers/common"
)

func BuildClusterConfig(
Expand All @@ -52,7 +41,7 @@ func BuildClusterConfig(
fedClient,
restConfig,
fedSystemNamespace,
cluster.Spec.UseServiceAccountToken,
!cluster.Spec.UseServiceAccountToken,

Check warning on line 44 in pkg/util/cluster/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/util/cluster/client.go#L44

Added line #L44 was not covered by tests
)
}

Expand All @@ -69,7 +58,7 @@ func BuildRawClusterConfig(
fedClient,
restConfig,
fedSystemNamespace,
false,
true,

Check warning on line 61 in pkg/util/cluster/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/util/cluster/client.go#L61

Added line #L61 was not covered by tests
)
}

Expand All @@ -78,7 +67,7 @@ func buildClusterConfig(
fedClient kubernetes.Interface,
restConfig *rest.Config,
fedSystemNamespace string,
useServiceAccountToken bool,
useBootstrap bool,
) (*rest.Config, error) {
apiEndpoint := cluster.Spec.APIEndpoint
if len(apiEndpoint) == 0 {
Expand Down Expand Up @@ -107,7 +96,7 @@ func buildClusterConfig(
return nil, err
}

err = PopulateAuthDetailsFromSecret(clusterConfig, cluster.Spec.Insecure, secret, useServiceAccountToken)
err = PopulateAuthDetailsFromSecret(clusterConfig, cluster.Spec.Insecure, secret, useBootstrap)

Check warning on line 99 in pkg/util/cluster/client.go

View check run for this annotation

Codecov / codecov/patch

pkg/util/cluster/client.go#L99

Added line #L99 was not covered by tests
if err != nil {
return nil, fmt.Errorf("cannot build rest config from cluster secret: %w", err)
}
Expand All @@ -118,45 +107,39 @@ func PopulateAuthDetailsFromSecret(
clusterConfig *rest.Config,
insecure bool,
secret *corev1.Secret,
useServiceAccount bool,
useBootstrap bool,
) error {
var exists bool

if useServiceAccount {
serviceAccountToken, exists := secret.Data[ServiceAccountTokenKey]
if !exists {
return fmt.Errorf("%q data is missing from secret", ServiceAccountTokenKey)
}
clusterConfig.BearerToken = string(serviceAccountToken)

if insecure {
clusterConfig.Insecure = true
} else {
clusterConfig.CAData, exists = secret.Data[CertificateAuthorityKey]
if !exists {
return fmt.Errorf("%q data is missing from secret and insecure is false", CertificateAuthorityKey)
}
}
if insecure {
clusterConfig.Insecure = true
} else {
clusterConfig.CertData, exists = secret.Data[ClientCertificateKey]
if !exists {
return fmt.Errorf("%q data is missing from secret", ClientCertificateKey)
// if federatedCluster.Spec.Insecure is false, ca data is required
if clusterConfig.CAData = secret.Data[common.ClusterCertificateAuthorityKey]; len(clusterConfig.CAData) == 0 {
return fmt.Errorf("%q data is missing from secret and insecure is false", common.ClusterCertificateAuthorityKey)
}
}

clusterConfig.KeyData, exists = secret.Data[ClientKeyKey]
if !exists {
return fmt.Errorf("%q data is missing from secret", ClientKeyKey)
}
// if useBootstrap is true, we will use the client authentication specified by user
// else, we will use the token created by admiral
if useBootstrap {
var bootstrapTokenExist bool
clusterConfig.BearerToken, bootstrapTokenExist = getTokenDataFromSecret(secret, common.ClusterBootstrapTokenKey)
clusterConfig.CertData, clusterConfig.KeyData = secret.Data[common.ClusterClientCertificateKey], secret.Data[common.ClusterClientKeyKey]

if insecure {
clusterConfig.Insecure = true
} else {
clusterConfig.CAData, exists = secret.Data[CertificateAuthorityKey]
if !exists {
return fmt.Errorf("%q data is missing from secret", CertificateAuthorityKey)
}
if !bootstrapTokenExist && (len(clusterConfig.CertData) == 0 || len(clusterConfig.KeyData) == 0) {
return fmt.Errorf("the client authentication information is missing from secret, " +
"at least token or (certificate, key) information is required")
}
} else {
var saTokenExist bool
if clusterConfig.BearerToken, saTokenExist = getTokenDataFromSecret(secret, common.ClusterServiceAccountTokenKey); !saTokenExist {
return fmt.Errorf("%q data is missing from secret", common.ClusterServiceAccountTokenKey)
}
}

return nil
}

func getTokenDataFromSecret(secret *corev1.Secret, tokenKey string) (string, bool) {
bootstrapToken := secret.Data[tokenKey]
return string(bootstrapToken), len(bootstrapToken) != 0
}
143 changes: 143 additions & 0 deletions pkg/util/cluster/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
Copyright 2023 The KubeAdmiral Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cluster

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/rest"

"github.com/kubewharf/kubeadmiral/pkg/controllers/common"
)

func TestPopulateAuthDetailsFromSecret(t *testing.T) {
tests := []struct {
name string
insecure bool
inputSecret *corev1.Secret
useBootstrap bool
wantTokenKind string
wantErr bool
expectErr error
}{
{
name: "normal test, insecure=true",
insecure: true,
inputSecret: buildSecretData("ca", "cert", "key", "bootstrapToken", ""),
useBootstrap: true,
wantTokenKind: common.ClusterBootstrapTokenKey,
wantErr: false,
},
{
name: "normal test, insecure=false",
insecure: true,
inputSecret: buildSecretData("ca", "cert", "key", "bootstrapToken", ""),
useBootstrap: true,
wantTokenKind: common.ClusterBootstrapTokenKey,
wantErr: false,
},
{
name: "normal test, useBootstrap==true, (cert,key) is empty, bootstrapToken is not empty",
insecure: true,
inputSecret: buildSecretData("ca", "", "", "bootstrapToken", ""),
useBootstrap: true,
wantTokenKind: common.ClusterBootstrapTokenKey,
wantErr: false,
},
{
name: "normal test, useBootstrap==true, (cert,key) is not empty, bootstrapToken is empty",
insecure: true,
inputSecret: buildSecretData("ca", "cert", "key", "", ""),
useBootstrap: true,
wantTokenKind: common.ClusterBootstrapTokenKey,
wantErr: false,
},
{
name: "normal test, useBootstrap==false",
insecure: true,
inputSecret: buildSecretData("ca", "", "", "", "sat"),
useBootstrap: false,
wantTokenKind: common.ClusterServiceAccountTokenKey,
wantErr: false,
},
// error test
{
name: "insecure=false, but secret.Data[certificate-authority-data] is empty",
insecure: false,
inputSecret: buildSecretData("", "", "", "", ""),
wantErr: true,
expectErr: fmt.Errorf(
"%q data is missing from secret and insecure is false", common.ClusterCertificateAuthorityKey),
},
{
name: "useBootstrap=false, but Secret.Data[service-account-token-data] is empty",
insecure: false,
useBootstrap: false,
inputSecret: buildSecretData("ca", "", "", "", ""),
wantErr: true,
expectErr: fmt.Errorf("%q data is missing from secret", common.ClusterServiceAccountTokenKey),
},
{
name: "useBootstrap=true, but both (cert,key) and bootstrapToken are empty",
insecure: false,
useBootstrap: true,
inputSecret: buildSecretData("ca", "", "", "", ""),
wantErr: true,
expectErr: fmt.Errorf("the client authentication information is missing from secret, " +
"at least token or (certificate, key) information is required"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actualClusterConfig := &rest.Config{}
err := PopulateAuthDetailsFromSecret(actualClusterConfig, tt.insecure, tt.inputSecret, tt.useBootstrap)

if tt.wantErr {
assert.Equal(t, tt.expectErr.Error(), err.Error())
} else {
assert.Equal(t, tt.insecure, actualClusterConfig.Insecure)
if !tt.insecure {
assert.Equal(t, tt.inputSecret.Data[common.ClusterCertificateAuthorityKey], actualClusterConfig.CAData)
}

if tt.useBootstrap {
assert.Equal(t, tt.inputSecret.Data[common.ClusterClientCertificateKey], actualClusterConfig.CertData)
assert.Equal(t, tt.inputSecret.Data[common.ClusterClientKeyKey], actualClusterConfig.KeyData)
} else if actualClusterConfig.CertData != nil || actualClusterConfig.KeyData != nil {
t.Errorf("PopulateAuthDetailsFromSecret() error, clusterConfig.CertData and KeyData should be nil")
}

assert.Equal(t, string(tt.inputSecret.Data[tt.wantTokenKind]), actualClusterConfig.BearerToken)
}
})
}
}

func buildSecretData(ca, cert, key, bt, sat string) *corev1.Secret {
return &corev1.Secret{
Data: map[string][]byte{
common.ClusterCertificateAuthorityKey: []byte(ca),
common.ClusterClientCertificateKey: []byte(cert),
common.ClusterClientKeyKey: []byte(key),
common.ClusterBootstrapTokenKey: []byte(bt),
common.ClusterServiceAccountTokenKey: []byte(sat),
},
}
}

0 comments on commit 931128f

Please sign in to comment.