Skip to content

Commit

Permalink
Support multiple bootstrapkubeconfigs in registration agent.
Browse files Browse the repository at this point in the history
Signed-off-by: xuezhaojun <zxue@redhat.com>
  • Loading branch information
xuezhaojun committed Apr 25, 2024
1 parent 0882f6d commit 649f468
Show file tree
Hide file tree
Showing 14 changed files with 990 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,51 @@ spec:
description: RegistrationConfiguration contains the configuration
of registration
properties:
bootstrapKubeConfigs:
description: "BootstrapKubeConfigs defines the ordered list of
bootstrap kubeconfigs. The order decides which bootstrap kubeconfig
to use first when rebootstrap. \n When the agent loses the connection
to the current hub over HubConnectionTimeoutSeconds, or the
managedcluster CR is set `hubAcceptsClient=false` on the hub,
the controller marks the related bootstrap kubeconfig as \"failed\".
\n A failed bootstrapkubeconfig won't be used for the duration
specified by SkipFailedBootstrapKubeConfigSeconds. But if the
user updates the content of a failed bootstrapkubeconfig, the
\"failed\" mark will be cleared."
properties:
localSecretsConfig:
description: LocalSecretsConfig include a list of secrets
that contains the kubeconfigs for ordered bootstrap kubeconifigs.
The secrets must be in the same namespace where the agent
controller runs.
properties:
hubConnectionTimeoutSeconds:
default: 600
description: HubConnectionTimeoutSeconds is used to set
the timeout of connecting to the hub cluster. When agent
loses the connection to the hub over the timeout seconds,
the agent do a rebootstrap. By default is 10 mins.
format: int32
minimum: 180
type: integer
secretNames:
description: SecretNames is a list of secret names. The
secrets are in the same namespace where the agent controller
runs.
items:
type: string
type: array
type: object
type:
default: None
description: Type specifies the type of priority bootstrap
kubeconfigs. By default, it is set to None, representing
no priority bootstrap kubeconfigs are set.
enum:
- None
- LocalSecrets
type: string
type: object
clientCertExpirationSeconds:
description: clientCertExpirationSeconds represents the seconds
of a client certificate to expire. If it is not set or 0, the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ metadata:
categories: Integration & Delivery,OpenShift Optional
certified: "false"
containerImage: quay.io/open-cluster-management/registration-operator:latest
createdAt: "2024-04-10T15:46:14Z"
createdAt: "2024-04-25T09:39:17Z"
description: Manages the installation and upgrade of the Klusterlet.
operators.operatorframework.io/builder: operator-sdk-v1.32.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package bootstrapkubeconfigsmanager

import (
"context"
"fmt"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

type boostrapKubeConfigStatus string

const (
boostrapKubeConfigStatusInValid boostrapKubeConfigStatus = "InValid"
boostrapKubeConfigStatusValid boostrapKubeConfigStatus = "Valid"
)

// bootstrapKubeConfig represents a bootstrap kubeconfig that agent can use to bootstrap a managed cluster.
type boostrapKubeConfig interface {
// Name returns the name of the bootstrap kubeconfig. It helps to identify the bootstrap kubeconfig.
Name() string

// KubeConfigData returns the kubeconfig data of the bootstrap kubeconfig.
KubeConfigData() ([]byte, error)

// Status returns the status of the bootstrap kubeconfig.
// A bootstrap kubeconfig has two status: Valid and InValid.
Status() (boostrapKubeConfigStatus, error)

// Fail means at the time t, the bootstrap kubeconfig failed to connect to the hub cluster.
Fail(t time.Time) error
}

var _ boostrapKubeConfig = &boostrapKubeConfigSecretImpl{}

const (
// BootstrapKubeconfigFailedTimeAnnotationKey represents the time when the bootstrap kubeconfig failed
BootstrapKubeconfigFailedTimeAnnotationKey = "agent.open-cluster-management.io/bootstrap-kubeconfig-failed-time"
)

type boostrapKubeConfigSecretImpl struct {
secretName string
secretNamespace string
skipFailedBootstrapKubeconfigSeconds int32 // if a bootstrap kubeconfig failed, in 3 mins, it can't be used in rebootstrap.
kubeClient kubernetes.Interface
}

func (b *boostrapKubeConfigSecretImpl) Name() string {
return b.secretName
}

func (b *boostrapKubeConfigSecretImpl) KubeConfigData() ([]byte, error) {
secret, err := b.kubeClient.CoreV1().Secrets(b.secretNamespace).Get(context.Background(), b.secretName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("get the bootstrap kubeconfig secret failed: %v", err)
}
if secret.Data == nil {
return nil, fmt.Errorf("the bootstrap kubeconfig secret %s has no data", b.secretName)
}
kubeconfigData, ok := secret.Data["kubeconfig"]
if !ok {
return nil, fmt.Errorf("the bootstrap kubeconfig secret %s has no kubeconfig data", b.secretName)
}
return kubeconfigData, nil
}

func (b *boostrapKubeConfigSecretImpl) Status() (boostrapKubeConfigStatus, error) {
secret, err := b.kubeClient.CoreV1().Secrets(b.secretNamespace).Get(context.Background(), b.secretName, metav1.GetOptions{})
if err != nil {
return boostrapKubeConfigStatusInValid, fmt.Errorf("get the bootstrap kubeconfig secret failed: %v", err)
}

if secret.Annotations == nil {
return boostrapKubeConfigStatusValid, nil
}

now := time.Now()
if failedTime, ok := secret.Annotations[BootstrapKubeconfigFailedTimeAnnotationKey]; ok {
failedTimeParsed, err := time.Parse(time.RFC3339, failedTime)
if err != nil {
return boostrapKubeConfigStatusInValid, fmt.Errorf("failed to parse the failed time %s of the secret %s: %v", failedTime, secret.Name, err)
}
if now.Sub(failedTimeParsed).Seconds() < float64(b.skipFailedBootstrapKubeconfigSeconds) {
return boostrapKubeConfigStatusInValid, nil
}
}
return boostrapKubeConfigStatusValid, nil
}

func (b *boostrapKubeConfigSecretImpl) Fail(t time.Time) error {
secret, err := b.kubeClient.CoreV1().Secrets(b.secretNamespace).Get(context.Background(), b.secretName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("get the bootstrap kubeconfig secret failed: %v", err)
}
secretCopy := secret.DeepCopy()
if secretCopy.Annotations == nil {
secretCopy.Annotations = make(map[string]string)
}
secretCopy.Annotations[BootstrapKubeconfigFailedTimeAnnotationKey] = t.Format(time.RFC3339)
_, err = b.kubeClient.CoreV1().Secrets(b.secretNamespace).Update(context.Background(), secretCopy, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("update the secret %s failed: %v", b.secretName, err)
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package bootstrapkubeconfigsmanager

import (
"testing"
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)

func TestBootstrapKubeConfigSecretImpl(t *testing.T) {
testcases := []struct {
name string
mockClient func(failedTime time.Time) *fake.Clientset
validate func(c boostrapKubeConfig)
}{
{
name: "Valid BootstrapKubeConfig",
mockClient: func(_ time.Time) *fake.Clientset {
// Create a mock secret
mockSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mock-secret",
Namespace: "test-namespace",
},
Data: map[string][]byte{
"kubeconfig": []byte("mock-kubeconfig"),
},
}

// Create a mock kubeClient
mockKubeClient := &fake.Clientset{}
mockKubeClient.AddReactor("get", "secrets", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, mockSecret, nil
})
return mockKubeClient
},
validate: func(c boostrapKubeConfig) {
if c.Name() != "mock-secret" {
t.Errorf("Expected name %v, but got %v", "mock-secret", c.Name())
}

kubeConfigData, err := c.KubeConfigData()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if string(kubeConfigData) != "mock-kubeconfig" {
t.Errorf("Expected kubeconfig data %v, but got %v", "mock-kubeconfig", string(kubeConfigData))
}

status, err := c.Status()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if status != boostrapKubeConfigStatusValid {
t.Errorf("Expected status %v, but got %v", boostrapKubeConfigStatusValid, status)
}
},
},
{
name: "Once failed but now valid BootstrapKubeConfig",
mockClient: func(_ time.Time) *fake.Clientset {
// Create a mock secret
mockSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mock-secret",
Namespace: "test-namespace",
Annotations: map[string]string{
BootstrapKubeconfigFailedTimeAnnotationKey: "2021-08-01T00:00:00Z",
},
},
Data: map[string][]byte{
"kubeconfig": []byte("mock-kubeconfig"),
},
}

// Create a mock kubeClient
mockKubeClient := &fake.Clientset{}
mockKubeClient.AddReactor("get", "secrets", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, mockSecret, nil
})
return mockKubeClient
},
validate: func(c boostrapKubeConfig) {
status, err := c.Status()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if status != boostrapKubeConfigStatusValid {
t.Errorf("Expected status %v, but got %v", boostrapKubeConfigStatusValid, status)
}
},
},
{
name: "Recently failed and invalid BootstrapKubeConfig",
mockClient: func(failedTime time.Time) *fake.Clientset {
// Create a mock secret
mockSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mock-secret",
Namespace: "test-namespace",
Annotations: map[string]string{
BootstrapKubeconfigFailedTimeAnnotationKey: failedTime.Format(time.RFC3339),
},
},
Data: map[string][]byte{
"kubeconfig": []byte("mock-kubeconfig"),
},
}

// Create a mock kubeClient
mockKubeClient := &fake.Clientset{}
mockKubeClient.AddReactor("get", "secrets", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, mockSecret, nil
})
return mockKubeClient
},
validate: func(c boostrapKubeConfig) {
status, err := c.Status()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if status != boostrapKubeConfigStatusInValid {
t.Errorf("Expected status %v, but got %v", boostrapKubeConfigStatusInValid, status)
}
},
},
{
name: "Fail a BootstrapKubeConfig",
mockClient: func(_ time.Time) *fake.Clientset {
// Create a mock secret
mockSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "mock-secret",
Namespace: "test-namespace",
},
Data: map[string][]byte{
"kubeconfig": []byte("mock-kubeconfig"),
},
}

// Create a mock kubeClient
mockKubeClient := &fake.Clientset{}
mockKubeClient.AddReactor("get", "secrets", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, mockSecret, nil
})
mockKubeClient.AddReactor("update", "secrets", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
// Get annotation from action
annotations := action.(k8stesting.UpdateAction).GetObject().(*corev1.Secret).Annotations
mockSecret.Annotations = annotations
return true, nil, nil
})
return mockKubeClient
},
validate: func(c boostrapKubeConfig) {
status, err := c.Status()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if status != boostrapKubeConfigStatusValid {
t.Errorf("Expected status %v, but got %v", boostrapKubeConfigStatusValid, status)
}

// Fail the BootstrapKubeConfig
err = c.Fail(time.Now())
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

status, err = c.Status()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if status != boostrapKubeConfigStatusInValid {
t.Errorf("Expected status %v, but got %v", boostrapKubeConfigStatusInValid, status)
}
},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
mockClient := tc.mockClient(time.Now())
bootstrapKubeConfig := &boostrapKubeConfigSecretImpl{
kubeClient: mockClient,
secretNamespace: "test-namespace",
secretName: "mock-secret",
skipFailedBootstrapKubeconfigSeconds: 60,
}
tc.validate(bootstrapKubeConfig)
})
}
}

0 comments on commit 649f468

Please sign in to comment.