mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-02-14 10:00:11 +00:00
✨ add token driver for addon registration (#1343)
Some checks failed
Post / images (amd64, placement) (push) Failing after 50s
Post / images (amd64, registration) (push) Failing after 41s
Post / images (amd64, registration-operator) (push) Failing after 40s
Post / images (amd64, work) (push) Failing after 44s
Post / images (arm64, addon-manager) (push) Failing after 41s
Post / images (arm64, placement) (push) Failing after 42s
Post / images (arm64, registration) (push) Failing after 41s
Post / images (amd64, addon-manager) (push) Failing after 5m29s
Post / images (arm64, registration-operator) (push) Failing after 43s
Post / images (arm64, work) (push) Failing after 5m35s
Post / image manifest (addon-manager) (push) Has been skipped
Post / image manifest (placement) (push) Has been skipped
Post / image manifest (registration) (push) Has been skipped
Post / image manifest (registration-operator) (push) Has been skipped
Post / image manifest (work) (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Post / coverage (push) Failing after 40m4s
Scorecard supply-chain security / Scorecard analysis (push) Failing after 4m1s
Some checks failed
Post / images (amd64, placement) (push) Failing after 50s
Post / images (amd64, registration) (push) Failing after 41s
Post / images (amd64, registration-operator) (push) Failing after 40s
Post / images (amd64, work) (push) Failing after 44s
Post / images (arm64, addon-manager) (push) Failing after 41s
Post / images (arm64, placement) (push) Failing after 42s
Post / images (arm64, registration) (push) Failing after 41s
Post / images (amd64, addon-manager) (push) Failing after 5m29s
Post / images (arm64, registration-operator) (push) Failing after 43s
Post / images (arm64, work) (push) Failing after 5m35s
Post / image manifest (addon-manager) (push) Has been skipped
Post / image manifest (placement) (push) Has been skipped
Post / image manifest (registration) (push) Has been skipped
Post / image manifest (registration-operator) (push) Has been skipped
Post / image manifest (work) (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Post / coverage (push) Failing after 40m4s
Scorecard supply-chain security / Scorecard analysis (push) Failing after 4m1s
Signed-off-by: Yang Le <yangle@redhat.com>
This commit is contained in:
@@ -125,6 +125,12 @@ spec:
|
|||||||
{{if .ReservedClusterClaimSuffixes}}
|
{{if .ReservedClusterClaimSuffixes}}
|
||||||
- "--reserved-cluster-claim-suffixes={{ .ReservedClusterClaimSuffixes }}"
|
- "--reserved-cluster-claim-suffixes={{ .ReservedClusterClaimSuffixes }}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .AddOnKubeClientRegistrationAuth}}
|
||||||
|
- "--addon-kubeclient-registration-auth={{ .AddOnKubeClientRegistrationAuth }}"
|
||||||
|
{{end}}
|
||||||
|
{{if gt .AddOnTokenExpirationSeconds 0}}
|
||||||
|
- "--addon-token-expiration-seconds={{ .AddOnTokenExpirationSeconds }}"
|
||||||
|
{{end}}
|
||||||
{{if .AppliedManifestWorkEvictionGracePeriod}}
|
{{if .AppliedManifestWorkEvictionGracePeriod}}
|
||||||
- "--appliedmanifestwork-eviction-grace-period={{ .AppliedManifestWorkEvictionGracePeriod }}"
|
- "--appliedmanifestwork-eviction-grace-period={{ .AppliedManifestWorkEvictionGracePeriod }}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ spec:
|
|||||||
{{if .ReservedClusterClaimSuffixes}}
|
{{if .ReservedClusterClaimSuffixes}}
|
||||||
- "--reserved-cluster-claim-suffixes={{ .ReservedClusterClaimSuffixes }}"
|
- "--reserved-cluster-claim-suffixes={{ .ReservedClusterClaimSuffixes }}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .AddOnKubeClientRegistrationAuth}}
|
||||||
|
- "--addon-kubeclient-registration-auth={{ .AddOnKubeClientRegistrationAuth }}"
|
||||||
|
{{end}}
|
||||||
|
{{if gt .AddOnTokenExpirationSeconds 0}}
|
||||||
|
- "--addon-token-expiration-seconds={{ .AddOnTokenExpirationSeconds }}"
|
||||||
|
{{end}}
|
||||||
{{if eq .RegistrationDriver.AuthType "awsirsa"}}
|
{{if eq .RegistrationDriver.AuthType "awsirsa"}}
|
||||||
- "--registration-auth={{ .RegistrationDriver.AuthType }}"
|
- "--registration-auth={{ .RegistrationDriver.AuthType }}"
|
||||||
- "--hub-cluster-arn={{ .RegistrationDriver.AwsIrsa.HubClusterArn }}"
|
- "--hub-cluster-arn={{ .RegistrationDriver.AwsIrsa.HubClusterArn }}"
|
||||||
|
|||||||
@@ -206,6 +206,11 @@ type klusterletConfig struct {
|
|||||||
Labels map[string]string
|
Labels map[string]string
|
||||||
RegistrationDriver RegistrationDriver
|
RegistrationDriver RegistrationDriver
|
||||||
|
|
||||||
|
// AddOnKubeClientRegistrationAuth is the authentication type for add-on registration (csr or token)
|
||||||
|
AddOnKubeClientRegistrationAuth string
|
||||||
|
// AddOnTokenExpirationSeconds is the expiration seconds for add-on tokens
|
||||||
|
AddOnTokenExpirationSeconds int64
|
||||||
|
|
||||||
ManagedClusterArn string
|
ManagedClusterArn string
|
||||||
ManagedClusterRoleArn string
|
ManagedClusterRoleArn string
|
||||||
ManagedClusterRoleSuffix string
|
ManagedClusterRoleSuffix string
|
||||||
@@ -390,6 +395,19 @@ func (n *klusterletController) sync(ctx context.Context, controllerContext facto
|
|||||||
annotationsArray = append(annotationsArray, fmt.Sprintf("%s=%s", k, v))
|
annotationsArray = append(annotationsArray, fmt.Sprintf("%s=%s", k, v))
|
||||||
}
|
}
|
||||||
config.ClusterAnnotationsString = strings.Join(annotationsArray, ",")
|
config.ClusterAnnotationsString = strings.Join(annotationsArray, ",")
|
||||||
|
|
||||||
|
// Set AddOnKubeClientRegistrationAuth from the Klusterlet spec
|
||||||
|
if klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver != nil &&
|
||||||
|
klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver.AuthType != "" {
|
||||||
|
config.AddOnKubeClientRegistrationAuth = klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver.AuthType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set AddOnTokenExpirationSeconds from the Klusterlet spec
|
||||||
|
if klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver != nil &&
|
||||||
|
klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver.Token != nil &&
|
||||||
|
klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver.Token.ExpirationSeconds > 0 {
|
||||||
|
config.AddOnTokenExpirationSeconds = klusterlet.Spec.RegistrationConfiguration.AddOnKubeClientRegistrationDriver.Token.ExpirationSeconds
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.AboutAPIEnabled = helpers.FeatureGateEnabled(
|
config.AboutAPIEnabled = helpers.FeatureGateEnabled(
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ package aws_irsa
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/informers"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
||||||
operatorv1 "open-cluster-management.io/api/operator/v1"
|
operatorv1 "open-cluster-management.io/api/operator/v1"
|
||||||
@@ -16,6 +20,8 @@ import (
|
|||||||
|
|
||||||
"open-cluster-management.io/ocm/pkg/common/helpers"
|
"open-cluster-management.io/ocm/pkg/common/helpers"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register"
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
//TODO: Remove these constants in once we have the function fully implemented for the AWSIRSADriver
|
//TODO: Remove these constants in once we have the function fully implemented for the AWSIRSADriver
|
||||||
@@ -36,6 +42,15 @@ type AWSIRSADriver struct {
|
|||||||
managedClusterRoleSuffix string
|
managedClusterRoleSuffix string
|
||||||
|
|
||||||
awsIRSAControl AWSIRSAControl
|
awsIRSAControl AWSIRSAControl
|
||||||
|
|
||||||
|
// addonClients holds the addon clients and informers
|
||||||
|
addonClients *register.AddOnClients
|
||||||
|
|
||||||
|
// tokenControl is used for token-based addon authentication
|
||||||
|
tokenControl token.TokenControl
|
||||||
|
|
||||||
|
// csrControl is used for CSR-based addon authentication
|
||||||
|
csrControl csr.CSRControl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AWSIRSADriver) Process(
|
func (c *AWSIRSADriver) Process(
|
||||||
@@ -98,7 +113,7 @@ func (c *AWSIRSADriver) ManagedClusterDecorator(cluster *clusterv1.ManagedCluste
|
|||||||
return cluster
|
return cluster
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AWSIRSADriver) BuildClients(_ context.Context, secretOption register.SecretOption, bootstrap bool) (*register.Clients, error) {
|
func (c *AWSIRSADriver) BuildClients(ctx context.Context, secretOption register.SecretOption, bootstrap bool) (*register.Clients, error) {
|
||||||
clients, err := register.BuildClientsFromSecretOption(secretOption, bootstrap)
|
clients, err := register.BuildClientsFromSecretOption(secretOption, bootstrap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -107,9 +122,68 @@ func (c *AWSIRSADriver) BuildClients(_ context.Context, secretOption register.Se
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create AWS IRSA control: %w", err)
|
return nil, fmt.Errorf("failed to create AWS IRSA control: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store addon clients and initialize controls for addon authentication after bootstrap
|
||||||
|
if !bootstrap {
|
||||||
|
c.addonClients = ®ister.AddOnClients{
|
||||||
|
AddonClient: clients.AddonClient,
|
||||||
|
AddonInformer: clients.AddonInformer,
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeConfig, err := register.KubeConfigFromSecretOption(secretOption, bootstrap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
kubeClient, err := kubernetes.NewForConfig(kubeConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.tokenControl = token.NewTokenControl(kubeClient.CoreV1())
|
||||||
|
|
||||||
|
// Initialize CSR control for CSR-based addon authentication
|
||||||
|
logger := klog.FromContext(ctx)
|
||||||
|
kubeInformerFactory := informers.NewSharedInformerFactoryWithOptions(
|
||||||
|
kubeClient,
|
||||||
|
10*time.Minute,
|
||||||
|
informers.WithTweakListOptions(func(listOptions *metav1.ListOptions) {
|
||||||
|
listOptions.LabelSelector = fmt.Sprintf("%s=%s", clusterv1.ClusterNameLabelKey, secretOption.ClusterName)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
csrControl, err := csr.NewCSRControl(logger, kubeInformerFactory.Certificates(), kubeClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create CSR control: %w", err)
|
||||||
|
}
|
||||||
|
c.csrControl = csrControl
|
||||||
|
}
|
||||||
|
|
||||||
return clients, nil
|
return clients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AWSIRSADriver) Fork(addonName string, authConfig register.AddonAuthConfig, secretOption register.SecretOption) (register.RegisterDriver, error) {
|
||||||
|
// Check if token-based authentication should be used (shared helper)
|
||||||
|
tokenDriver, err := token.TryForkTokenDriver(addonName, authConfig, secretOption, c.tokenControl, c.addonClients)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tokenDriver != nil {
|
||||||
|
return tokenDriver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For CSR driver, create a CSR-based driver for addon authentication
|
||||||
|
// This handles:
|
||||||
|
// - CustomSigner type (secretOption.Signer != KubeAPIServerClientSignerName)
|
||||||
|
// - KubeClient type with CSR authentication
|
||||||
|
|
||||||
|
// Get CSR configuration from AddonAuthConfig (type-safe interface)
|
||||||
|
csrConfig := authConfig.GetCSRConfiguration()
|
||||||
|
if csrConfig == nil {
|
||||||
|
return nil, fmt.Errorf("CSR configuration is nil for addon %s", addonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: tokenControl is not set for addon CSR drivers since they use CSR-based auth
|
||||||
|
return csr.NewCSRDriverForAddOn(addonName, csrConfig, secretOption, c.csrControl), nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewAWSIRSADriver(opt *AWSOption, secretOption register.SecretOption) register.RegisterDriver {
|
func NewAWSIRSADriver(opt *AWSOption, secretOption register.SecretOption) register.RegisterDriver {
|
||||||
return &AWSIRSADriver{
|
return &AWSIRSADriver{
|
||||||
managedClusterArn: opt.ManagedClusterArn,
|
managedClusterArn: opt.ManagedClusterArn,
|
||||||
@@ -118,3 +192,6 @@ func NewAWSIRSADriver(opt *AWSOption, secretOption register.SecretOption) regist
|
|||||||
name: secretOption.ClusterName,
|
name: secretOption.ClusterName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ register.RegisterDriver = &AWSIRSADriver{}
|
||||||
|
var _ register.AddonDriverFactory = &AWSIRSADriver{}
|
||||||
|
|||||||
@@ -4,14 +4,25 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
certificatesv1 "k8s.io/api/certificates/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
clienttesting "k8s.io/client-go/testing"
|
clienttesting "k8s.io/client-go/testing"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
|
||||||
|
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
||||||
|
addonfake "open-cluster-management.io/api/client/addon/clientset/versioned/fake"
|
||||||
|
addoninformers "open-cluster-management.io/api/client/addon/informers/externalversions"
|
||||||
|
"open-cluster-management.io/sdk-go/pkg/basecontroller/events"
|
||||||
|
|
||||||
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
|
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register"
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
||||||
|
registertesting "open-cluster-management.io/ocm/pkg/registration/register/testing"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ AWSIRSAControl = &mockAWSIRSAControl{}
|
var _ AWSIRSAControl = &mockAWSIRSAControl{}
|
||||||
@@ -202,3 +213,266 @@ func TestIsHubKubeConfigValidFunc(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAWSIRSADriver_Fork_TokenAuth(t *testing.T) {
|
||||||
|
// Setup addon client and informer
|
||||||
|
addon := &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "addon1",
|
||||||
|
Namespace: "cluster1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addonClient := addonfake.NewSimpleClientset(addon)
|
||||||
|
addonInformerFactory := addoninformers.NewSharedInformerFactory(addonClient, 10*time.Minute)
|
||||||
|
addonInformer := addonInformerFactory.Addon().V1alpha1().ManagedClusterAddOns()
|
||||||
|
|
||||||
|
addonClients := ®ister.AddOnClients{
|
||||||
|
AddonClient: addonClient,
|
||||||
|
AddonInformer: addonInformer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup mock AWS IRSA control
|
||||||
|
mockAWSIRSACtrl := &mockAWSIRSAControl{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupDriver func() *AWSIRSADriver
|
||||||
|
addonName string
|
||||||
|
secretOption register.SecretOption
|
||||||
|
regOption register.AddonAuthConfig
|
||||||
|
expectErr bool
|
||||||
|
expectErrMsg string
|
||||||
|
validateResult func(t *testing.T, driver register.RegisterDriver)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "token auth - success",
|
||||||
|
setupDriver: func() *AWSIRSADriver {
|
||||||
|
driver := &AWSIRSADriver{
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: addonClients,
|
||||||
|
awsIRSAControl: mockAWSIRSACtrl,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificatesv1.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validateResult: func(t *testing.T, driver register.RegisterDriver) {
|
||||||
|
if _, ok := driver.(*token.TokenDriver); !ok {
|
||||||
|
t.Errorf("expected TokenDriver, got %T", driver)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - invalid token option",
|
||||||
|
setupDriver: func() *AWSIRSADriver {
|
||||||
|
driver := &AWSIRSADriver{
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: addonClients,
|
||||||
|
awsIRSAControl: mockAWSIRSACtrl,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificatesv1.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: nil,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "token authentication requested but TokenConfiguration is nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - tokenControl not initialized",
|
||||||
|
setupDriver: func() *AWSIRSADriver {
|
||||||
|
driver := &AWSIRSADriver{
|
||||||
|
tokenControl: nil,
|
||||||
|
addonClients: addonClients,
|
||||||
|
awsIRSAControl: mockAWSIRSACtrl,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificatesv1.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "tokenControl is not initialized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - addonClients not initialized",
|
||||||
|
setupDriver: func() *AWSIRSADriver {
|
||||||
|
driver := &AWSIRSADriver{
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: nil,
|
||||||
|
awsIRSAControl: mockAWSIRSACtrl,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificatesv1.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "addonClients is not initialized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "csr auth with KubeAPIServerClientSigner - success",
|
||||||
|
setupDriver: func() *AWSIRSADriver {
|
||||||
|
driver := &AWSIRSADriver{
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
csrControl: newMockCSRControl(),
|
||||||
|
addonClients: addonClients,
|
||||||
|
awsIRSAControl: mockAWSIRSACtrl,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificatesv1.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "csr",
|
||||||
|
CSROption: csr.NewCSROption(),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validateResult: func(t *testing.T, driver register.RegisterDriver) {
|
||||||
|
if _, ok := driver.(*csr.CSRDriver); !ok {
|
||||||
|
t.Errorf("expected CSRDriver, got %T", driver)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "csr auth with custom signer - success",
|
||||||
|
setupDriver: func() *AWSIRSADriver {
|
||||||
|
driver := &AWSIRSADriver{
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
csrControl: newMockCSRControl(),
|
||||||
|
addonClients: addonClients,
|
||||||
|
awsIRSAControl: mockAWSIRSACtrl,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: "custom.signer.io/custom",
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "csr",
|
||||||
|
CSROption: csr.NewCSROption(),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validateResult: func(t *testing.T, driver register.RegisterDriver) {
|
||||||
|
if _, ok := driver.(*csr.CSRDriver); !ok {
|
||||||
|
t.Errorf("expected CSRDriver, got %T", driver)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "csr auth - invalid CSR option",
|
||||||
|
setupDriver: func() *AWSIRSADriver {
|
||||||
|
driver := &AWSIRSADriver{
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
csrControl: newMockCSRControl(),
|
||||||
|
addonClients: addonClients,
|
||||||
|
awsIRSAControl: mockAWSIRSACtrl,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificatesv1.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "csr",
|
||||||
|
CSROption: nil,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "CSR configuration is nil",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
driver := tt.setupDriver()
|
||||||
|
|
||||||
|
forkedDriver, err := driver.Fork(tt.addonName, tt.regOption, tt.secretOption)
|
||||||
|
|
||||||
|
if tt.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error but got nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.expectErrMsg != "" && !strings.Contains(err.Error(), tt.expectErrMsg) {
|
||||||
|
t.Errorf("expected error message to contain %q, got %q", tt.expectErrMsg, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.validateResult != nil {
|
||||||
|
tt.validateResult(t, forkedDriver)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockCSRControl is a simple mock for testing CSR-based registration
|
||||||
|
type mockCSRControl struct {
|
||||||
|
informer cache.SharedIndexInformer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCSRControl) Create(ctx context.Context, recorder events.Recorder, objMeta metav1.ObjectMeta, csrData []byte, signerName string, expirationSeconds *int32) (string, error) {
|
||||||
|
return "mock-csr", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCSRControl) IsApproved(name string) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCSRControl) GetIssuedCertificate(name string) ([]byte, error) {
|
||||||
|
return []byte("mock-cert"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockCSRControl) Informer() cache.SharedIndexInformer {
|
||||||
|
return m.informer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockCSRControl() *mockCSRControl {
|
||||||
|
// Create a fake client and informer
|
||||||
|
return &mockCSRControl{
|
||||||
|
informer: cache.NewSharedIndexInformer(
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
0,
|
||||||
|
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -254,6 +254,12 @@ type Clients struct {
|
|||||||
AddonInformer addonv1alpha1informers.ManagedClusterAddOnInformer
|
AddonInformer addonv1alpha1informers.ManagedClusterAddOnInformer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddOnClients hold clients and informers used by addon driver
|
||||||
|
type AddOnClients struct {
|
||||||
|
AddonClient addonclient.Interface
|
||||||
|
AddonInformer addonv1alpha1informers.ManagedClusterAddOnInformer
|
||||||
|
}
|
||||||
|
|
||||||
func KubeConfigFromSecretOption(s SecretOption, bootstrap bool) (*rest.Config, error) {
|
func KubeConfigFromSecretOption(s SecretOption, bootstrap bool) (*rest.Config, error) {
|
||||||
var kubeConfig *rest.Config
|
var kubeConfig *rest.Config
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import (
|
|||||||
|
|
||||||
"open-cluster-management.io/ocm/pkg/registration/hub/user"
|
"open-cluster-management.io/ocm/pkg/registration/hub/user"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register"
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -67,10 +68,16 @@ type CSRDriver struct {
|
|||||||
|
|
||||||
csrControl CSRControl
|
csrControl CSRControl
|
||||||
|
|
||||||
|
// addonClients holds the addon clients and informers (for addon driver only)
|
||||||
|
addonClients *register.AddOnClients
|
||||||
|
|
||||||
|
// tokenControl is used for token-based addon authentication
|
||||||
|
tokenControl token.TokenControl
|
||||||
|
|
||||||
// HaltCSRCreation halt the csr creation
|
// HaltCSRCreation halt the csr creation
|
||||||
haltCSRCreation func() bool
|
haltCSRCreation func() bool
|
||||||
|
|
||||||
opt *Option
|
opt register.CSRConfiguration
|
||||||
|
|
||||||
csrOption *CSROption
|
csrOption *CSROption
|
||||||
}
|
}
|
||||||
@@ -213,7 +220,7 @@ func (c *CSRDriver) Process(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not set expiration second if it is 0
|
// do not set expiration second if it is 0
|
||||||
expirationSeconds := pointer.Int32(c.opt.ExpirationSeconds)
|
expirationSeconds := pointer.Int32(c.opt.GetExpirationSeconds())
|
||||||
if *expirationSeconds == 0 {
|
if *expirationSeconds == 0 {
|
||||||
expirationSeconds = nil
|
expirationSeconds = nil
|
||||||
}
|
}
|
||||||
@@ -295,30 +302,28 @@ func (c *CSRDriver) ManagedClusterDecorator(cluster *clusterv1.ManagedCluster) *
|
|||||||
return cluster
|
return cluster
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CSRDriver) Fork(addonName string, secretOption register.SecretOption) register.RegisterDriver {
|
func (c *CSRDriver) Fork(addonName string, authConfig register.AddonAuthConfig, secretOption register.SecretOption) (register.RegisterDriver, error) {
|
||||||
csrOption := &CSROption{
|
// Check if token-based authentication should be used (shared helper)
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
tokenDriver, err := token.TryForkTokenDriver(addonName, authConfig, secretOption, c.tokenControl, c.addonClients)
|
||||||
GenerateName: fmt.Sprintf("addon-%s-%s-", secretOption.ClusterName, addonName),
|
if err != nil {
|
||||||
Labels: map[string]string{
|
return nil, err
|
||||||
// the labels are only hints. Anyone could set/modify them.
|
}
|
||||||
clusterv1.ClusterNameLabelKey: secretOption.ClusterName,
|
if tokenDriver != nil {
|
||||||
addonv1alpha1.AddonLabelKey: addonName,
|
return tokenDriver, nil
|
||||||
},
|
|
||||||
},
|
|
||||||
Subject: secretOption.Subject,
|
|
||||||
DNSNames: []string{fmt.Sprintf("%s.addon.open-cluster-management.io", addonName)},
|
|
||||||
SignerName: secretOption.Signer,
|
|
||||||
EventFilterFunc: createCSREventFilterFunc(secretOption.ClusterName, addonName, secretOption.Signer),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
driver := &CSRDriver{
|
// For CSR driver, return a CSR-based driver
|
||||||
csrOption: csrOption,
|
// This handles:
|
||||||
opt: c.opt,
|
// - CustomSigner type (secretOption.Signer != KubeAPIServerClientSignerName)
|
||||||
csrControl: c.csrControl,
|
// - KubeClient type with CSR authentication
|
||||||
haltCSRCreation: haltAddonCSRCreationFunc(c.csrControl.Informer().GetIndexer(), secretOption.ClusterName, addonName),
|
|
||||||
|
// Get CSR configuration from AddonAuthConfig (type-safe interface)
|
||||||
|
csrConfig := authConfig.GetCSRConfiguration()
|
||||||
|
if csrConfig == nil {
|
||||||
|
return nil, fmt.Errorf("CSR configuration is nil for addon %s", addonName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return driver
|
return NewCSRDriverForAddOn(addonName, csrConfig, secretOption, c.csrControl), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CSRDriver) BuildClients(ctx context.Context, secretOption register.SecretOption, bootstrap bool) (*register.Clients, error) {
|
func (c *CSRDriver) BuildClients(ctx context.Context, secretOption register.SecretOption, bootstrap bool) (*register.Clients, error) {
|
||||||
@@ -353,6 +358,16 @@ func (c *CSRDriver) BuildClients(ctx context.Context, secretOption register.Secr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize addon clients and token control for addon mode after bootstrap
|
||||||
|
if !bootstrap {
|
||||||
|
c.addonClients = ®ister.AddOnClients{
|
||||||
|
AddonClient: clients.AddonClient,
|
||||||
|
AddonInformer: clients.AddonInformer,
|
||||||
|
}
|
||||||
|
c.tokenControl = token.NewTokenControl(kubeClient.CoreV1())
|
||||||
|
}
|
||||||
|
|
||||||
return clients, nil
|
return clients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,10 +385,45 @@ func (c *CSRDriver) SetCSRControl(csrControl CSRControl, clusterName string) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ register.RegisterDriver = &CSRDriver{}
|
// SetAddonClients sets the addon clients for the CSR driver
|
||||||
var _ register.AddonDriver = &CSRDriver{}
|
func (c *CSRDriver) SetAddonClients(addonClients *register.AddOnClients) {
|
||||||
|
c.addonClients = addonClients
|
||||||
|
}
|
||||||
|
|
||||||
func NewCSRDriver(opt *Option, secretOpts register.SecretOption) (*CSRDriver, error) {
|
// SetTokenControl sets the token control for the CSR driver
|
||||||
|
func (c *CSRDriver) SetTokenControl(tokenControl token.TokenControl) {
|
||||||
|
c.tokenControl = tokenControl
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ register.RegisterDriver = &CSRDriver{}
|
||||||
|
var _ register.AddonDriverFactory = &CSRDriver{}
|
||||||
|
|
||||||
|
// NewCSRDriverForAddOn creates a CSRDriver for addon registration with the given parameters
|
||||||
|
func NewCSRDriverForAddOn(addonName string, csrConfig register.CSRConfiguration, secretOption register.SecretOption, csrControl CSRControl) *CSRDriver {
|
||||||
|
csrOption := &CSROption{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
GenerateName: fmt.Sprintf("addon-%s-%s-", secretOption.ClusterName, addonName),
|
||||||
|
Labels: map[string]string{
|
||||||
|
// the labels are only hints. Anyone could set/modify them.
|
||||||
|
clusterv1.ClusterNameLabelKey: secretOption.ClusterName,
|
||||||
|
addonv1alpha1.AddonLabelKey: addonName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subject: secretOption.Subject,
|
||||||
|
DNSNames: []string{fmt.Sprintf("%s.addon.open-cluster-management.io", addonName)},
|
||||||
|
SignerName: secretOption.Signer,
|
||||||
|
EventFilterFunc: createCSREventFilterFunc(secretOption.ClusterName, addonName, secretOption.Signer),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CSRDriver{
|
||||||
|
csrOption: csrOption,
|
||||||
|
opt: csrConfig,
|
||||||
|
csrControl: csrControl,
|
||||||
|
haltCSRCreation: haltAddonCSRCreationFunc(csrControl.Informer().GetIndexer(), secretOption.ClusterName, addonName),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCSRDriver(csrConfig register.CSRConfiguration, secretOpts register.SecretOption) (*CSRDriver, error) {
|
||||||
signer := certificates.KubeAPIServerClientSignerName
|
signer := certificates.KubeAPIServerClientSignerName
|
||||||
if secretOpts.Signer != "" {
|
if secretOpts.Signer != "" {
|
||||||
signer = secretOpts.Signer
|
signer = secretOpts.Signer
|
||||||
@@ -385,7 +435,7 @@ func NewCSRDriver(opt *Option, secretOpts register.SecretOption) (*CSRDriver, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
driver := &CSRDriver{
|
driver := &CSRDriver{
|
||||||
opt: opt,
|
opt: csrConfig,
|
||||||
}
|
}
|
||||||
driver.csrOption = &CSROption{
|
driver.csrOption = &CSROption{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ import (
|
|||||||
"k8s.io/klog/v2/ktesting"
|
"k8s.io/klog/v2/ktesting"
|
||||||
|
|
||||||
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
||||||
|
addonfake "open-cluster-management.io/api/client/addon/clientset/versioned/fake"
|
||||||
|
addoninformers "open-cluster-management.io/api/client/addon/informers/externalversions"
|
||||||
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
||||||
ocmfeature "open-cluster-management.io/api/feature"
|
ocmfeature "open-cluster-management.io/api/feature"
|
||||||
"open-cluster-management.io/sdk-go/pkg/basecontroller/events"
|
"open-cluster-management.io/sdk-go/pkg/basecontroller/events"
|
||||||
@@ -32,6 +35,8 @@ import (
|
|||||||
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
|
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/hub/user"
|
"open-cluster-management.io/ocm/pkg/registration/hub/user"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register"
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
registertesting "open-cluster-management.io/ocm/pkg/registration/register/testing"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -623,8 +628,15 @@ func TestNewCSRDriver(t *testing.T) {
|
|||||||
CommonName: "addonagent1",
|
CommonName: "addonagent1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
addonDriver := driver.Fork("addon1", addonSecretOptions)
|
regOption := ®istertesting.TestAddonAuthConfig{
|
||||||
csrAddonDriver := addonDriver.(*CSRDriver)
|
KubeClientAuth: "csr",
|
||||||
|
CSROption: NewCSROption(),
|
||||||
|
}
|
||||||
|
forkedDriver, err := driver.Fork("addon1", regOption, addonSecretOptions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
csrAddonDriver := forkedDriver.(*CSRDriver)
|
||||||
if csrAddonDriver.csrOption.Subject.CommonName != "addonagent1" {
|
if csrAddonDriver.csrOption.Subject.CommonName != "addonagent1" {
|
||||||
t.Errorf("common name is not set correctly")
|
t.Errorf("common name is not set correctly")
|
||||||
}
|
}
|
||||||
@@ -784,3 +796,244 @@ func TestBuildClient(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCSRDriver_Fork_TokenAuth(t *testing.T) {
|
||||||
|
// Setup addon client and informer
|
||||||
|
addon := &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "addon1",
|
||||||
|
Namespace: "cluster1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addonClient := addonfake.NewSimpleClientset(addon)
|
||||||
|
addonInformerFactory := addoninformers.NewSharedInformerFactory(addonClient, 10*time.Minute)
|
||||||
|
addonInformer := addonInformerFactory.Addon().V1alpha1().ManagedClusterAddOns()
|
||||||
|
|
||||||
|
ctrl := &mockCSRControl{}
|
||||||
|
hubKubeClient := kubefake.NewClientset()
|
||||||
|
ctrl.csrClient = &hubKubeClient.Fake
|
||||||
|
|
||||||
|
addonClients := ®ister.AddOnClients{
|
||||||
|
AddonClient: addonClient,
|
||||||
|
AddonInformer: addonInformer,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupDriver func() *CSRDriver
|
||||||
|
addonName string
|
||||||
|
secretOption register.SecretOption
|
||||||
|
regOption register.AddonAuthConfig
|
||||||
|
expectErr bool
|
||||||
|
expectErrMsg string
|
||||||
|
validateResult func(t *testing.T, driver register.RegisterDriver)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "token auth - success",
|
||||||
|
setupDriver: func() *CSRDriver {
|
||||||
|
driver := &CSRDriver{
|
||||||
|
csrControl: ctrl,
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: addonClients,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validateResult: func(t *testing.T, driver register.RegisterDriver) {
|
||||||
|
if _, ok := driver.(*token.TokenDriver); !ok {
|
||||||
|
t.Errorf("expected TokenDriver, got %T", driver)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - invalid token option",
|
||||||
|
setupDriver: func() *CSRDriver {
|
||||||
|
driver := &CSRDriver{
|
||||||
|
csrControl: ctrl,
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: addonClients,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: nil,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "token authentication requested but TokenConfiguration is nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - tokenControl not initialized",
|
||||||
|
setupDriver: func() *CSRDriver {
|
||||||
|
driver := &CSRDriver{
|
||||||
|
csrControl: ctrl,
|
||||||
|
tokenControl: nil,
|
||||||
|
addonClients: addonClients,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "tokenControl is not initialized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - addonClients not initialized",
|
||||||
|
setupDriver: func() *CSRDriver {
|
||||||
|
driver := &CSRDriver{
|
||||||
|
csrControl: ctrl,
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: nil,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "addonClients is not initialized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "csr auth with custom signer - success",
|
||||||
|
setupDriver: func() *CSRDriver {
|
||||||
|
driver := &CSRDriver{
|
||||||
|
csrControl: ctrl,
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: addonClients,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: "custom.signer.io/custom",
|
||||||
|
Subject: &pkix.Name{
|
||||||
|
CommonName: "custom-addon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "csr",
|
||||||
|
CSROption: NewCSROption(),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validateResult: func(t *testing.T, driver register.RegisterDriver) {
|
||||||
|
if _, ok := driver.(*CSRDriver); !ok {
|
||||||
|
t.Errorf("expected CSRDriver, got %T", driver)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "csr auth - invalid CSR option",
|
||||||
|
setupDriver: func() *CSRDriver {
|
||||||
|
driver := &CSRDriver{
|
||||||
|
csrControl: ctrl,
|
||||||
|
tokenControl: ®istertesting.MockTokenControl{},
|
||||||
|
addonClients: addonClients,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "csr",
|
||||||
|
CSROption: nil,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "CSR configuration is nil",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
driver := tt.setupDriver()
|
||||||
|
|
||||||
|
forkedDriver, err := driver.Fork(tt.addonName, tt.regOption, tt.secretOption)
|
||||||
|
|
||||||
|
if tt.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error but got nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.expectErrMsg != "" && !strings.Contains(err.Error(), tt.expectErrMsg) {
|
||||||
|
t.Errorf("expected error message to contain %q, got %q", tt.expectErrMsg, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.validateResult != nil {
|
||||||
|
tt.validateResult(t, forkedDriver)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRDriver_SetTokenControl(t *testing.T) {
|
||||||
|
driver := &CSRDriver{}
|
||||||
|
mockControl := ®istertesting.MockTokenControl{}
|
||||||
|
|
||||||
|
driver.SetTokenControl(mockControl)
|
||||||
|
|
||||||
|
if driver.tokenControl != mockControl {
|
||||||
|
t.Error("SetTokenControl did not set tokenControl correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSRDriver_SetAddonClients(t *testing.T) {
|
||||||
|
driver := &CSRDriver{}
|
||||||
|
|
||||||
|
addon := &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "addon1",
|
||||||
|
Namespace: "cluster1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addonClient := addonfake.NewSimpleClientset(addon)
|
||||||
|
addonInformerFactory := addoninformers.NewSharedInformerFactory(addonClient, 10*time.Minute)
|
||||||
|
addonInformer := addonInformerFactory.Addon().V1alpha1().ManagedClusterAddOns()
|
||||||
|
|
||||||
|
addonClients := ®ister.AddOnClients{
|
||||||
|
AddonClient: addonClient,
|
||||||
|
AddonInformer: addonInformer,
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.SetAddonClients(addonClients)
|
||||||
|
|
||||||
|
if driver.addonClients != addonClients {
|
||||||
|
t.Error("SetAddonClients did not set addonClients correctly")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
|
||||||
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
|
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
|
||||||
|
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSROption includes options that is used to create and monitor csrs
|
// CSROption includes options that is used to create and monitor csrs
|
||||||
@@ -43,6 +45,9 @@ type Option struct {
|
|||||||
ExpirationSeconds int32
|
ExpirationSeconds int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure Option implements register.CSRConfiguration interface at compile time
|
||||||
|
var _ register.CSRConfiguration = &Option{}
|
||||||
|
|
||||||
func NewCSROption() *Option {
|
func NewCSROption() *Option {
|
||||||
return &Option{}
|
return &Option{}
|
||||||
}
|
}
|
||||||
@@ -60,6 +65,10 @@ func (o *Option) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Option) GetExpirationSeconds() int32 {
|
||||||
|
return o.ExpirationSeconds
|
||||||
|
}
|
||||||
|
|
||||||
func haltCSRCreationFunc(indexer cache.Indexer, clusterName string) func() bool {
|
func haltCSRCreationFunc(indexer cache.Indexer, clusterName string) func() bool {
|
||||||
return func() bool {
|
return func() bool {
|
||||||
items, err := indexer.ByIndex(indexByCluster, clusterName)
|
items, err := indexer.ByIndex(indexByCluster, clusterName)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package factory
|
package factory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
operatorv1 "open-cluster-management.io/api/operator/v1"
|
operatorv1 "open-cluster-management.io/api/operator/v1"
|
||||||
@@ -9,6 +11,7 @@ import (
|
|||||||
awsirsa "open-cluster-management.io/ocm/pkg/registration/register/aws_irsa"
|
awsirsa "open-cluster-management.io/ocm/pkg/registration/register/aws_irsa"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register/grpc"
|
"open-cluster-management.io/ocm/pkg/registration/register/grpc"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -16,25 +19,42 @@ type Options struct {
|
|||||||
CSROption *csr.Option
|
CSROption *csr.Option
|
||||||
AWSIRSAOption *awsirsa.AWSOption
|
AWSIRSAOption *awsirsa.AWSOption
|
||||||
GRPCOption *grpc.Option
|
GRPCOption *grpc.Option
|
||||||
|
TokenOption *token.Option
|
||||||
|
|
||||||
|
// AddonKubeClientRegistrationAuth specifies the authentication method for addons
|
||||||
|
// with registration type KubeClient. Possible values are "csr" (default) and "token".
|
||||||
|
AddonKubeClientRegistrationAuth string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOptions() *Options {
|
func NewOptions() *Options {
|
||||||
return &Options{
|
return &Options{
|
||||||
CSROption: csr.NewCSROption(),
|
CSROption: csr.NewCSROption(),
|
||||||
AWSIRSAOption: awsirsa.NewAWSOption(),
|
AWSIRSAOption: awsirsa.NewAWSOption(),
|
||||||
GRPCOption: grpc.NewOptions(),
|
GRPCOption: grpc.NewOptions(),
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
AddonKubeClientRegistrationAuth: "csr", // default to csr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Options) AddFlags(fs *pflag.FlagSet) {
|
func (s *Options) AddFlags(fs *pflag.FlagSet) {
|
||||||
fs.StringVar(&s.RegistrationAuth, "registration-auth", s.RegistrationAuth,
|
fs.StringVar(&s.RegistrationAuth, "registration-auth", s.RegistrationAuth,
|
||||||
"The type of authentication to use to authenticate with hub.")
|
"The type of authentication to use to authenticate with hub.")
|
||||||
|
fs.StringVar(&s.AddonKubeClientRegistrationAuth, "addon-kubeclient-registration-auth", s.AddonKubeClientRegistrationAuth,
|
||||||
|
"The authentication method for addons with registration type KubeClient. Possible values are 'csr' (default) and 'token'.")
|
||||||
s.CSROption.AddFlags(fs)
|
s.CSROption.AddFlags(fs)
|
||||||
s.AWSIRSAOption.AddFlags(fs)
|
s.AWSIRSAOption.AddFlags(fs)
|
||||||
s.GRPCOption.AddFlags(fs)
|
s.GRPCOption.AddFlags(fs)
|
||||||
|
s.TokenOption.AddFlags(fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Options) Validate() error {
|
func (s *Options) Validate() error {
|
||||||
|
switch s.AddonKubeClientRegistrationAuth {
|
||||||
|
case "", "csr", "token":
|
||||||
|
// valid values
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported addon-kubeclient-registration-auth: %s", s.AddonKubeClientRegistrationAuth)
|
||||||
|
}
|
||||||
|
|
||||||
switch s.RegistrationAuth {
|
switch s.RegistrationAuth {
|
||||||
case operatorv1.AwsIrsaAuthType:
|
case operatorv1.AwsIrsaAuthType:
|
||||||
return s.AWSIRSAOption.Validate()
|
return s.AWSIRSAOption.Validate()
|
||||||
@@ -45,6 +65,18 @@ func (s *Options) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Options) GetKubeClientAuth() string {
|
||||||
|
return s.AddonKubeClientRegistrationAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Options) GetCSRConfiguration() register.CSRConfiguration {
|
||||||
|
return s.CSROption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Options) GetTokenConfiguration() register.TokenConfiguration {
|
||||||
|
return s.TokenOption
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Options) Driver(secretOption register.SecretOption) (register.RegisterDriver, error) {
|
func (s *Options) Driver(secretOption register.SecretOption) (register.RegisterDriver, error) {
|
||||||
switch s.RegistrationAuth {
|
switch s.RegistrationAuth {
|
||||||
case operatorv1.AwsIrsaAuthType:
|
case operatorv1.AwsIrsaAuthType:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
certificatesv1 "k8s.io/api/certificates/v1"
|
certificatesv1 "k8s.io/api/certificates/v1"
|
||||||
coordv1 "k8s.io/api/coordination/v1"
|
coordv1 "k8s.io/api/coordination/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@@ -30,6 +31,7 @@ import (
|
|||||||
cloudeventsevent "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/event"
|
cloudeventsevent "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/event"
|
||||||
cloudeventslease "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/lease"
|
cloudeventslease "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/lease"
|
||||||
cloudeventsoptions "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/options"
|
cloudeventsoptions "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/options"
|
||||||
|
cloudeventsserviceaccount "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/serviceaccount"
|
||||||
cloudeventsstore "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/store"
|
cloudeventsstore "open-cluster-management.io/sdk-go/pkg/cloudevents/clients/store"
|
||||||
"open-cluster-management.io/sdk-go/pkg/cloudevents/constants"
|
"open-cluster-management.io/sdk-go/pkg/cloudevents/constants"
|
||||||
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options/builder"
|
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options/builder"
|
||||||
@@ -38,6 +40,7 @@ import (
|
|||||||
|
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register"
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GRPCDriver struct {
|
type GRPCDriver struct {
|
||||||
@@ -45,10 +48,12 @@ type GRPCDriver struct {
|
|||||||
control *ceCSRControl
|
control *ceCSRControl
|
||||||
opt *Option
|
opt *Option
|
||||||
configTemplate []byte
|
configTemplate []byte
|
||||||
|
addonClients *register.AddOnClients
|
||||||
|
tokenControl token.TokenControl
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ register.RegisterDriver = &GRPCDriver{}
|
var _ register.RegisterDriver = &GRPCDriver{}
|
||||||
var _ register.AddonDriver = &GRPCDriver{}
|
var _ register.AddonDriverFactory = &GRPCDriver{}
|
||||||
|
|
||||||
func NewGRPCDriver(opt *Option, csrOption *csr.Option, secretOption register.SecretOption) (register.RegisterDriver, error) {
|
func NewGRPCDriver(opt *Option, csrOption *csr.Option, secretOption register.SecretOption) (register.RegisterDriver, error) {
|
||||||
secretOption.Signer = operatorv1.GRPCAuthSigner
|
secretOption.Signer = operatorv1.GRPCAuthSigner
|
||||||
@@ -161,16 +166,34 @@ func (d *GRPCDriver) BuildClients(ctx context.Context, secretOption register.Sec
|
|||||||
LeaseClient: leaseClient,
|
LeaseClient: leaseClient,
|
||||||
EventsClient: eventClient,
|
EventsClient: eventClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize addon clients for addon mode
|
||||||
|
d.addonClients = ®ister.AddOnClients{
|
||||||
|
AddonClient: addonClient,
|
||||||
|
AddonInformer: addonInformer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize gRPC token control for token-based addon authentication
|
||||||
|
grpcOptions, ok := config.(*grpc.GRPCOptions)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid gRPC config type")
|
||||||
|
}
|
||||||
|
saClient := cloudeventsserviceaccount.NewServiceAccountClient(secretOption.ClusterName, grpcOptions)
|
||||||
|
d.tokenControl = &grpcTokenControl{
|
||||||
|
saClient: saClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set addonClients and tokenControl on the embedded csrDriver for forked driver instances
|
||||||
|
d.csrDriver.SetAddonClients(d.addonClients)
|
||||||
|
d.csrDriver.SetTokenControl(d.tokenControl)
|
||||||
|
|
||||||
return clients, nil
|
return clients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *GRPCDriver) Fork(addonName string, secretOption register.SecretOption) register.RegisterDriver {
|
func (d *GRPCDriver) Fork(addonName string, authConfig register.AddonAuthConfig, secretOption register.SecretOption) (register.RegisterDriver, error) {
|
||||||
csrDriver := d.csrDriver.Fork(addonName, secretOption)
|
// Delegate to csrDriver.Fork which handles both token and CSR authentication
|
||||||
return &GRPCDriver{
|
// Return the driver directly (either TokenDriver or CSRDriver) without wrapping
|
||||||
control: d.control,
|
return d.csrDriver.Fork(addonName, authConfig, secretOption)
|
||||||
opt: d.opt,
|
|
||||||
csrDriver: csrDriver.(*csr.CSRDriver),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *GRPCDriver) Process(
|
func (d *GRPCDriver) Process(
|
||||||
@@ -282,6 +305,32 @@ type ceCSRControl struct {
|
|||||||
|
|
||||||
var _ csr.CSRControl = &ceCSRControl{}
|
var _ csr.CSRControl = &ceCSRControl{}
|
||||||
|
|
||||||
|
type grpcTokenControl struct {
|
||||||
|
saClient *cloudeventsserviceaccount.ServiceAccountClient
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ token.TokenControl = &grpcTokenControl{}
|
||||||
|
|
||||||
|
// CreateToken creates a ServiceAccount token using cloud events
|
||||||
|
func (g *grpcTokenControl) CreateToken(ctx context.Context, serviceAccountName, namespace string, expirationSeconds int64) (string, error) {
|
||||||
|
if g.saClient == nil {
|
||||||
|
return "", fmt.Errorf("ServiceAccount client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRequest := &authenticationv1.TokenRequest{
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
ExpirationSeconds: &expirationSeconds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := g.saClient.CreateToken(ctx, serviceAccountName, tokenRequest, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create token for ServiceAccount %s/%s: %w", namespace, serviceAccountName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Status.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ceCSRControl) IsApproved(name string) (bool, error) {
|
func (c *ceCSRControl) IsApproved(name string) (bool, error) {
|
||||||
csr, err := c.csrClientHolder.Clients().Get(context.Background(), name, metav1.GetOptions{})
|
csr, err := c.csrClientHolder.Clients().Get(context.Background(), name, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,14 +4,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
certificates "k8s.io/api/certificates/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
||||||
|
addonfake "open-cluster-management.io/api/client/addon/clientset/versioned/fake"
|
||||||
|
addoninformers "open-cluster-management.io/api/client/addon/informers/externalversions"
|
||||||
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options/grpc"
|
"open-cluster-management.io/sdk-go/pkg/cloudevents/generic/options/grpc"
|
||||||
|
|
||||||
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
|
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register"
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
||||||
|
registertesting "open-cluster-management.io/ocm/pkg/registration/register/testing"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsHubKubeConfigValid(t *testing.T) {
|
func TestIsHubKubeConfigValid(t *testing.T) {
|
||||||
@@ -164,3 +173,267 @@ func TestLoadConfig(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGRPCDriver_Fork_TokenAuth(t *testing.T) {
|
||||||
|
// Setup addon client and informer
|
||||||
|
addon := &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "addon1",
|
||||||
|
Namespace: "cluster1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
addonClient := addonfake.NewSimpleClientset(addon)
|
||||||
|
addonInformerFactory := addoninformers.NewSharedInformerFactory(addonClient, 10*time.Minute)
|
||||||
|
addonInformer := addonInformerFactory.Addon().V1alpha1().ManagedClusterAddOns()
|
||||||
|
|
||||||
|
addonClients := ®ister.AddOnClients{
|
||||||
|
AddonClient: addonClient,
|
||||||
|
AddonInformer: addonInformer,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupDriver func() *GRPCDriver
|
||||||
|
addonName string
|
||||||
|
secretOption register.SecretOption
|
||||||
|
regOption register.AddonAuthConfig
|
||||||
|
expectErr bool
|
||||||
|
expectErrMsg string
|
||||||
|
validateResult func(t *testing.T, driver register.RegisterDriver)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "token auth - success",
|
||||||
|
setupDriver: func() *GRPCDriver {
|
||||||
|
tokenControl := &grpcTokenControl{}
|
||||||
|
csrDriver := &csr.CSRDriver{}
|
||||||
|
csrDriver.SetTokenControl(tokenControl)
|
||||||
|
csrDriver.SetAddonClients(addonClients)
|
||||||
|
driver := &GRPCDriver{
|
||||||
|
tokenControl: tokenControl,
|
||||||
|
addonClients: addonClients,
|
||||||
|
csrDriver: csrDriver,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: false,
|
||||||
|
validateResult: func(t *testing.T, driver register.RegisterDriver) {
|
||||||
|
if _, ok := driver.(*token.TokenDriver); !ok {
|
||||||
|
t.Errorf("expected TokenDriver, got %T", driver)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - invalid token option",
|
||||||
|
setupDriver: func() *GRPCDriver {
|
||||||
|
tokenControl := &grpcTokenControl{}
|
||||||
|
csrDriver := &csr.CSRDriver{}
|
||||||
|
csrDriver.SetTokenControl(tokenControl)
|
||||||
|
csrDriver.SetAddonClients(addonClients)
|
||||||
|
driver := &GRPCDriver{
|
||||||
|
tokenControl: tokenControl,
|
||||||
|
addonClients: addonClients,
|
||||||
|
csrDriver: csrDriver,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: nil,
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "token authentication requested but TokenConfiguration is nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - tokenControl not initialized",
|
||||||
|
setupDriver: func() *GRPCDriver {
|
||||||
|
csrDriver := &csr.CSRDriver{}
|
||||||
|
csrDriver.SetTokenControl(nil)
|
||||||
|
csrDriver.SetAddonClients(addonClients)
|
||||||
|
driver := &GRPCDriver{
|
||||||
|
tokenControl: nil,
|
||||||
|
addonClients: addonClients,
|
||||||
|
csrDriver: csrDriver,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "tokenControl is not initialized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token auth - addonClients not initialized",
|
||||||
|
setupDriver: func() *GRPCDriver {
|
||||||
|
tokenControl := &grpcTokenControl{}
|
||||||
|
csrDriver := &csr.CSRDriver{}
|
||||||
|
csrDriver.SetTokenControl(tokenControl)
|
||||||
|
csrDriver.SetAddonClients(nil)
|
||||||
|
driver := &GRPCDriver{
|
||||||
|
tokenControl: tokenControl,
|
||||||
|
addonClients: nil,
|
||||||
|
csrDriver: csrDriver,
|
||||||
|
}
|
||||||
|
return driver
|
||||||
|
},
|
||||||
|
addonName: "addon1",
|
||||||
|
secretOption: register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
Signer: certificates.KubeAPIServerClientSignerName,
|
||||||
|
},
|
||||||
|
regOption: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "token",
|
||||||
|
TokenOption: token.NewTokenOption(),
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
expectErrMsg: "addonClients is not initialized",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
driver := tt.setupDriver()
|
||||||
|
|
||||||
|
forkedDriver, err := driver.Fork(tt.addonName, tt.regOption, tt.secretOption)
|
||||||
|
|
||||||
|
if tt.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error but got nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tt.expectErrMsg != "" && !strings.Contains(err.Error(), tt.expectErrMsg) {
|
||||||
|
t.Errorf("expected error message to contain %q, got %q", tt.expectErrMsg, err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.validateResult != nil {
|
||||||
|
tt.validateResult(t, forkedDriver)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCDriver_BuildClients_InitializesTokenControl(t *testing.T) {
|
||||||
|
// This test verifies that BuildClients properly initializes tokenControl
|
||||||
|
// We can't fully test BuildClients without a real gRPC server, so we just
|
||||||
|
// verify the structure is correct
|
||||||
|
tempDir, err := os.MkdirTemp("", "grpc-test-build-clients")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
configFile := filepath.Join(tempDir, "config.yaml")
|
||||||
|
if err := os.WriteFile(configFile, []byte(`url: "https://localhost:8443"`), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kubeconfig := testinghelpers.NewKubeconfig("cluster1", "https://127.0.0.1:6001", "", "", nil, nil, nil)
|
||||||
|
kubeconfigFile := filepath.Join(tempDir, "kubeconfig")
|
||||||
|
if err := os.WriteFile(kubeconfigFile, kubeconfig, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretOption := register.SecretOption{
|
||||||
|
ClusterName: "cluster1",
|
||||||
|
AgentName: "agent1",
|
||||||
|
HubKubeconfigFile: kubeconfigFile,
|
||||||
|
BootStrapKubeConfigFile: kubeconfigFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
driverInterface, err := NewGRPCDriver(&Option{
|
||||||
|
ConfigFile: configFile,
|
||||||
|
BootstrapConfigFile: configFile,
|
||||||
|
}, csr.NewCSROption(), secretOption)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create GRPC driver: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type assert to access internal fields for verification
|
||||||
|
driver, ok := driverInterface.(*GRPCDriver)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected *GRPCDriver, got %T", driverInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify driver was created with expected structure
|
||||||
|
if driver.csrDriver == nil {
|
||||||
|
t.Error("csrDriver should be initialized")
|
||||||
|
}
|
||||||
|
if driver.opt == nil {
|
||||||
|
t.Error("opt should be initialized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCTokenControl_CreateToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
saClient interface{}
|
||||||
|
expectErr bool
|
||||||
|
expectErrContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "saClient is nil",
|
||||||
|
saClient: nil,
|
||||||
|
expectErr: true,
|
||||||
|
expectErrContains: "ServiceAccount client is not initialized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "saClient is set but we can't actually call it without a real server",
|
||||||
|
saClient: &struct{}{}, // Mock object, will fail when we try to use it
|
||||||
|
expectErr: false, // We just test that nil check passes
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctrl := &grpcTokenControl{}
|
||||||
|
if tt.saClient != nil {
|
||||||
|
// We can't easily test the actual CreateToken call without a real gRPC server,
|
||||||
|
// so we just verify the nil check works
|
||||||
|
if ctrl.saClient == nil && tt.saClient == nil {
|
||||||
|
_, err := ctrl.CreateToken(context.Background(), "test-sa", "test-ns", 3600)
|
||||||
|
if !tt.expectErr {
|
||||||
|
t.Errorf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
if err != nil && !strings.Contains(err.Error(), tt.expectErrContains) {
|
||||||
|
t.Errorf("expected error to contain %q, got: %v", tt.expectErrContains, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := ctrl.CreateToken(context.Background(), "test-sa", "test-ns", 3600)
|
||||||
|
if !tt.expectErr {
|
||||||
|
t.Errorf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
if err != nil && !strings.Contains(err.Error(), tt.expectErrContains) {
|
||||||
|
t.Errorf("expected error to contain %q, got: %v", tt.expectErrContains, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,18 @@ const (
|
|||||||
KubeconfigFile = "kubeconfig"
|
KubeconfigFile = "kubeconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CSRConfiguration provides configuration for CSR-based authentication.
|
||||||
|
type CSRConfiguration interface {
|
||||||
|
// GetExpirationSeconds returns the requested duration of validity of the issued certificate in seconds
|
||||||
|
GetExpirationSeconds() int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenConfiguration provides configuration for token-based authentication.
|
||||||
|
type TokenConfiguration interface {
|
||||||
|
// GetExpirationSeconds returns the requested duration of validity of the token in seconds
|
||||||
|
GetExpirationSeconds() int64
|
||||||
|
}
|
||||||
|
|
||||||
type SecretOption struct {
|
type SecretOption struct {
|
||||||
// SecretNamespace is the namespace of the secret containing client certificate.
|
// SecretNamespace is the namespace of the secret containing client certificate.
|
||||||
SecretNamespace string
|
SecretNamespace string
|
||||||
@@ -81,9 +93,30 @@ type RegisterDriver interface {
|
|||||||
BuildClients(ctx context.Context, secretOption SecretOption, bootstrap bool) (*Clients, error)
|
BuildClients(ctx context.Context, secretOption SecretOption, bootstrap bool) (*Clients, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddonDriver is an interface for the driver to fork a driver for addons registration
|
// AddonAuthConfig provides complete configuration for addon registration,
|
||||||
type AddonDriver interface {
|
// including authentication method and access to driver options.
|
||||||
Fork(addonName string, secretOption SecretOption) RegisterDriver
|
type AddonAuthConfig interface {
|
||||||
|
// GetKubeClientAuth returns the authentication method for addons with registration type KubeClient.
|
||||||
|
// Possible values are "csr" (default) and "token".
|
||||||
|
GetKubeClientAuth() string
|
||||||
|
|
||||||
|
// GetCSRConfiguration returns the CSR driver configuration interface
|
||||||
|
GetCSRConfiguration() CSRConfiguration
|
||||||
|
|
||||||
|
// GetTokenConfiguration returns the token driver configuration interface
|
||||||
|
GetTokenConfiguration() TokenConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddonDriverFactory is an interface for creating RegisterDriver instances for addon registration.
|
||||||
|
// It acts as a factory that creates (forks) driver instances for specific addons.
|
||||||
|
type AddonDriverFactory interface {
|
||||||
|
// Fork creates a RegisterDriver instance for a specific addon.
|
||||||
|
// Parameters:
|
||||||
|
// - addonName: the name of the addon
|
||||||
|
// - authConfig: authentication configuration including type and authentication method
|
||||||
|
// - secretOption: configuration for the secret that will store credentials
|
||||||
|
// Returns the driver instance and an error if the driver cannot be created
|
||||||
|
Fork(addonName string, authConfig AddonAuthConfig, secretOption SecretOption) (RegisterDriver, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HubDriver interface is used to implement operations required to complete aws-irsa registration and csr registration.
|
// HubDriver interface is used to implement operations required to complete aws-irsa registration and csr registration.
|
||||||
|
|||||||
33
pkg/registration/register/testing/helpers.go
Normal file
33
pkg/registration/register/testing/helpers.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package testing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAddonAuthConfig is a simple implementation of AddonAuthConfig for testing
|
||||||
|
type TestAddonAuthConfig struct {
|
||||||
|
KubeClientAuth string
|
||||||
|
CSROption register.CSRConfiguration
|
||||||
|
TokenOption register.TokenConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TestAddonAuthConfig) GetKubeClientAuth() string {
|
||||||
|
return t.KubeClientAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TestAddonAuthConfig) GetCSRConfiguration() register.CSRConfiguration {
|
||||||
|
return t.CSROption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TestAddonAuthConfig) GetTokenConfiguration() register.TokenConfiguration {
|
||||||
|
return t.TokenOption
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockTokenControl is a simple mock implementation of TokenControl for testing
|
||||||
|
type MockTokenControl struct{}
|
||||||
|
|
||||||
|
func (m *MockTokenControl) CreateToken(ctx context.Context, serviceAccountName, namespace string, expirationSeconds int64) (string, error) {
|
||||||
|
return "mock-token", nil
|
||||||
|
}
|
||||||
5
pkg/registration/register/token/doc.go
Normal file
5
pkg/registration/register/token/doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package token provides a token-based authentication driver for addon registration only.
|
||||||
|
// This driver uses Kubernetes ServiceAccount token projection to authenticate addons with the hub cluster.
|
||||||
|
// It is designed to be forked from cluster-level drivers (CSR/gRPC/AWS IRSA) and cannot be used
|
||||||
|
// for cluster registration itself.
|
||||||
|
package token
|
||||||
42
pkg/registration/register/token/options.go
Normal file
42
pkg/registration/register/token/options.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option contains configuration for the token driver
|
||||||
|
type Option struct {
|
||||||
|
// ExpirationSeconds is the requested duration of validity of the token.
|
||||||
|
// This is used to configure the ServiceAccount token projection.
|
||||||
|
// Default is 31536000 seconds (1 year)
|
||||||
|
ExpirationSeconds int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Option implements register.TokenConfiguration interface at compile time
|
||||||
|
var _ register.TokenConfiguration = &Option{}
|
||||||
|
|
||||||
|
func NewTokenOption() *Option {
|
||||||
|
return &Option{
|
||||||
|
ExpirationSeconds: 31536000, // Default 1 year
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option) AddFlags(fs *pflag.FlagSet) {
|
||||||
|
fs.Int64Var(&o.ExpirationSeconds, "addon-token-expiration-seconds", o.ExpirationSeconds,
|
||||||
|
"Requested duration of validity of the token in seconds. Used for ServiceAccount token projection. Minimum 600 seconds (10 minutes)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option) Validate() error {
|
||||||
|
if o.ExpirationSeconds < 600 {
|
||||||
|
return errors.New("token expiration seconds must be at least 600 (10 minutes)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Option) GetExpirationSeconds() int64 {
|
||||||
|
return o.ExpirationSeconds
|
||||||
|
}
|
||||||
481
pkg/registration/register/token/token.go
Normal file
481
pkg/registration/register/token/token.go
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
authenticationv1 "k8s.io/api/authentication/v1"
|
||||||
|
certificatesv1 "k8s.io/api/certificates/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
||||||
|
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
||||||
|
"open-cluster-management.io/sdk-go/pkg/basecontroller/events"
|
||||||
|
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
|
||||||
|
"open-cluster-management.io/sdk-go/pkg/patcher"
|
||||||
|
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TokenFile is the name of the token file in the secret
|
||||||
|
TokenFile = "token"
|
||||||
|
|
||||||
|
// TokenRefreshedCondition is the condition type for addon token refresh status
|
||||||
|
TokenRefreshedCondition = "TokenRefreshed"
|
||||||
|
|
||||||
|
// TokenInfrastructureReadyCondition is the condition type set by hub to indicate
|
||||||
|
// that the ServiceAccount infrastructure is ready for token-based authentication
|
||||||
|
TokenInfrastructureReadyCondition = "TokenInfrastructureReady"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenControl encapsulates the operations needed for token-based authentication
|
||||||
|
type TokenControl interface {
|
||||||
|
// CreateToken creates a ServiceAccount token
|
||||||
|
CreateToken(ctx context.Context, serviceAccountName, namespace string, expirationSeconds int64) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenControl implements TokenControl interface
|
||||||
|
type tokenControl struct {
|
||||||
|
hubCoreV1Client corev1client.CoreV1Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ TokenControl = &tokenControl{}
|
||||||
|
|
||||||
|
// CreateToken creates a ServiceAccount token using the TokenRequest API
|
||||||
|
func (t *tokenControl) CreateToken(ctx context.Context, serviceAccountName, namespace string, expirationSeconds int64) (string, error) {
|
||||||
|
if t.hubCoreV1Client == nil {
|
||||||
|
return "", fmt.Errorf("failed to create token for ServiceAccount %s/%s: hub CoreV1 client is not initialized", namespace, serviceAccountName)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRequest := &authenticationv1.TokenRequest{
|
||||||
|
Spec: authenticationv1.TokenRequestSpec{
|
||||||
|
ExpirationSeconds: &expirationSeconds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := t.hubCoreV1Client.ServiceAccounts(namespace).CreateToken(
|
||||||
|
ctx,
|
||||||
|
serviceAccountName,
|
||||||
|
tokenRequest,
|
||||||
|
metav1.CreateOptions{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create token for ServiceAccount %s/%s: %w", namespace, serviceAccountName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Status.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenControl creates a new TokenControl instance
|
||||||
|
func NewTokenControl(hubCoreV1Client corev1client.CoreV1Interface) TokenControl {
|
||||||
|
return &tokenControl{
|
||||||
|
hubCoreV1Client: hubCoreV1Client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenDriver implements token-based authentication for addon registration only.
|
||||||
|
// It uses ServiceAccount token projection for authentication with the hub cluster.
|
||||||
|
type TokenDriver struct {
|
||||||
|
addonName string
|
||||||
|
clusterName string
|
||||||
|
opt register.TokenConfiguration
|
||||||
|
tokenControl TokenControl
|
||||||
|
|
||||||
|
// addonClients holds the addon clients and informers
|
||||||
|
addonClients *register.AddOnClients
|
||||||
|
|
||||||
|
// addonPatcher for updating addon status
|
||||||
|
addonPatcher patcher.Patcher[
|
||||||
|
*addonv1alpha1.ManagedClusterAddOn, addonv1alpha1.ManagedClusterAddOnSpec, addonv1alpha1.ManagedClusterAddOnStatus]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ register.RegisterDriver = &TokenDriver{}
|
||||||
|
|
||||||
|
// NewTokenDriverForAddOn creates a new token driver instance for an addon.
|
||||||
|
// This should only be called from a cluster driver's Fork() method.
|
||||||
|
func NewTokenDriverForAddOn(addonName, clusterName string, tokenConfig register.TokenConfiguration, tokenControl TokenControl, addonClients *register.AddOnClients) *TokenDriver {
|
||||||
|
driver := &TokenDriver{
|
||||||
|
addonName: addonName,
|
||||||
|
clusterName: clusterName,
|
||||||
|
tokenControl: tokenControl,
|
||||||
|
addonClients: addonClients,
|
||||||
|
opt: tokenConfig,
|
||||||
|
addonPatcher: patcher.NewPatcher[
|
||||||
|
*addonv1alpha1.ManagedClusterAddOn, addonv1alpha1.ManagedClusterAddOnSpec, addonv1alpha1.ManagedClusterAddOnStatus](
|
||||||
|
addonClients.AddonClient.AddonV1alpha1().ManagedClusterAddOns(clusterName)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return driver
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process updates the secret with the current token from the ServiceAccount token file
|
||||||
|
func (t *TokenDriver) Process(
|
||||||
|
ctx context.Context,
|
||||||
|
controllerName string,
|
||||||
|
secret *corev1.Secret,
|
||||||
|
additionalSecretData map[string][]byte,
|
||||||
|
recorder events.Recorder) (*corev1.Secret, *metav1.Condition, error) {
|
||||||
|
// Get the addon
|
||||||
|
addon, err := t.addonClients.AddonInformer.Lister().ManagedClusterAddOns(t.clusterName).Get(t.addonName)
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
// Addon not found (likely deleted), skip processing
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure subject field is set (driver should already be set by addon registration controller)
|
||||||
|
updated, err := t.ensureSubject(ctx, addon)
|
||||||
|
if err != nil || updated {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for token infrastructure to be ready and get ServiceAccount UID
|
||||||
|
desiredUID, ready, err := t.ensureTokenInfrastructureReady(ctx, addon)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !ready {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need to refresh the token
|
||||||
|
shouldRefresh, err := t.shouldRefreshToken(ctx, secret, desiredUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !shouldRefresh {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the token
|
||||||
|
return t.refreshToken(ctx, secret, recorder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureSubject ensures the subject field is set correctly for token-based authentication.
|
||||||
|
// Subject.user is set to system:serviceaccount:<cluster-namespace>:<addon-name>-agent
|
||||||
|
// Returns (updated, error) where updated indicates if an update was performed
|
||||||
|
func (t *TokenDriver) ensureSubject(ctx context.Context, addon *addonv1alpha1.ManagedClusterAddOn) (bool, error) {
|
||||||
|
logger := klog.FromContext(ctx)
|
||||||
|
|
||||||
|
// Find the registration configuration index
|
||||||
|
var regIndex = -1
|
||||||
|
for i := range addon.Status.Registrations {
|
||||||
|
if addon.Status.Registrations[i].SignerName == certificatesv1.KubeAPIServerClientSignerName {
|
||||||
|
regIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if regIndex == -1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set subject for token-based authentication
|
||||||
|
expectedSubjectUser := fmt.Sprintf("system:serviceaccount:%s:%s-agent", t.clusterName, t.addonName)
|
||||||
|
|
||||||
|
// Make a copy and update subject (create new Subject with only User specified)
|
||||||
|
addonCopy := addon.DeepCopy()
|
||||||
|
addonCopy.Status.Registrations[regIndex].Subject = addonv1alpha1.Subject{
|
||||||
|
User: expectedSubjectUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the addon status using addonPatcher
|
||||||
|
updated, err := t.addonPatcher.PatchStatus(ctx, addonCopy, addonCopy.Status, addon.Status)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated {
|
||||||
|
logger.Info("Updated subject field", "addon", t.addonName, "subject", expectedSubjectUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureTokenInfrastructureReady waits for the TokenInfrastructureReady condition and extracts the ServiceAccount UID
|
||||||
|
// Returns (uid, ready, error) where:
|
||||||
|
// - If ready is true, uid is guaranteed to be non-empty
|
||||||
|
// - If ready is false, infrastructure is not ready yet (caller should wait and retry)
|
||||||
|
// - If error is non-nil, an actual error occurred
|
||||||
|
func (t *TokenDriver) ensureTokenInfrastructureReady(ctx context.Context, addon *addonv1alpha1.ManagedClusterAddOn) (string, bool, error) {
|
||||||
|
logger := klog.FromContext(ctx)
|
||||||
|
|
||||||
|
infraReady := meta.FindStatusCondition(addon.Status.Conditions, TokenInfrastructureReadyCondition)
|
||||||
|
if infraReady == nil {
|
||||||
|
logger.Info("TokenInfrastructureReady condition not found, waiting for hub to set it", "addon", t.addonName)
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if infraReady.Status != metav1.ConditionTrue {
|
||||||
|
logger.Info("TokenInfrastructureReady condition is not True, waiting",
|
||||||
|
"addon", t.addonName,
|
||||||
|
"status", infraReady.Status,
|
||||||
|
"reason", infraReady.Reason)
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
desiredUID, err := t.parseServiceAccountUIDFromMessage(infraReady.Message)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "Failed to parse ServiceAccount UID from TokenInfrastructureReady condition message",
|
||||||
|
"addon", t.addonName,
|
||||||
|
"message", infraReady.Message)
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.V(4).Info("Parsed ServiceAccount UID from TokenInfrastructureReady condition",
|
||||||
|
"addon", t.addonName,
|
||||||
|
"serviceAccountUID", desiredUID)
|
||||||
|
|
||||||
|
return desiredUID, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshToken creates a new token and updates the secret
|
||||||
|
func (t *TokenDriver) refreshToken(ctx context.Context, secret *corev1.Secret, recorder events.Recorder) (*corev1.Secret, *metav1.Condition, error) {
|
||||||
|
tokenData, expiresAt, err := t.createToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &metav1.Condition{
|
||||||
|
Type: TokenRefreshedCondition,
|
||||||
|
Status: metav1.ConditionFalse,
|
||||||
|
Reason: "TokenCreationFailed",
|
||||||
|
Message: fmt.Sprintf("Failed to create token: %v", err),
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.Data[TokenFile] = tokenData
|
||||||
|
recorder.Eventf(ctx, "AddonTokenRefreshed", "Token refreshed for addon %s", t.addonName)
|
||||||
|
|
||||||
|
return secret, &metav1.Condition{
|
||||||
|
Type: TokenRefreshedCondition,
|
||||||
|
Status: metav1.ConditionTrue,
|
||||||
|
Reason: "AddonTokenRefreshed",
|
||||||
|
Message: fmt.Sprintf("Addon token refreshed, expires at %s", expiresAt.Format(time.RFC3339)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildKubeConfigFromTemplate builds kubeconfig with bearer token authentication
|
||||||
|
func (t *TokenDriver) BuildKubeConfigFromTemplate(kubeConfig *clientcmdapi.Config) *clientcmdapi.Config {
|
||||||
|
kubeConfig.AuthInfos = map[string]*clientcmdapi.AuthInfo{
|
||||||
|
register.DefaultKubeConfigAuth: {
|
||||||
|
TokenFile: TokenFile,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return kubeConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// InformerHandler returns the addon informer with a filter for the specific addon
|
||||||
|
func (t *TokenDriver) InformerHandler() (cache.SharedIndexInformer, factory.EventFilterFunc) {
|
||||||
|
if t.addonClients == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create event filter function to only watch the specific addon
|
||||||
|
// Note: The informer is already scoped to the cluster namespace, so we only need to filter by addon name
|
||||||
|
eventFilterFunc := func(obj interface{}) bool {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only enqueue the specific addon (name matches addonName)
|
||||||
|
return accessor.GetName() == t.addonName
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.addonClients.AddonInformer.Informer(), eventFilterFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHubKubeConfigValid checks if the current token is valid
|
||||||
|
func (t *TokenDriver) IsHubKubeConfigValid(ctx context.Context, secretOption register.SecretOption) (bool, error) {
|
||||||
|
logger := klog.FromContext(ctx)
|
||||||
|
|
||||||
|
tokenData, err := t.readTokenFile(secretOption.HubKubeconfigDir)
|
||||||
|
if err != nil {
|
||||||
|
logger.V(4).Info("Token file not found or unreadable")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
desiredUID := t.getDesiredUIDForValidation(ctx)
|
||||||
|
valid, reason := isTokenValid(tokenData, desiredUID)
|
||||||
|
if !valid {
|
||||||
|
logger.V(4).Info("Token is invalid", "reason", reason)
|
||||||
|
}
|
||||||
|
return valid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagedClusterDecorator returns the cluster unchanged (no modifications needed)
|
||||||
|
func (t *TokenDriver) ManagedClusterDecorator(cluster *clusterv1.ManagedCluster) *clusterv1.ManagedCluster {
|
||||||
|
return cluster
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildClients does nothing for TokenDriver
|
||||||
|
func (t *TokenDriver) BuildClients(ctx context.Context, secretOption register.SecretOption, bootstrap bool) (*register.Clients, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldRefreshToken determines if the token needs to be refreshed
|
||||||
|
func (t *TokenDriver) shouldRefreshToken(ctx context.Context, secret *corev1.Secret, desiredUID string) (bool, error) {
|
||||||
|
logger := klog.FromContext(ctx)
|
||||||
|
|
||||||
|
// If no token in secret, refresh is needed
|
||||||
|
tokenData, ok := secret.Data[TokenFile]
|
||||||
|
if !ok || len(tokenData) == 0 {
|
||||||
|
logger.Info("Token refresh needed: no token found in secret", "addon", t.addonName)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is still valid based on expiration and UID
|
||||||
|
valid, reason := isTokenValid(tokenData, desiredUID)
|
||||||
|
if !valid {
|
||||||
|
logger.Info("Token refresh needed", "addon", t.addonName, "reason", reason)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.V(4).Info("Token is valid, no refresh needed", "addon", t.addonName)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readTokenFile reads the token file from the specified directory
|
||||||
|
func (t *TokenDriver) readTokenFile(hubKubeconfigDir string) ([]byte, error) {
|
||||||
|
tokenPath := path.Join(hubKubeconfigDir, TokenFile)
|
||||||
|
return os.ReadFile(path.Clean(tokenPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDesiredUIDForValidation retrieves the desired ServiceAccount UID for token validation
|
||||||
|
// Returns empty string if UID cannot be determined (validation will skip UID check)
|
||||||
|
func (t *TokenDriver) getDesiredUIDForValidation(ctx context.Context) string {
|
||||||
|
logger := klog.FromContext(ctx)
|
||||||
|
|
||||||
|
if t.addonClients == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
addon, err := t.addonClients.AddonInformer.Lister().ManagedClusterAddOns(t.clusterName).Get(t.addonName)
|
||||||
|
if err != nil {
|
||||||
|
logger.V(4).Info("Failed to get addon for token validation", "addon", t.addonName, "error", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
infraReady := meta.FindStatusCondition(addon.Status.Conditions, TokenInfrastructureReadyCondition)
|
||||||
|
if infraReady == nil || infraReady.Status != metav1.ConditionTrue {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := t.parseServiceAccountUIDFromMessage(infraReady.Message)
|
||||||
|
if err != nil {
|
||||||
|
logger.Info("Failed to parse ServiceAccount UID for token validation", "error", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
// createToken creates a new ServiceAccount token using the TokenRequest API
|
||||||
|
// ServiceAccount name format: <addon-name>-agent
|
||||||
|
// Returns the token data and expiration time
|
||||||
|
func (t *TokenDriver) createToken(ctx context.Context) ([]byte, time.Time, error) {
|
||||||
|
if t.tokenControl == nil {
|
||||||
|
return nil, time.Time{}, fmt.Errorf("token control not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceAccount naming convention: <addon-name>-agent
|
||||||
|
// The ServiceAccount is created by the hub addon manager for token-based registration
|
||||||
|
serviceAccountName := fmt.Sprintf("%s-agent", t.addonName)
|
||||||
|
|
||||||
|
// Create token using TokenRequest API
|
||||||
|
token, err := t.tokenControl.CreateToken(ctx, serviceAccountName, t.clusterName, t.opt.GetExpirationSeconds())
|
||||||
|
if err != nil {
|
||||||
|
return nil, time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData := []byte(token)
|
||||||
|
|
||||||
|
// Parse token to get expiration time
|
||||||
|
_, expiresAt, _, err := parseToken(tokenData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, time.Time{}, fmt.Errorf("failed to parse created token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenData, expiresAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseServiceAccountUIDFromMessage parses the ServiceAccount UID from the TokenInfrastructureReady condition message
|
||||||
|
// Expected message format: "ServiceAccount <namespace>/<name> (UID: <uid>) is ready"
|
||||||
|
func (t *TokenDriver) parseServiceAccountUIDFromMessage(message string) (string, error) {
|
||||||
|
// Look for "UID: <uid>" pattern in the message
|
||||||
|
const uidPrefix = "UID: "
|
||||||
|
const uidSuffix = ")"
|
||||||
|
|
||||||
|
startIdx := strings.Index(message, uidPrefix)
|
||||||
|
if startIdx == -1 {
|
||||||
|
return "", fmt.Errorf("ServiceAccount UID not found in message: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
startIdx += len(uidPrefix)
|
||||||
|
endIdx := strings.Index(message[startIdx:], uidSuffix)
|
||||||
|
if endIdx == -1 {
|
||||||
|
return "", fmt.Errorf("malformed ServiceAccount UID in message: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := message[startIdx : startIdx+endIdx]
|
||||||
|
if uid == "" {
|
||||||
|
return "", fmt.Errorf("empty ServiceAccount UID in message: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryForkTokenDriver checks if token-based authentication should be used for the addon,
|
||||||
|
// and if so, creates and returns a TokenDriver. Returns nil if token auth is not needed.
|
||||||
|
// This helper is shared by all cluster drivers (CSR, AWS IRSA, gRPC) to avoid code duplication.
|
||||||
|
func TryForkTokenDriver(
|
||||||
|
addonName string,
|
||||||
|
authConfig register.AddonAuthConfig,
|
||||||
|
secretOption register.SecretOption,
|
||||||
|
tokenControl TokenControl,
|
||||||
|
addonClients *register.AddOnClients,
|
||||||
|
) (register.RegisterDriver, error) {
|
||||||
|
// Determine registration type from signer name
|
||||||
|
isKubeClientType := secretOption.Signer == certificatesv1.KubeAPIServerClientSignerName
|
||||||
|
|
||||||
|
// Only use token auth for KubeClient type with token authentication
|
||||||
|
if !isKubeClientType || authConfig.GetKubeClientAuth() != "token" {
|
||||||
|
return nil, nil // Not using token auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token configuration from AddonAuthConfig (type-safe interface)
|
||||||
|
tokenConfig := authConfig.GetTokenConfiguration()
|
||||||
|
if tokenConfig == nil {
|
||||||
|
return nil, fmt.Errorf("token authentication requested but TokenConfiguration is nil for addon %s", addonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenControl == nil {
|
||||||
|
return nil, fmt.Errorf("token authentication requested but tokenControl is not initialized for addon %s", addonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addonClients == nil {
|
||||||
|
return nil, fmt.Errorf("token authentication requested but addonClients is not initialized for addon %s", addonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addonClients.AddonClient == nil {
|
||||||
|
return nil, fmt.Errorf("token authentication requested but addonClients.AddonClient is nil for addon %s", addonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addonClients.AddonInformer == nil {
|
||||||
|
return nil, fmt.Errorf("token authentication requested but addonClients.AddonInformer is nil for addon %s", addonName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewTokenDriverForAddOn(addonName, secretOption.ClusterName, tokenConfig, tokenControl, addonClients), nil
|
||||||
|
}
|
||||||
820
pkg/registration/register/token/token_test.go
Normal file
820
pkg/registration/register/token/token_test.go
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
|
||||||
|
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
||||||
|
addonfake "open-cluster-management.io/api/client/addon/clientset/versioned/fake"
|
||||||
|
addoninformers "open-cluster-management.io/api/client/addon/informers/externalversions"
|
||||||
|
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
||||||
|
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestAddonClients creates fake addon clients for testing
|
||||||
|
func newTestAddonClients() *register.AddOnClients {
|
||||||
|
addonClient := addonfake.NewSimpleClientset()
|
||||||
|
addonInformerFactory := addoninformers.NewSharedInformerFactory(addonClient, 10*time.Minute)
|
||||||
|
return ®ister.AddOnClients{
|
||||||
|
AddonClient: addonClient,
|
||||||
|
AddonInformer: addonInformerFactory.Addon().V1alpha1().ManagedClusterAddOns(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenDriver_BuildKubeConfigFromTemplate(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
template := &clientcmdapi.Config{
|
||||||
|
Clusters: map[string]*clientcmdapi.Cluster{
|
||||||
|
"hub": {
|
||||||
|
Server: "https://hub.example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Contexts: map[string]*clientcmdapi.Context{
|
||||||
|
register.DefaultKubeConfigContext: {
|
||||||
|
Cluster: "hub",
|
||||||
|
AuthInfo: register.DefaultKubeConfigAuth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CurrentContext: register.DefaultKubeConfigContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := driver.BuildKubeConfigFromTemplate(template)
|
||||||
|
|
||||||
|
if result.AuthInfos == nil {
|
||||||
|
t.Fatal("AuthInfos should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
authInfo, ok := result.AuthInfos[register.DefaultKubeConfigAuth]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("AuthInfo not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if authInfo.TokenFile != TokenFile {
|
||||||
|
t.Errorf("Expected TokenFile to be %q, got %q", TokenFile, authInfo.TokenFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenDriver_ManagedClusterDecorator(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
cluster := &clusterv1.ManagedCluster{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-cluster",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := driver.ManagedClusterDecorator(cluster)
|
||||||
|
|
||||||
|
if result != cluster {
|
||||||
|
t.Error("ManagedClusterDecorator should return the same cluster object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenDriver_InformerHandler(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
informer, filter := driver.InformerHandler()
|
||||||
|
|
||||||
|
if informer == nil {
|
||||||
|
t.Error("Expected informer to be non-nil when addonClients is provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter == nil {
|
||||||
|
t.Error("Expected filter to be non-nil when addonClients is provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenDriver_Process(t *testing.T) {
|
||||||
|
t.Skip("Skipping Process test - requires full mock implementation")
|
||||||
|
// This test requires mocking:
|
||||||
|
// - AddonInformer with lister
|
||||||
|
// - AddonClient for status updates
|
||||||
|
// - TokenControl for token creation
|
||||||
|
// Consider implementing with fake clients in future
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldRefreshToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tokenAge time.Duration
|
||||||
|
tokenExpiry time.Duration
|
||||||
|
shouldRefresh bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fresh token - no refresh",
|
||||||
|
tokenAge: 0,
|
||||||
|
tokenExpiry: 1 * time.Hour,
|
||||||
|
shouldRefresh: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token near expiry - refresh needed",
|
||||||
|
tokenAge: 50 * time.Minute,
|
||||||
|
tokenExpiry: 1 * time.Hour,
|
||||||
|
shouldRefresh: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
opt := NewTokenOption()
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", opt, nil, addonClients)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
iat := now.Add(-tt.tokenAge).Unix()
|
||||||
|
exp := now.Add(tt.tokenExpiry - tt.tokenAge).Unix()
|
||||||
|
mockToken := createMockJWT(t, exp, iat)
|
||||||
|
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
Data: map[string][]byte{
|
||||||
|
TokenFile: []byte(mockToken),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRefresh, err := driver.shouldRefreshToken(context.Background(), secret, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("shouldRefreshToken failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldRefresh != tt.shouldRefresh {
|
||||||
|
t.Errorf("Expected shouldRefresh=%v, got %v", tt.shouldRefresh, shouldRefresh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldRefreshToken_EdgeCases(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
secretData map[string][]byte
|
||||||
|
desiredUID string
|
||||||
|
shouldRefresh bool
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty secret data - refresh needed",
|
||||||
|
secretData: map[string][]byte{},
|
||||||
|
shouldRefresh: true,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty token - refresh needed",
|
||||||
|
secretData: map[string][]byte{
|
||||||
|
TokenFile: []byte(""),
|
||||||
|
},
|
||||||
|
shouldRefresh: true,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid token format - refresh needed",
|
||||||
|
secretData: map[string][]byte{
|
||||||
|
TokenFile: []byte("invalid.token"),
|
||||||
|
},
|
||||||
|
shouldRefresh: true,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UID mismatch - refresh needed",
|
||||||
|
secretData: map[string][]byte{
|
||||||
|
TokenFile: []byte(createMockJWTWithUID(t, time.Now().Add(1*time.Hour).Unix(), time.Now().Unix(), "wrong-uid")),
|
||||||
|
},
|
||||||
|
desiredUID: "expected-uid",
|
||||||
|
shouldRefresh: true,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
Data: tt.secretData,
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRefresh, err := driver.shouldRefreshToken(context.Background(), secret, tt.desiredUID)
|
||||||
|
if (err != nil) != tt.wantError {
|
||||||
|
t.Errorf("shouldRefreshToken() error = %v, wantError %v", err, tt.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldRefresh != tt.shouldRefresh {
|
||||||
|
t.Errorf("shouldRefreshToken() = %v, want %v", shouldRefresh, tt.shouldRefresh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseServiceAccountUIDFromMessage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
wantUID string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid message",
|
||||||
|
message: "ServiceAccount cluster1/cluster1-addon1-agent (UID: 12345678-1234-1234-1234-123456789abc) is ready",
|
||||||
|
wantUID: "12345678-1234-1234-1234-123456789abc",
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing UID prefix",
|
||||||
|
message: "ServiceAccount cluster1/cluster1-addon1-agent is ready",
|
||||||
|
wantUID: "",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing closing parenthesis",
|
||||||
|
message: "ServiceAccount cluster1/cluster1-addon1-agent (UID: 12345678-1234-1234-1234-123456789abc is ready",
|
||||||
|
wantUID: "",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty UID",
|
||||||
|
message: "ServiceAccount cluster1/cluster1-addon1-agent (UID: ) is ready",
|
||||||
|
wantUID: "",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
uid, err := driver.parseServiceAccountUIDFromMessage(tt.message)
|
||||||
|
if (err != nil) != tt.wantError {
|
||||||
|
t.Errorf("parseServiceAccountUIDFromMessage() error = %v, wantError %v", err, tt.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if uid != tt.wantUID {
|
||||||
|
t.Errorf("parseServiceAccountUIDFromMessage() = %v, want %v", uid, tt.wantUID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsHubKubeConfigValid(t *testing.T) {
|
||||||
|
// Create a temporary directory for test files
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
validToken := createMockJWTWithUID(t, now.Add(1*time.Hour).Unix(), now.Unix(), "test-uid")
|
||||||
|
expiredToken := createMockJWTWithUID(t, now.Add(-1*time.Hour).Unix(), now.Add(-2*time.Hour).Unix(), "test-uid")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupFunc func() string
|
||||||
|
wantValid bool
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid token file",
|
||||||
|
setupFunc: func() string {
|
||||||
|
dir := filepath.Join(tmpDir, "valid")
|
||||||
|
os.MkdirAll(dir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(dir, TokenFile), []byte(validToken), 0644)
|
||||||
|
return dir
|
||||||
|
},
|
||||||
|
wantValid: true,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token file does not exist",
|
||||||
|
setupFunc: func() string {
|
||||||
|
return filepath.Join(tmpDir, "nonexistent")
|
||||||
|
},
|
||||||
|
wantValid: false,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired token",
|
||||||
|
setupFunc: func() string {
|
||||||
|
dir := filepath.Join(tmpDir, "expired")
|
||||||
|
os.MkdirAll(dir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(dir, TokenFile), []byte(expiredToken), 0644)
|
||||||
|
return dir
|
||||||
|
},
|
||||||
|
wantValid: false,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
hubKubeconfigDir := tt.setupFunc()
|
||||||
|
|
||||||
|
secretOption := register.SecretOption{
|
||||||
|
HubKubeconfigDir: hubKubeconfigDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
valid, err := driver.IsHubKubeConfigValid(context.Background(), secretOption)
|
||||||
|
if (err != nil) != tt.wantError {
|
||||||
|
t.Errorf("IsHubKubeConfigValid() error = %v, wantError %v", err, tt.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if valid != tt.wantValid {
|
||||||
|
t.Errorf("IsHubKubeConfigValid() = %v, want %v", valid, tt.wantValid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseToken(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
iat := now.Unix()
|
||||||
|
exp := now.Add(1 * time.Hour).Unix()
|
||||||
|
expectedUID := "test-uid-12345"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
token string
|
||||||
|
wantIat int64
|
||||||
|
wantExp int64
|
||||||
|
wantUID string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid token",
|
||||||
|
token: createMockJWTWithUID(t, exp, iat, expectedUID),
|
||||||
|
wantIat: iat,
|
||||||
|
wantExp: exp,
|
||||||
|
wantUID: expectedUID,
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid format - only 2 parts",
|
||||||
|
token: "header.payload",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid format - 4 parts",
|
||||||
|
token: "header.payload.signature.extra",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid base64 payload",
|
||||||
|
token: "header.!!!invalid!!!.signature",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing exp claim",
|
||||||
|
token: createMockJWTWithoutClaim(t, "exp", iat),
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing iat claim",
|
||||||
|
token: createMockJWTWithoutClaim(t, "iat", exp),
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
issueTime, expirationTime, uid, err := parseToken([]byte(tt.token))
|
||||||
|
if (err != nil) != tt.wantError {
|
||||||
|
t.Errorf("parseToken() error = %v, wantError %v", err, tt.wantError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantError {
|
||||||
|
if issueTime.Unix() != tt.wantIat {
|
||||||
|
t.Errorf("parseToken() issueTime = %v, want %v", issueTime.Unix(), tt.wantIat)
|
||||||
|
}
|
||||||
|
if expirationTime.Unix() != tt.wantExp {
|
||||||
|
t.Errorf("parseToken() expirationTime = %v, want %v", expirationTime.Unix(), tt.wantExp)
|
||||||
|
}
|
||||||
|
if uid != tt.wantUID {
|
||||||
|
t.Errorf("parseToken() uid = %v, want %v", uid, tt.wantUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsTokenValid(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tokenData []byte
|
||||||
|
desiredUID string
|
||||||
|
wantValid bool
|
||||||
|
reason string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty token",
|
||||||
|
tokenData: []byte(""),
|
||||||
|
wantValid: false,
|
||||||
|
reason: "token is empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid token with plenty of life remaining",
|
||||||
|
tokenData: []byte(createMockJWTWithUID(t, now.Add(1*time.Hour).Unix(), now.Unix(), "test-uid")),
|
||||||
|
wantValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token near expiration (within refresh threshold)",
|
||||||
|
tokenData: []byte(createMockJWTWithUID(t, now.Add(10*time.Minute).Unix(), now.Add(-50*time.Minute).Unix(), "test-uid")),
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired token",
|
||||||
|
tokenData: []byte(createMockJWTWithUID(t, now.Add(-1*time.Hour).Unix(), now.Add(-2*time.Hour).Unix(), "test-uid")),
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UID mismatch",
|
||||||
|
tokenData: []byte(createMockJWTWithUID(t, now.Add(1*time.Hour).Unix(), now.Unix(), "wrong-uid")),
|
||||||
|
desiredUID: "expected-uid",
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-positive lifetime (exp == iat)",
|
||||||
|
tokenData: []byte(createMockJWTWithUID(t, now.Unix(), now.Unix(), "test-uid")),
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-positive lifetime (exp < iat)",
|
||||||
|
tokenData: []byte(createMockJWTWithUID(t, now.Add(-1*time.Hour).Unix(), now.Unix(), "test-uid")),
|
||||||
|
wantValid: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
valid, reason := isTokenValid(tt.tokenData, tt.desiredUID)
|
||||||
|
if valid != tt.wantValid {
|
||||||
|
t.Errorf("isTokenValid() = %v, want %v (reason: %s)", valid, tt.wantValid, reason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMockJWT creates a simplified mock JWT token for testing
|
||||||
|
// Note: This is not a real signed JWT, just enough structure for parsing tests
|
||||||
|
func createMockJWT(t *testing.T, exp, iat int64) string {
|
||||||
|
t.Helper()
|
||||||
|
return createMockJWTWithUID(t, exp, iat, "12345678-1234-1234-1234-123456789abc")
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMockJWTWithUID creates a mock JWT token with a specific UID
|
||||||
|
func createMockJWTWithUID(t *testing.T, exp, iat int64, uid string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// JWT header (base64url encoded)
|
||||||
|
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" // {"alg":"RS256","typ":"JWT"}
|
||||||
|
|
||||||
|
// Create payload with exp, iat, and kubernetes metadata
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"exp": exp,
|
||||||
|
"iat": iat,
|
||||||
|
"sub": "system:serviceaccount:default:test-sa",
|
||||||
|
"aud": "https://kubernetes.default.svc",
|
||||||
|
"kubernetes.io": map[string]interface{}{
|
||||||
|
"namespace": "default",
|
||||||
|
"serviceaccount": map[string]interface{}{
|
||||||
|
"name": "test-sa",
|
||||||
|
"uid": uid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadEncoded := base64.RawURLEncoding.EncodeToString(payloadBytes)
|
||||||
|
|
||||||
|
// Mock signature (base64url encoded)
|
||||||
|
signature := "mock-signature"
|
||||||
|
|
||||||
|
return header + "." + payloadEncoded + "." + signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMockJWTWithoutClaim creates a mock JWT token missing a specific claim
|
||||||
|
func createMockJWTWithoutClaim(t *testing.T, missingClaim string, value int64) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"sub": "system:serviceaccount:default:test-sa",
|
||||||
|
"aud": "https://kubernetes.default.svc",
|
||||||
|
"kubernetes.io": map[string]interface{}{
|
||||||
|
"namespace": "default",
|
||||||
|
"serviceaccount": map[string]interface{}{
|
||||||
|
"name": "test-sa",
|
||||||
|
"uid": "test-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the claims except the one we want to omit
|
||||||
|
if missingClaim != "exp" {
|
||||||
|
payload["exp"] = value
|
||||||
|
}
|
||||||
|
if missingClaim != "iat" {
|
||||||
|
payload["iat"] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadEncoded := base64.RawURLEncoding.EncodeToString(payloadBytes)
|
||||||
|
signature := "mock-signature"
|
||||||
|
|
||||||
|
return header + "." + payloadEncoded + "." + signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenDriver_NilGuards tests nil pointer guards for critical components
|
||||||
|
func TestTokenDriver_NilGuards(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupDriver func() *TokenDriver
|
||||||
|
testFunc func(t *testing.T, driver *TokenDriver)
|
||||||
|
expectError bool
|
||||||
|
expectedErrorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "createToken with nil tokenControl",
|
||||||
|
setupDriver: func() *TokenDriver {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
return NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
},
|
||||||
|
testFunc: func(t *testing.T, driver *TokenDriver) {
|
||||||
|
_, _, err := driver.createToken(context.Background())
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when tokenControl is nil")
|
||||||
|
}
|
||||||
|
if err != nil && err.Error() != "token control not initialized" {
|
||||||
|
t.Errorf("Expected 'token control not initialized' error, got: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectedErrorMsg: "token control not initialized",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InformerHandler returns correct informer and filter",
|
||||||
|
setupDriver: func() *TokenDriver {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
return NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
},
|
||||||
|
testFunc: func(t *testing.T, driver *TokenDriver) {
|
||||||
|
informer, filter := driver.InformerHandler()
|
||||||
|
if informer == nil {
|
||||||
|
t.Error("Expected non-nil informer")
|
||||||
|
}
|
||||||
|
if filter == nil {
|
||||||
|
t.Error("Expected non-nil filter")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
driver := tt.setupDriver()
|
||||||
|
tt.testFunc(t, driver)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenDriver_EnsureTokenInfrastructureReady tests infrastructure readiness checks
|
||||||
|
func TestTokenDriver_EnsureTokenInfrastructureReady(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addon *addonv1alpha1.ManagedClusterAddOn
|
||||||
|
expectedUID string
|
||||||
|
expectedReady bool
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no TokenInfrastructureReady condition",
|
||||||
|
addon: &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-addon",
|
||||||
|
},
|
||||||
|
Status: addonv1alpha1.ManagedClusterAddOnStatus{
|
||||||
|
Conditions: []metav1.Condition{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUID: "",
|
||||||
|
expectedReady: false,
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TokenInfrastructureReady condition is False",
|
||||||
|
addon: &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-addon",
|
||||||
|
},
|
||||||
|
Status: addonv1alpha1.ManagedClusterAddOnStatus{
|
||||||
|
Conditions: []metav1.Condition{
|
||||||
|
{
|
||||||
|
Type: "TokenInfrastructureReady",
|
||||||
|
Status: metav1.ConditionFalse,
|
||||||
|
Reason: "ServiceAccountNotFound",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUID: "",
|
||||||
|
expectedReady: false,
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TokenInfrastructureReady is True with valid UID",
|
||||||
|
addon: &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-addon",
|
||||||
|
},
|
||||||
|
Status: addonv1alpha1.ManagedClusterAddOnStatus{
|
||||||
|
Conditions: []metav1.Condition{
|
||||||
|
{
|
||||||
|
Type: "TokenInfrastructureReady",
|
||||||
|
Status: metav1.ConditionTrue,
|
||||||
|
Reason: "ServiceAccountReady",
|
||||||
|
Message: "ServiceAccount default/test-addon-agent (UID: 12345678-1234-1234-1234-123456789abc) is ready",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUID: "12345678-1234-1234-1234-123456789abc",
|
||||||
|
expectedReady: true,
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TokenInfrastructureReady is True but invalid message format",
|
||||||
|
addon: &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-addon",
|
||||||
|
},
|
||||||
|
Status: addonv1alpha1.ManagedClusterAddOnStatus{
|
||||||
|
Conditions: []metav1.Condition{
|
||||||
|
{
|
||||||
|
Type: "TokenInfrastructureReady",
|
||||||
|
Status: metav1.ConditionTrue,
|
||||||
|
Reason: "ServiceAccountReady",
|
||||||
|
Message: "Invalid message without UID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedUID: "",
|
||||||
|
expectedReady: false,
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
uid, ready, err := driver.ensureTokenInfrastructureReady(context.Background(), tt.addon)
|
||||||
|
|
||||||
|
if (err != nil) != tt.expectedError {
|
||||||
|
t.Errorf("Expected error=%v, got error=%v", tt.expectedError, err)
|
||||||
|
}
|
||||||
|
if ready != tt.expectedReady {
|
||||||
|
t.Errorf("Expected ready=%v, got ready=%v", tt.expectedReady, ready)
|
||||||
|
}
|
||||||
|
if uid != tt.expectedUID {
|
||||||
|
t.Errorf("Expected UID=%q, got UID=%q", tt.expectedUID, uid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenDriver_ParseServiceAccountUIDFromMessage tests UID parsing from condition messages
|
||||||
|
func TestTokenDriver_ParseServiceAccountUIDFromMessage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
message string
|
||||||
|
expectedUID string
|
||||||
|
wantError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid message with UID",
|
||||||
|
message: "ServiceAccount default/test-addon-agent (UID: 12345678-1234-1234-1234-123456789abc) is ready",
|
||||||
|
expectedUID: "12345678-1234-1234-1234-123456789abc",
|
||||||
|
wantError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message without UID prefix",
|
||||||
|
message: "ServiceAccount is ready",
|
||||||
|
expectedUID: "",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message with UID prefix but no closing paren",
|
||||||
|
message: "ServiceAccount (UID: 12345678-1234-1234-1234-123456789abc",
|
||||||
|
expectedUID: "",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty message",
|
||||||
|
message: "",
|
||||||
|
expectedUID: "",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "message with empty UID",
|
||||||
|
message: "ServiceAccount (UID: ) is ready",
|
||||||
|
expectedUID: "",
|
||||||
|
wantError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
uid, err := driver.parseServiceAccountUIDFromMessage(tt.message)
|
||||||
|
|
||||||
|
if (err != nil) != tt.wantError {
|
||||||
|
t.Errorf("Expected error=%v, got error=%v", tt.wantError, err)
|
||||||
|
}
|
||||||
|
if uid != tt.expectedUID {
|
||||||
|
t.Errorf("Expected UID=%q, got UID=%q", tt.expectedUID, uid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenDriver_InformerHandlerFilter tests the event filter function
|
||||||
|
func TestTokenDriver_InformerHandlerFilter(t *testing.T) {
|
||||||
|
addonClients := newTestAddonClients()
|
||||||
|
driver := NewTokenDriverForAddOn("test-addon", "test-cluster", NewTokenOption(), nil, addonClients)
|
||||||
|
|
||||||
|
_, filter := driver.InformerHandler()
|
||||||
|
if filter == nil {
|
||||||
|
t.Fatal("Expected non-nil filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
obj interface{}
|
||||||
|
expectedResult bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "matching addon name",
|
||||||
|
obj: &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-addon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedResult: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching addon name",
|
||||||
|
obj: &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "other-addon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedResult: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid object type",
|
||||||
|
obj: "not-a-valid-object",
|
||||||
|
expectedResult: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := filter(tt.obj)
|
||||||
|
if result != tt.expectedResult {
|
||||||
|
t.Errorf("Expected filter result=%v, got=%v", tt.expectedResult, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
123
pkg/registration/register/token/token_validation.go
Normal file
123
pkg/registration/register/token/token_validation.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RefreshThreshold is the percentage of token lifetime remaining before refresh
|
||||||
|
// When the token has 20% or less of its lifetime remaining, it will be refreshed
|
||||||
|
RefreshThreshold = 0.2
|
||||||
|
)
|
||||||
|
|
||||||
|
// jwtClaims represents the claims section of a JWT token
|
||||||
|
type jwtClaims struct {
|
||||||
|
Exp int64 `json:"exp"` // Expiration time (Unix timestamp)
|
||||||
|
Iat int64 `json:"iat"` // Issued at time (Unix timestamp)
|
||||||
|
Kubernetes kubernetesMetadata `json:"kubernetes.io,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// kubernetesMetadata represents Kubernetes-specific claims in the token
|
||||||
|
type kubernetesMetadata struct {
|
||||||
|
ServiceAccount serviceAccountMetadata `json:"serviceaccount,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceAccountMetadata represents service account information in the token
|
||||||
|
type serviceAccountMetadata struct {
|
||||||
|
UID string `json:"uid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseToken parses a JWT token and extracts issue time, expiration time, and service account UID
|
||||||
|
func parseToken(tokenData []byte) (issueTime, expirationTime time.Time, uid string, err error) {
|
||||||
|
token := string(tokenData)
|
||||||
|
|
||||||
|
// JWT tokens have three parts separated by dots: header.payload.signature
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
err = fmt.Errorf("invalid JWT token format: expected 3 parts, got %d", len(parts))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the payload (second part)
|
||||||
|
payloadEncoded := parts[1]
|
||||||
|
// JWT uses base64url encoding, which may need padding
|
||||||
|
if l := len(payloadEncoded) % 4; l > 0 {
|
||||||
|
payloadEncoded += strings.Repeat("=", 4-l)
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadBytes, decodeErr := base64.URLEncoding.DecodeString(payloadEncoded)
|
||||||
|
if decodeErr != nil {
|
||||||
|
err = fmt.Errorf("failed to decode JWT payload: %w", decodeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the claims
|
||||||
|
var claims jwtClaims
|
||||||
|
if unmarshalErr := json.Unmarshal(payloadBytes, &claims); unmarshalErr != nil {
|
||||||
|
err = fmt.Errorf("failed to unmarshal JWT claims: %w", unmarshalErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Exp == 0 {
|
||||||
|
err = fmt.Errorf("token does not have expiration claim")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Iat == 0 {
|
||||||
|
err = fmt.Errorf("token does not have issued-at claim")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
issueTime = time.Unix(claims.Iat, 0)
|
||||||
|
expirationTime = time.Unix(claims.Exp, 0)
|
||||||
|
uid = claims.Kubernetes.ServiceAccount.UID
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTokenValid checks if a token is still valid based on its expiration and RefreshThreshold (20%)
|
||||||
|
// Returns (valid, reason) where reason describes why the token is invalid
|
||||||
|
// desiredUID is the expected service account UID; if empty, UID check is skipped
|
||||||
|
func isTokenValid(tokenData []byte, desiredUID string) (bool, string) {
|
||||||
|
if len(tokenData) == 0 {
|
||||||
|
return false, "token is empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
issueTime, expirationTime, uid, err := parseToken(tokenData)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Sprintf("failed to parse token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the token's service account UID matches the desired UID
|
||||||
|
if desiredUID != "" && uid != desiredUID {
|
||||||
|
return false, fmt.Sprintf("token service account UID mismatch (expected %s, got %s)", desiredUID, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if expirationTime.Before(now) {
|
||||||
|
return false, fmt.Sprintf("token has expired at %s", expirationTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
totalLifetime := expirationTime.Sub(issueTime)
|
||||||
|
// Guard against non-positive lifetime (exp == iat or exp < iat)
|
||||||
|
if totalLifetime <= 0 {
|
||||||
|
return false, "token has non-positive lifetime"
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining := time.Until(expirationTime)
|
||||||
|
|
||||||
|
// Calculate the percentage of lifetime remaining
|
||||||
|
remainingPercentage := remaining.Seconds() / totalLifetime.Seconds()
|
||||||
|
|
||||||
|
// Token is valid if it has more than the threshold percentage of its lifetime remaining
|
||||||
|
if remainingPercentage <= RefreshThreshold {
|
||||||
|
return false, fmt.Sprintf("token is close to expiration (%.1f%% remaining, threshold %.1f%%)",
|
||||||
|
remainingPercentage*100, RefreshThreshold*100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
@@ -81,34 +81,37 @@ func isAddonRunningOutsideManagedCluster(addOn *addonv1alpha1.ManagedClusterAddO
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRegistrationConfigs reads annotations of a addon and returns a map of registrationConfig whose
|
// getRegistrationConfigs reads registrations and returns a map of registrationConfig whose
|
||||||
// key is the hash of the registrationConfig
|
// key is the hash of the registrationConfig.
|
||||||
func getRegistrationConfigs(addOn *addonv1alpha1.ManagedClusterAddOn) (map[string]registrationConfig, error) {
|
func getRegistrationConfigs(
|
||||||
|
addOnName string,
|
||||||
|
installOption addonInstallOption,
|
||||||
|
registrations []addonv1alpha1.RegistrationConfig,
|
||||||
|
kubeClientDriver string,
|
||||||
|
) (map[string]registrationConfig, error) {
|
||||||
configs := map[string]registrationConfig{}
|
configs := map[string]registrationConfig{}
|
||||||
|
|
||||||
for _, registration := range addOn.Status.Registrations {
|
for _, registration := range registrations {
|
||||||
config := registrationConfig{
|
config := registrationConfig{
|
||||||
addOnName: addOn.Name,
|
addOnName: addOnName,
|
||||||
addonInstallOption: addonInstallOption{
|
addonInstallOption: installOption,
|
||||||
AgentRunningOutsideManagedCluster: isAddonRunningOutsideManagedCluster(addOn),
|
registration: registration,
|
||||||
InstallationNamespace: getAddOnInstallationNamespace(addOn),
|
|
||||||
},
|
|
||||||
registration: registration,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the secret name of client certificate
|
// set the secret name of client certificate
|
||||||
switch registration.SignerName {
|
switch registration.SignerName {
|
||||||
case certificatesv1.KubeAPIServerClientSignerName:
|
case certificatesv1.KubeAPIServerClientSignerName:
|
||||||
config.secretName = fmt.Sprintf("%s-hub-kubeconfig", addOn.Name)
|
config.secretName = fmt.Sprintf("%s-hub-kubeconfig", addOnName)
|
||||||
default:
|
default:
|
||||||
config.secretName = fmt.Sprintf("%s-%s-client-cert", addOn.Name, strings.ReplaceAll(registration.SignerName, "/", "-"))
|
config.secretName = fmt.Sprintf("%s-%s-client-cert", addOnName, strings.ReplaceAll(registration.SignerName, "/", "-"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// hash registration configuration, install namespace and addOnAgentRunningOutsideManagedCluster. Use the hash
|
// hash registration configuration, install namespace and addOnAgentRunningOutsideManagedCluster. Use the hash
|
||||||
// value as the key of map to make sure each registration configuration and addon installation option is unique
|
// value as the key of map to make sure each registration configuration and addon installation option is unique
|
||||||
hash, err := getConfigHash(
|
hash, err := getConfigHash(
|
||||||
registration,
|
registration,
|
||||||
config.addonInstallOption)
|
config.addonInstallOption,
|
||||||
|
kubeClientDriver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
@@ -119,8 +122,23 @@ func getRegistrationConfigs(addOn *addonv1alpha1.ManagedClusterAddOn) (map[strin
|
|||||||
return configs, nil
|
return configs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigHash(registration addonv1alpha1.RegistrationConfig, installOption addonInstallOption) (string, error) {
|
func getConfigHash(registration addonv1alpha1.RegistrationConfig, installOption addonInstallOption, kubeClientDriver string) (string, error) {
|
||||||
data, err := json.Marshal(registration)
|
// Create a canonical config for hashing, excluding status fields set by the agent.
|
||||||
|
// Driver is always excluded (set by agent as status, not configuration)
|
||||||
|
// Subject is excluded only for token-based authentication (set by token driver)
|
||||||
|
// Subject is included for CSR-based and custom signer authentication (part of the configuration)
|
||||||
|
canonicalConfig := addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: registration.SignerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// For KubeClient type registrations, check if driver is token
|
||||||
|
// For custom signers, always include subject
|
||||||
|
isKubeClientType := registration.SignerName == certificatesv1.KubeAPIServerClientSignerName
|
||||||
|
if !isKubeClientType || kubeClientDriver != "token" {
|
||||||
|
canonicalConfig.Subject = registration.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(canonicalConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,11 @@ func TestGetRegistrationConfigs(t *testing.T) {
|
|||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
configs, err := getRegistrationConfigs(c.addon)
|
installOption := addonInstallOption{
|
||||||
|
AgentRunningOutsideManagedCluster: isAddonRunningOutsideManagedCluster(c.addon),
|
||||||
|
InstallationNamespace: getAddOnInstallationNamespace(c.addon),
|
||||||
|
}
|
||||||
|
configs, err := getRegistrationConfigs(c.addon.Name, installOption, c.addon.Status.Registrations, c.addon.Status.KubeClientDriver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -165,8 +169,191 @@ func newRegistrationConfig(addOnName, addOnNamespace, signerName, commonName str
|
|||||||
registration: registration,
|
registration: registration,
|
||||||
}
|
}
|
||||||
|
|
||||||
hash, _ := getConfigHash(registration, config.addonInstallOption)
|
hash, _ := getConfigHash(registration, config.addonInstallOption, "")
|
||||||
config.hash = hash
|
config.hash = hash
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConfigHash_StatusFieldsExcluded verifies that driver is always excluded from hash,
|
||||||
|
// and subject field is conditionally excluded based on driver type
|
||||||
|
func TestConfigHash_StatusFieldsExcluded(t *testing.T) {
|
||||||
|
installOption := addonInstallOption{
|
||||||
|
InstallationNamespace: "test-ns",
|
||||||
|
AgentRunningOutsideManagedCluster: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
config1 addonv1alpha1.RegistrationConfig
|
||||||
|
driver1 string
|
||||||
|
config2 addonv1alpha1.RegistrationConfig
|
||||||
|
driver2 string
|
||||||
|
expectEqual bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "driver field always excluded - csr vs token with same subject",
|
||||||
|
config1: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver1: "csr",
|
||||||
|
config2: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver2: "token",
|
||||||
|
expectEqual: false, // Different because csr includes subject, token excludes it
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "token driver excludes subject - different subjects should have same hash",
|
||||||
|
config1: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user-1",
|
||||||
|
Groups: []string{"test-group-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver1: "token",
|
||||||
|
config2: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user-2",
|
||||||
|
Groups: []string{"test-group-2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver2: "token",
|
||||||
|
expectEqual: true, // Same hash because subject is excluded for token driver
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "csr driver includes subject - different subjects should have different hash",
|
||||||
|
config1: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user-1",
|
||||||
|
Groups: []string{"test-group-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver1: "csr",
|
||||||
|
config2: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user-2",
|
||||||
|
Groups: []string{"test-group-2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver2: "csr",
|
||||||
|
expectEqual: false, // Different hash because subject is included for csr driver
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "csr driver includes subject - same subjects should have same hash",
|
||||||
|
config1: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver1: "csr",
|
||||||
|
config2: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver2: "csr",
|
||||||
|
expectEqual: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom signer includes subject - different subjects should have different hash",
|
||||||
|
config1: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: "custom.signer.io/custom",
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user-1",
|
||||||
|
Groups: []string{"test-group-1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver1: "",
|
||||||
|
config2: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: "custom.signer.io/custom",
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user-2",
|
||||||
|
Groups: []string{"test-group-2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver2: "",
|
||||||
|
expectEqual: false, // Different hash because subject is included for custom signer
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom signer includes subject - same subjects should have same hash",
|
||||||
|
config1: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: "custom.signer.io/custom",
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver1: "",
|
||||||
|
config2: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: "custom.signer.io/custom",
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver2: "",
|
||||||
|
expectEqual: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signer name changes hash",
|
||||||
|
config1: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: certificates.KubeAPIServerClientSignerName,
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver1: "csr",
|
||||||
|
config2: addonv1alpha1.RegistrationConfig{
|
||||||
|
SignerName: "custom.signer.io/custom",
|
||||||
|
Subject: addonv1alpha1.Subject{
|
||||||
|
User: "test-user",
|
||||||
|
Groups: []string{"test-group"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
driver2: "",
|
||||||
|
expectEqual: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
hash1, err := getConfigHash(c.config1, installOption, c.driver1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash2, err := getConfigHash(c.config2, installOption, c.driver2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.expectEqual {
|
||||||
|
if hash1 != hash2 {
|
||||||
|
t.Errorf("expected hashes to be equal, got:\nhash1=%s\nhash2=%s", hash1, hash2)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if hash1 == hash2 {
|
||||||
|
t.Errorf("expected hashes to differ, got same hash: %s", hash1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,9 +38,12 @@ type addOnRegistrationController struct {
|
|||||||
hubAddOnLister addonlisterv1alpha1.ManagedClusterAddOnLister
|
hubAddOnLister addonlisterv1alpha1.ManagedClusterAddOnLister
|
||||||
patcher patcher.Patcher[
|
patcher patcher.Patcher[
|
||||||
*addonv1alpha1.ManagedClusterAddOn, addonv1alpha1.ManagedClusterAddOnSpec, addonv1alpha1.ManagedClusterAddOnStatus]
|
*addonv1alpha1.ManagedClusterAddOn, addonv1alpha1.ManagedClusterAddOnSpec, addonv1alpha1.ManagedClusterAddOnStatus]
|
||||||
addonDriver register.AddonDriver
|
addonDriverFactory register.AddonDriverFactory
|
||||||
|
// addonAuthConfig holds the cluster-wide addon registration configuration
|
||||||
|
// that provides authentication method and access to driver options (CSR, Token)
|
||||||
|
addonAuthConfig register.AddonAuthConfig
|
||||||
|
|
||||||
startRegistrationFunc func(ctx context.Context, config registrationConfig) context.CancelFunc
|
startRegistrationFunc func(ctx context.Context, config registrationConfig) (context.CancelFunc, error)
|
||||||
|
|
||||||
// registrationConfigs maps the addon name to a map of registrationConfigs whose key is the hash of
|
// registrationConfigs maps the addon name to a map of registrationConfigs whose key is the hash of
|
||||||
// the registrationConfig
|
// the registrationConfig
|
||||||
@@ -55,7 +58,8 @@ func NewAddOnRegistrationController(
|
|||||||
addOnClient addonclient.Interface,
|
addOnClient addonclient.Interface,
|
||||||
managementKubeClient kubernetes.Interface,
|
managementKubeClient kubernetes.Interface,
|
||||||
managedKubeClient kubernetes.Interface,
|
managedKubeClient kubernetes.Interface,
|
||||||
addonDriver register.AddonDriver,
|
addonDriverFactory register.AddonDriverFactory,
|
||||||
|
addonAuthConfig register.AddonAuthConfig,
|
||||||
hubAddOnInformers addoninformerv1alpha1.ManagedClusterAddOnInformer,
|
hubAddOnInformers addoninformerv1alpha1.ManagedClusterAddOnInformer,
|
||||||
) factory.Controller {
|
) factory.Controller {
|
||||||
c := &addOnRegistrationController{
|
c := &addOnRegistrationController{
|
||||||
@@ -65,7 +69,8 @@ func NewAddOnRegistrationController(
|
|||||||
managementKubeClient: managementKubeClient,
|
managementKubeClient: managementKubeClient,
|
||||||
spokeKubeClient: managedKubeClient,
|
spokeKubeClient: managedKubeClient,
|
||||||
hubAddOnLister: hubAddOnInformers.Lister(),
|
hubAddOnLister: hubAddOnInformers.Lister(),
|
||||||
addonDriver: addonDriver,
|
addonDriverFactory: addonDriverFactory,
|
||||||
|
addonAuthConfig: addonAuthConfig,
|
||||||
patcher: patcher.NewPatcher[
|
patcher: patcher.NewPatcher[
|
||||||
*addonv1alpha1.ManagedClusterAddOn, addonv1alpha1.ManagedClusterAddOnSpec, addonv1alpha1.ManagedClusterAddOnStatus](
|
*addonv1alpha1.ManagedClusterAddOn, addonv1alpha1.ManagedClusterAddOnSpec, addonv1alpha1.ManagedClusterAddOnStatus](
|
||||||
addOnClient.AddonV1alpha1().ManagedClusterAddOns(clusterName)),
|
addOnClient.AddonV1alpha1().ManagedClusterAddOns(clusterName)),
|
||||||
@@ -127,14 +132,24 @@ func (c *addOnRegistrationController) syncAddOn(ctx context.Context, syncCtx fac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedConfigs := c.addOnRegistrationConfigs[addOnName]
|
// Ensure driver field is set for kubeClient type registrations
|
||||||
configs, err := getRegistrationConfigs(addOn)
|
// If updated, return early to allow re-sync with the updated state
|
||||||
|
if updated, err := c.ensureDriver(ctx, addOn); err != nil || updated {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installOption := addonInstallOption{
|
||||||
|
AgentRunningOutsideManagedCluster: isAddonRunningOutsideManagedCluster(addOn),
|
||||||
|
InstallationNamespace: getAddOnInstallationNamespace(addOn),
|
||||||
|
}
|
||||||
|
configs, err := getRegistrationConfigs(addOnName, installOption, addOn.Status.Registrations, addOn.Status.KubeClientDriver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop registration for the stale registration configs
|
// stop registration for the stale registration configs
|
||||||
var errs []error
|
var errs []error
|
||||||
|
cachedConfigs := c.addOnRegistrationConfigs[addOnName]
|
||||||
for hash, cachedConfig := range cachedConfigs {
|
for hash, cachedConfig := range cachedConfigs {
|
||||||
if _, ok := configs[hash]; ok {
|
if _, ok := configs[hash]; ok {
|
||||||
continue
|
continue
|
||||||
@@ -152,27 +167,32 @@ func (c *addOnRegistrationController) syncAddOn(ctx context.Context, syncCtx fac
|
|||||||
for hash, config := range configs {
|
for hash, config := range configs {
|
||||||
// keep the unchanged configs
|
// keep the unchanged configs
|
||||||
if cachedConfig, ok := cachedConfigs[hash]; ok {
|
if cachedConfig, ok := cachedConfigs[hash]; ok {
|
||||||
|
// Hash matches - keep the existing controller running
|
||||||
|
// Note: Driver is always excluded from hash, Subject is conditionally included based on driver type
|
||||||
syncedConfigs[hash] = cachedConfig
|
syncedConfigs[hash] = cachedConfig
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// start registration for the new added configs
|
// start registration for the new added configs
|
||||||
config.stopFunc = c.startRegistrationFunc(ctx, config)
|
stopFunc, err := c.startRegistrationFunc(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
config.stopFunc = stopFunc
|
||||||
syncedConfigs[hash] = config
|
syncedConfigs[hash] = config
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(syncedConfigs) == 0 {
|
if len(syncedConfigs) == 0 {
|
||||||
delete(c.addOnRegistrationConfigs, addOnName)
|
delete(c.addOnRegistrationConfigs, addOnName)
|
||||||
return nil
|
return operatorhelpers.NewMultiLineAggregate(errs)
|
||||||
}
|
}
|
||||||
c.addOnRegistrationConfigs[addOnName] = syncedConfigs
|
c.addOnRegistrationConfigs[addOnName] = syncedConfigs
|
||||||
return nil
|
return operatorhelpers.NewMultiLineAggregate(errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startRegistration starts a client certificate controller with the given config
|
// startRegistration starts a client certificate controller with the given config
|
||||||
func (c *addOnRegistrationController) startRegistration(ctx context.Context, config registrationConfig) context.CancelFunc {
|
func (c *addOnRegistrationController) startRegistration(ctx context.Context, config registrationConfig) (context.CancelFunc, error) {
|
||||||
ctx, stopFunc := context.WithCancel(ctx)
|
|
||||||
|
|
||||||
// the kubeClient here will be used to generate the hub kubeconfig secret for addon agents, it generates the secret
|
// the kubeClient here will be used to generate the hub kubeconfig secret for addon agents, it generates the secret
|
||||||
// on the managed cluster by default, but if the addon agent is not running on the managed cluster(in Hosted mode
|
// on the managed cluster by default, but if the addon agent is not running on the managed cluster(in Hosted mode
|
||||||
// the addon agent runs outside the managed cluster, for more details see the hosted mode design docs for addon:
|
// the addon agent runs outside the managed cluster, for more details see the hosted mode design docs for addon:
|
||||||
@@ -197,7 +217,16 @@ func (c *addOnRegistrationController) startRegistration(ctx context.Context, con
|
|||||||
if config.registration.SignerName == certificatesv1.KubeAPIServerClientSignerName {
|
if config.registration.SignerName == certificatesv1.KubeAPIServerClientSignerName {
|
||||||
secretOption.BootStrapKubeConfigFile = c.kubeconfigFile
|
secretOption.BootStrapKubeConfigFile = c.kubeconfigFile
|
||||||
}
|
}
|
||||||
driver := c.addonDriver.Fork(config.addOnName, secretOption)
|
|
||||||
|
// Pass AddonAuthConfig directly to Fork
|
||||||
|
// It provides authentication method and access to driver options (CSR, Token)
|
||||||
|
// Registration type (KubeClient vs CustomSigner) is determined by Fork implementation
|
||||||
|
// based on secretOption.Signer
|
||||||
|
driver, err := c.addonDriverFactory.Fork(config.addOnName, c.addonAuthConfig, secretOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create driver for addon %s: %w", config.addOnName, err)
|
||||||
|
}
|
||||||
|
|
||||||
controllerName := fmt.Sprintf("ClientCertController@addon:%s:signer:%s", config.addOnName, config.registration.SignerName)
|
controllerName := fmt.Sprintf("ClientCertController@addon:%s:signer:%s", config.addOnName, config.registration.SignerName)
|
||||||
statusUpdater := c.generateStatusUpdate(c.clusterName, config.addOnName)
|
statusUpdater := c.generateStatusUpdate(c.clusterName, config.addOnName)
|
||||||
secretController := register.NewSecretController(
|
secretController := register.NewSecretController(
|
||||||
@@ -206,10 +235,11 @@ func (c *addOnRegistrationController) startRegistration(ctx context.Context, con
|
|||||||
kubeInformerFactory.Core().V1().Secrets().Informer(),
|
kubeInformerFactory.Core().V1().Secrets().Informer(),
|
||||||
controllerName)
|
controllerName)
|
||||||
|
|
||||||
|
ctx, stopFunc := context.WithCancel(ctx)
|
||||||
go kubeInformerFactory.Start(ctx.Done())
|
go kubeInformerFactory.Start(ctx.Done())
|
||||||
go secretController.Run(ctx, 1)
|
go secretController.Run(ctx, 1)
|
||||||
|
|
||||||
return stopFunc
|
return stopFunc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *addOnRegistrationController) generateStatusUpdate(clusterName, addonName string) register.StatusUpdateFunc {
|
func (c *addOnRegistrationController) generateStatusUpdate(clusterName, addonName string) register.StatusUpdateFunc {
|
||||||
@@ -250,6 +280,41 @@ func (c *addOnRegistrationController) stopRegistration(ctx context.Context, conf
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureDriver sets Status.KubeClientDriver based on agent configuration to declare
|
||||||
|
// the authentication capability (csr or token) to the hub controller.
|
||||||
|
// Only sets the driver if the addon has a KubeClient type registration (KubeAPIServerClientSignerName).
|
||||||
|
// For custom signers, the driver is set to empty string.
|
||||||
|
func (c *addOnRegistrationController) ensureDriver(ctx context.Context, addon *addonv1alpha1.ManagedClusterAddOn) (bool, error) {
|
||||||
|
logger := klog.FromContext(ctx)
|
||||||
|
|
||||||
|
// Check if addon has any KubeClient type registrations
|
||||||
|
hasKubeClientRegistration := false
|
||||||
|
for _, reg := range addon.Status.Registrations {
|
||||||
|
if reg.SignerName == certificatesv1.KubeAPIServerClientSignerName {
|
||||||
|
hasKubeClientRegistration = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addonCopy := addon.DeepCopy()
|
||||||
|
if hasKubeClientRegistration {
|
||||||
|
addonCopy.Status.KubeClientDriver = c.addonAuthConfig.GetKubeClientAuth()
|
||||||
|
} else {
|
||||||
|
addonCopy.Status.KubeClientDriver = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := c.patcher.PatchStatus(ctx, addonCopy, addonCopy.Status, addon.Status)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated {
|
||||||
|
logger.Info("Updated kubeClientDriver in status", "addon", addon.Name, "driver", addonCopy.Status.KubeClientDriver, "hasKubeClientRegistration", hasKubeClientRegistration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
// cleanup cleans both the registration configs and client certificate controllers for the addon
|
// cleanup cleans both the registration configs and client certificate controllers for the addon
|
||||||
func (c *addOnRegistrationController) cleanup(ctx context.Context, addOnName string) error {
|
func (c *addOnRegistrationController) cleanup(ctx context.Context, addOnName string) error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|||||||
@@ -14,10 +14,19 @@ import (
|
|||||||
addonfake "open-cluster-management.io/api/client/addon/clientset/versioned/fake"
|
addonfake "open-cluster-management.io/api/client/addon/clientset/versioned/fake"
|
||||||
addoninformers "open-cluster-management.io/api/client/addon/informers/externalversions"
|
addoninformers "open-cluster-management.io/api/client/addon/informers/externalversions"
|
||||||
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
|
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
|
||||||
|
"open-cluster-management.io/sdk-go/pkg/patcher"
|
||||||
|
|
||||||
testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
|
testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
registertesting "open-cluster-management.io/ocm/pkg/registration/register/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type testDriverFactory struct{}
|
||||||
|
|
||||||
|
func (f *testDriverFactory) Fork(addonName string, authConfig register.AddonAuthConfig, secretOption register.SecretOption) (register.RegisterDriver, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestRegistrationSync(t *testing.T) {
|
func TestRegistrationSync(t *testing.T) {
|
||||||
clusterName := "cluster1"
|
clusterName := "cluster1"
|
||||||
signerName := "signer1"
|
signerName := "signer1"
|
||||||
@@ -327,18 +336,43 @@ func TestRegistrationSync(t *testing.T) {
|
|||||||
managementKubeClient: managementClient,
|
managementKubeClient: managementClient,
|
||||||
spokeKubeClient: kubeClient,
|
spokeKubeClient: kubeClient,
|
||||||
hubAddOnLister: addonInformerFactory.Addon().V1alpha1().ManagedClusterAddOns().Lister(),
|
hubAddOnLister: addonInformerFactory.Addon().V1alpha1().ManagedClusterAddOns().Lister(),
|
||||||
startRegistrationFunc: func(ctx context.Context, config registrationConfig) context.CancelFunc {
|
patcher: patcher.NewPatcher[
|
||||||
|
*addonv1alpha1.ManagedClusterAddOn, addonv1alpha1.ManagedClusterAddOnSpec, addonv1alpha1.ManagedClusterAddOnStatus](
|
||||||
|
addonClient.AddonV1alpha1().ManagedClusterAddOns(clusterName)),
|
||||||
|
addonDriverFactory: &testDriverFactory{},
|
||||||
|
startRegistrationFunc: func(ctx context.Context, config registrationConfig) (context.CancelFunc, error) {
|
||||||
_, cancel := context.WithCancel(context.Background())
|
_, cancel := context.WithCancel(context.Background())
|
||||||
return cancel
|
return cancel, nil
|
||||||
},
|
},
|
||||||
addOnRegistrationConfigs: c.addOnRegistrationConfigs,
|
addOnRegistrationConfigs: c.addOnRegistrationConfigs,
|
||||||
|
addonAuthConfig: ®istertesting.TestAddonAuthConfig{
|
||||||
|
KubeClientAuth: "csr",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First sync: sets the driver and returns early (status updated)
|
||||||
err := controller.sync(context.Background(), testingcommon.NewFakeSyncContext(t, c.queueKey), c.queueKey)
|
err := controller.sync(context.Background(), testingcommon.NewFakeSyncContext(t, c.queueKey), c.queueKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Second sync: processes registrations (only if addon has registrations to process)
|
||||||
|
// The condition checks if there are registrations to test, not whether driver is set
|
||||||
|
if c.addOn != nil && len(c.addOn.Status.Registrations) > 0 {
|
||||||
|
// Update addon in store with driver set (simulating informer update)
|
||||||
|
updatedAddOn := c.addOn.DeepCopy()
|
||||||
|
updatedAddOn.Status.KubeClientDriver = "csr"
|
||||||
|
if err := addonStore.Update(updatedAddOn); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync again to process registrations
|
||||||
|
err = controller.sync(context.Background(), testingcommon.NewFakeSyncContext(t, c.queueKey), c.queueKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(c.expectedAddOnRegistrationConfigHashs) != len(controller.addOnRegistrationConfigs) {
|
if len(c.expectedAddOnRegistrationConfigHashs) != len(controller.addOnRegistrationConfigs) {
|
||||||
t.Errorf("expected %d addOns, but got %d",
|
t.Errorf("expected %d addOns, but got %d",
|
||||||
len(c.expectedAddOnRegistrationConfigHashs), len(controller.addOnRegistrationConfigs))
|
len(c.expectedAddOnRegistrationConfigHashs), len(controller.addOnRegistrationConfigs))
|
||||||
@@ -404,6 +438,6 @@ func hash(registration addonv1alpha1.RegistrationConfig, installNamespace string
|
|||||||
h, _ := getConfigHash(registration, addonInstallOption{
|
h, _ := getConfigHash(registration, addonInstallOption{
|
||||||
InstallationNamespace: installNamespace,
|
InstallationNamespace: installNamespace,
|
||||||
AgentRunningOutsideManagedCluster: addOnAgentRunningOutsideManagedCluster,
|
AgentRunningOutsideManagedCluster: addOnAgentRunningOutsideManagedCluster,
|
||||||
})
|
}, "")
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,8 +414,8 @@ func (o *SpokeAgentConfig) RunSpokeAgentWithSpokeInformers(ctx context.Context,
|
|||||||
AddOnLeaseControllerSyncInterval, //TODO: this interval time should be allowed to change from outside
|
AddOnLeaseControllerSyncInterval, //TODO: this interval time should be allowed to change from outside
|
||||||
)
|
)
|
||||||
|
|
||||||
// addon registration only enabled when the registration driver is csr.
|
// addon registration enabled when AddonDriverFactory is provided (supports CSR and token-based drivers)
|
||||||
if addonDriver, ok := o.driver.(register.AddonDriver); ok {
|
if addonDriverFactory, ok := o.driver.(register.AddonDriverFactory); ok {
|
||||||
addOnRegistrationController = addon.NewAddOnRegistrationController(
|
addOnRegistrationController = addon.NewAddOnRegistrationController(
|
||||||
o.agentOptions.SpokeClusterName,
|
o.agentOptions.SpokeClusterName,
|
||||||
o.agentOptions.AgentID,
|
o.agentOptions.AgentID,
|
||||||
@@ -423,7 +423,8 @@ func (o *SpokeAgentConfig) RunSpokeAgentWithSpokeInformers(ctx context.Context,
|
|||||||
hubClient.AddonClient,
|
hubClient.AddonClient,
|
||||||
managementKubeClient,
|
managementKubeClient,
|
||||||
spokeKubeClient,
|
spokeKubeClient,
|
||||||
addonDriver,
|
addonDriverFactory,
|
||||||
|
o.registrationOption.RegisterDriverOption,
|
||||||
hubClient.AddonInformer,
|
hubClient.AddonInformer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -475,8 +476,9 @@ func (o *SpokeAgentConfig) RunSpokeAgentWithSpokeInformers(ctx context.Context,
|
|||||||
go managedClusterHealthCheckController.Run(ctx, 1)
|
go managedClusterHealthCheckController.Run(ctx, 1)
|
||||||
if features.SpokeMutableFeatureGate.Enabled(ocmfeature.AddonManagement) {
|
if features.SpokeMutableFeatureGate.Enabled(ocmfeature.AddonManagement) {
|
||||||
go addOnLeaseController.Run(ctx, 1)
|
go addOnLeaseController.Run(ctx, 1)
|
||||||
// addon controller will only run when the registration driver is csr.
|
// addon registration controller runs when the driver implements AddonDriverFactory
|
||||||
if _, ok := o.driver.(register.AddonDriver); ok {
|
// (supports CSR, GRPC, and AWS IRSA drivers with both CSR and token-based authentication)
|
||||||
|
if _, ok := o.driver.(register.AddonDriverFactory); ok {
|
||||||
go addOnRegistrationController.Run(ctx, 1)
|
go addOnRegistrationController.Run(ctx, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,11 +237,9 @@ var _ = ginkgo.Describe("Addon Registration", func() {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
addOn.Status = addonv1alpha1.ManagedClusterAddOnStatus{
|
addOn.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
Registrations: []addonv1alpha1.RegistrationConfig{
|
{
|
||||||
{
|
SignerName: signerName,
|
||||||
SignerName: signerName,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
@@ -335,17 +333,19 @@ var _ = ginkgo.Describe("Addon Registration", func() {
|
|||||||
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.TODO(), addOn, metav1.CreateOptions{})
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.TODO(), addOn, metav1.CreateOptions{})
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
created, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
gomega.Eventually(func() error {
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
created, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
created.Status = addonv1alpha1.ManagedClusterAddOnStatus{
|
if err != nil {
|
||||||
Registrations: []addonv1alpha1.RegistrationConfig{
|
return err
|
||||||
|
}
|
||||||
|
created.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
{
|
{
|
||||||
SignerName: signerName,
|
SignerName: signerName,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), created, metav1.UpdateOptions{})
|
||||||
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), created, metav1.UpdateOptions{})
|
return err
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
assertSuccessCSRApproval()
|
assertSuccessCSRApproval()
|
||||||
|
|
||||||
@@ -392,18 +392,20 @@ var _ = ginkgo.Describe("Addon Registration", func() {
|
|||||||
assertSuccessAddOnBootstrap(signerName)
|
assertSuccessAddOnBootstrap(signerName)
|
||||||
|
|
||||||
// update registration config and change the signer
|
// update registration config and change the signer
|
||||||
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
newSignerName := "example.com/signer1"
|
newSignerName := "example.com/signer1"
|
||||||
addOn.Status = addonv1alpha1.ManagedClusterAddOnStatus{
|
gomega.Eventually(func() error {
|
||||||
Registrations: []addonv1alpha1.RegistrationConfig{
|
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
addOn.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
{
|
{
|
||||||
SignerName: newSignerName,
|
SignerName: newSignerName,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
||||||
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
return err
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
assertSecretGone(addOnName, getSecretName(addOnName, signerName))
|
assertSecretGone(addOnName, getSecretName(addOnName, signerName))
|
||||||
|
|
||||||
assertSuccessCSRApproval()
|
assertSuccessCSRApproval()
|
||||||
@@ -437,20 +439,26 @@ var _ = ginkgo.Describe("Addon Registration", func() {
|
|||||||
|
|
||||||
// update subject for 15 times
|
// update subject for 15 times
|
||||||
for i := 1; i <= 15; i++ {
|
for i := 1; i <= 15; i++ {
|
||||||
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
currentIndex := i
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
gomega.Eventually(func() error {
|
||||||
addOn.Status = addonv1alpha1.ManagedClusterAddOnStatus{
|
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
Registrations: []addonv1alpha1.RegistrationConfig{
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(addOn.Status.Registrations) == 0 {
|
||||||
|
return fmt.Errorf("no registrations found")
|
||||||
|
}
|
||||||
|
addOn.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
{
|
{
|
||||||
SignerName: addOn.Status.Registrations[0].SignerName,
|
SignerName: addOn.Status.Registrations[0].SignerName,
|
||||||
Subject: addonv1alpha1.Subject{
|
Subject: addonv1alpha1.Subject{
|
||||||
User: fmt.Sprintf("test-%d", i),
|
User: fmt.Sprintf("test-%d", currentIndex),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
||||||
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
return err
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
// sleep 1 second to ensure controller issue a new csr.
|
// sleep 1 second to ensure controller issue a new csr.
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
}
|
}
|
||||||
|
|||||||
654
test/integration/registration/addon_token_registration_test.go
Normal file
654
test/integration/registration/addon_token_registration_test.go
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
package registration_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/onsi/ginkgo/v2"
|
||||||
|
"github.com/onsi/gomega"
|
||||||
|
"github.com/openshift/library-go/pkg/controller/controllercmd"
|
||||||
|
certificates "k8s.io/api/certificates/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/rand"
|
||||||
|
|
||||||
|
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
|
||||||
|
clusterv1 "open-cluster-management.io/api/cluster/v1"
|
||||||
|
|
||||||
|
"open-cluster-management.io/ocm/pkg/addon"
|
||||||
|
commonhelpers "open-cluster-management.io/ocm/pkg/common/helpers"
|
||||||
|
commonoptions "open-cluster-management.io/ocm/pkg/common/options"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/csr"
|
||||||
|
registerfactory "open-cluster-management.io/ocm/pkg/registration/register/factory"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/register/token"
|
||||||
|
"open-cluster-management.io/ocm/pkg/registration/spoke"
|
||||||
|
"open-cluster-management.io/ocm/test/integration/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = ginkgo.Describe("Addon Token Registration", func() {
|
||||||
|
var managedClusterName, hubKubeconfigSecret, hubKubeconfigDir, addOnName string
|
||||||
|
var err error
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
var cancelAddonManager context.CancelFunc
|
||||||
|
var bootstrapKubeconfig string
|
||||||
|
var expectedProxyURL string
|
||||||
|
var signerName = certificates.KubeAPIServerClientSignerName
|
||||||
|
|
||||||
|
ginkgo.BeforeEach(func() {
|
||||||
|
// Start the addon manager which includes the tokenInfrastructureController
|
||||||
|
// This controller creates ServiceAccounts for token-based authentication
|
||||||
|
addonMgrCtx, addonMgrCancel := context.WithCancel(context.Background())
|
||||||
|
cancelAddonManager = addonMgrCancel
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer ginkgo.GinkgoRecover()
|
||||||
|
err := addon.RunManager(addonMgrCtx, &controllercmd.ControllerContext{
|
||||||
|
KubeConfig: hubCfg,
|
||||||
|
EventRecorder: util.NewIntegrationTestEventRecorder("addon-manager"),
|
||||||
|
})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.JustBeforeEach(func() {
|
||||||
|
suffix := rand.String(5)
|
||||||
|
managedClusterName = fmt.Sprintf("token-cluster-%s", suffix)
|
||||||
|
hubKubeconfigSecret = fmt.Sprintf("token-hub-kubeconfig-secret-%s", suffix)
|
||||||
|
hubKubeconfigDir = path.Join(util.TestDir, fmt.Sprintf("token-addontest-%s", suffix), "hub-kubeconfig")
|
||||||
|
addOnName = fmt.Sprintf("token-addon-%s", suffix)
|
||||||
|
|
||||||
|
// Create agent options with token authentication for addons
|
||||||
|
driverOption := registerfactory.NewOptions()
|
||||||
|
driverOption.AddonKubeClientRegistrationAuth = "token" // Use token-based authentication for addons
|
||||||
|
|
||||||
|
agentOptions := &spoke.SpokeAgentOptions{
|
||||||
|
BootstrapKubeconfig: bootstrapKubeconfig,
|
||||||
|
HubKubeconfigSecret: hubKubeconfigSecret,
|
||||||
|
ClusterHealthCheckPeriod: 1 * time.Minute,
|
||||||
|
RegisterDriverOption: driverOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
commOptions := commonoptions.NewAgentOptions()
|
||||||
|
commOptions.HubKubeconfigDir = hubKubeconfigDir
|
||||||
|
commOptions.SpokeClusterName = managedClusterName
|
||||||
|
|
||||||
|
// run registration agent
|
||||||
|
cancel = runAgent("token-addontest", agentOptions, commOptions, spokeCfg)
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.AfterEach(func() {
|
||||||
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
if cancelAddonManager != nil {
|
||||||
|
cancelAddonManager()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assertSuccessClusterBootstrap := func() {
|
||||||
|
// the spoke cluster and csr should be created after bootstrap
|
||||||
|
ginkgo.By("Check existence of ManagedCluster & CSR")
|
||||||
|
gomega.Eventually(func() error {
|
||||||
|
if _, err := util.GetManagedCluster(clusterClient, managedClusterName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
|
gomega.Eventually(func() error {
|
||||||
|
if _, err := util.FindUnapprovedSpokeCSR(kubeClient, managedClusterName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
|
// the spoke cluster should has finalizer that is added by hub controller
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
spokeCluster, err := util.GetManagedCluster(clusterClient, managedClusterName)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !commonhelpers.HasFinalizer(spokeCluster.Finalizers, clusterv1.ManagedClusterFinalizer) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
|
||||||
|
ginkgo.By("Accept and approve the ManagedCluster")
|
||||||
|
// simulate hub cluster admin to accept the managedcluster and approve the csr
|
||||||
|
err = util.AcceptManagedCluster(clusterClient, managedClusterName)
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
|
err = authn.ApproveSpokeClusterCSR(kubeClient, managedClusterName, time.Hour*24)
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
|
// the managed cluster should have accepted condition after it is accepted
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
spokeCluster, err := util.GetManagedCluster(clusterClient, managedClusterName)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
accepted := meta.FindStatusCondition(spokeCluster.Status.Conditions, clusterv1.ManagedClusterConditionHubAccepted)
|
||||||
|
return accepted != nil
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
|
||||||
|
// the hub kubeconfig secret should be filled after the csr is approved
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
if _, err := util.GetFilledHubKubeConfigSecret(kubeClient, testNamespace, hubKubeconfigSecret); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
|
||||||
|
ginkgo.By("ManagedCluster joins the hub")
|
||||||
|
// the spoke cluster should have joined condition finally
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
spokeCluster, err := util.GetManagedCluster(clusterClient, managedClusterName)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
joined := meta.FindStatusCondition(spokeCluster.Status.Conditions, clusterv1.ManagedClusterConditionJoined)
|
||||||
|
return joined != nil
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
|
||||||
|
// ensure cluster namespace is in place
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
_, err := kubeClient.CoreV1().Namespaces().Get(context.TODO(), managedClusterName, metav1.GetOptions{})
|
||||||
|
return err == nil
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertValidTokenCredential := func(secretNamespace, secretName, expectedProxyURL string) {
|
||||||
|
ginkgo.By("Check token credential in secret")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
secret, err := kubeClient.CoreV1().Secrets(secretNamespace).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Verify token exists
|
||||||
|
if _, ok := secret.Data[token.TokenFile]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Verify kubeconfig with token
|
||||||
|
kubeconfigData, ok := secret.Data[register.KubeconfigFile]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedProxyURL != "" {
|
||||||
|
proxyURL, err := getProxyURLFromKubeconfigData(kubeconfigData)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return proxyURL == expectedProxyURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertAddonLabel := func(clusterName, addonName, status string) {
|
||||||
|
ginkgo.By("Check addon status label on managed cluster")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
cluster, err := util.GetManagedCluster(clusterClient, managedClusterName)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(cluster.Labels) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("feature.open-cluster-management.io/addon-%s", addonName)
|
||||||
|
return cluster.Labels[key] == status
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTokenRefreshedCondition := func(clusterName, addonName string) {
|
||||||
|
ginkgo.By("Check token refreshed addon status condition")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
addon, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(clusterName).
|
||||||
|
Get(context.TODO(), addonName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return meta.IsStatusConditionTrue(addon.Status.Conditions, token.TokenRefreshedCondition)
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSuccessCSRApproval := func() {
|
||||||
|
ginkgo.By("Approve bootstrap csr")
|
||||||
|
var csr *certificates.CertificateSigningRequest
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
csr, err = util.FindUnapprovedAddOnCSR(kubeClient, managedClusterName, addOnName)
|
||||||
|
return err == nil
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
err = authn.ApproveCSR(kubeClient, csr, now.UTC(), now.Add(30*time.Second).UTC())
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertValidClientCertificate := func(secretNamespace, secretName, expectedProxyURL string) {
|
||||||
|
ginkgo.By("Check client certificate in secret")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
secret, err := kubeClient.CoreV1().Secrets(secretNamespace).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := secret.Data[csr.TLSKeyFile]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := secret.Data[csr.TLSCertFile]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
kubeconfigData, ok := secret.Data[register.KubeconfigFile]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedProxyURL != "" {
|
||||||
|
proxyURL, err := getProxyURLFromKubeconfigData(kubeconfigData)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return proxyURL == expectedProxyURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertHasNoAddonLabel := func(clusterName, addonName string) {
|
||||||
|
ginkgo.By("Check if addon status label on managed cluster deleted")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
cluster, err := util.GetManagedCluster(clusterClient, managedClusterName)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(cluster.Labels) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
key := fmt.Sprintf("feature.open-cluster-management.io/addon-%s", addonName)
|
||||||
|
_, ok := cluster.Labels[key]
|
||||||
|
return !ok
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertAddOnSignerUpdate := func(signerName string) {
|
||||||
|
gomega.Eventually(func() error {
|
||||||
|
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addOn.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
|
{
|
||||||
|
SignerName: signerName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSuccessAddOnEnabling := func() {
|
||||||
|
ginkgo.By("Create ManagedClusterAddOn cr for token authentication")
|
||||||
|
// create addon namespace
|
||||||
|
ns := &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: addOnName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = kubeClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
|
// create addon
|
||||||
|
addOn := &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: addOnName,
|
||||||
|
Namespace: managedClusterName,
|
||||||
|
},
|
||||||
|
Spec: addonv1alpha1.ManagedClusterAddOnSpec{
|
||||||
|
InstallNamespace: addOnName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.TODO(), addOn, metav1.CreateOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
|
_, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSuccessAddOnBootstrap := func(signerName string) {
|
||||||
|
assertSuccessAddOnEnabling()
|
||||||
|
assertAddOnSignerUpdate(signerName)
|
||||||
|
assertValidTokenCredential(addOnName, getSecretName(addOnName, signerName), expectedProxyURL)
|
||||||
|
assertAddonLabel(managedClusterName, addOnName, "unreachable")
|
||||||
|
assertTokenRefreshedCondition(managedClusterName, addOnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSecretGone := func(secretNamespace, secretName string) {
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
_, err = kubeClient.CoreV1().Secrets(secretNamespace).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
return errors.IsNotFound(err)
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertRegistrationSucceed := func() {
|
||||||
|
ginkgo.It("should register addon with token successfully", func() {
|
||||||
|
assertSuccessClusterBootstrap()
|
||||||
|
assertSuccessAddOnBootstrap(signerName)
|
||||||
|
|
||||||
|
ginkgo.By("Delete the addon and check if secret is gone")
|
||||||
|
err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Delete(context.TODO(), addOnName, metav1.DeleteOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
assertSecretGone(addOnName, getSecretName(addOnName, signerName))
|
||||||
|
|
||||||
|
assertHasNoAddonLabel(managedClusterName, addOnName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ginkgo.Context("without proxy", func() {
|
||||||
|
ginkgo.BeforeEach(func() {
|
||||||
|
bootstrapKubeconfig = bootstrapKubeConfigFile
|
||||||
|
expectedProxyURL = ""
|
||||||
|
})
|
||||||
|
assertRegistrationSucceed()
|
||||||
|
|
||||||
|
ginkgo.It("should register addon with token successfully even when the install namespace is not available at the beginning", func() {
|
||||||
|
assertSuccessClusterBootstrap()
|
||||||
|
|
||||||
|
ginkgo.By("Create ManagedClusterAddOn cr for token authentication")
|
||||||
|
|
||||||
|
// create addon
|
||||||
|
addOn := &addonv1alpha1.ManagedClusterAddOn{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: addOnName,
|
||||||
|
Namespace: managedClusterName,
|
||||||
|
},
|
||||||
|
Spec: addonv1alpha1.ManagedClusterAddOnSpec{
|
||||||
|
InstallNamespace: addOnName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.TODO(), addOn, metav1.CreateOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
|
gomega.Eventually(func() error {
|
||||||
|
created, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
created.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
|
{
|
||||||
|
SignerName: signerName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), created, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
|
ginkgo.By("Wait for addon namespace - token should not be created yet")
|
||||||
|
gomega.Consistently(func() bool {
|
||||||
|
_, err := kubeClient.CoreV1().Secrets(addOnName).Get(context.TODO(), getSecretName(addOnName, signerName), metav1.GetOptions{})
|
||||||
|
return errors.IsNotFound(err)
|
||||||
|
}, 10*time.Second, 2*time.Second).Should(gomega.BeTrue())
|
||||||
|
|
||||||
|
ginkgo.By("Create addon namespace")
|
||||||
|
// create addon namespace
|
||||||
|
ns := &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: addOnName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = kubeClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
|
assertValidTokenCredential(addOnName, getSecretName(addOnName, signerName), expectedProxyURL)
|
||||||
|
|
||||||
|
ginkgo.By("Delete the addon and check if secret is gone")
|
||||||
|
err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Delete(context.TODO(), addOnName, metav1.DeleteOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
assertSecretGone(addOnName, getSecretName(addOnName, signerName))
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should rotate token successfully", func() {
|
||||||
|
assertSuccessClusterBootstrap()
|
||||||
|
assertSuccessAddOnBootstrap(signerName)
|
||||||
|
|
||||||
|
secretName := getSecretName(addOnName, signerName)
|
||||||
|
secret, err := kubeClient.CoreV1().Secrets(addOnName).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
originalToken := secret.Data[token.TokenFile]
|
||||||
|
gomega.Expect(originalToken).NotTo(gomega.BeNil())
|
||||||
|
|
||||||
|
ginkgo.By("Trigger token rotation by deleting the ServiceAccount")
|
||||||
|
// ServiceAccount naming convention: <addon-name>-agent
|
||||||
|
serviceAccountName := fmt.Sprintf("%s-agent", addOnName)
|
||||||
|
err = kubeClient.CoreV1().ServiceAccounts(managedClusterName).Delete(context.TODO(), serviceAccountName, metav1.DeleteOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
|
ginkgo.By("Wait for ServiceAccount to be recreated")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
_, err := kubeClient.CoreV1().ServiceAccounts(managedClusterName).Get(context.TODO(), serviceAccountName, metav1.GetOptions{})
|
||||||
|
return err == nil
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
|
||||||
|
ginkgo.By("Wait for token rotation")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
newSecret, err := kubeClient.CoreV1().Secrets(addOnName).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
newToken := newSecret.Data[token.TokenFile]
|
||||||
|
if newToken == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the token has changed
|
||||||
|
return !reflect.DeepEqual(originalToken, newToken)
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should switch from token to CSR driver successfully", func() {
|
||||||
|
assertSuccessClusterBootstrap()
|
||||||
|
assertSuccessAddOnBootstrap(signerName)
|
||||||
|
|
||||||
|
secretName := getSecretName(addOnName, signerName)
|
||||||
|
|
||||||
|
ginkgo.By("Verify token-based secret exists")
|
||||||
|
secret, err := kubeClient.CoreV1().Secrets(addOnName).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
gomega.Expect(secret.Data[token.TokenFile]).NotTo(gomega.BeNil())
|
||||||
|
|
||||||
|
ginkgo.By("Stop the agent and restart with CSR driver")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Restart agent with CSR driver
|
||||||
|
driverOption := registerfactory.NewOptions()
|
||||||
|
driverOption.AddonKubeClientRegistrationAuth = "csr" // Switch to CSR-based authentication
|
||||||
|
|
||||||
|
agentOptions := &spoke.SpokeAgentOptions{
|
||||||
|
BootstrapKubeconfig: bootstrapKubeconfig,
|
||||||
|
HubKubeconfigSecret: hubKubeconfigSecret,
|
||||||
|
ClusterHealthCheckPeriod: 1 * time.Minute,
|
||||||
|
RegisterDriverOption: driverOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
commOptions := commonoptions.NewAgentOptions()
|
||||||
|
commOptions.HubKubeconfigDir = hubKubeconfigDir
|
||||||
|
commOptions.SpokeClusterName = managedClusterName
|
||||||
|
|
||||||
|
cancel = runAgent("token-to-csr-test", agentOptions, commOptions, spokeCfg)
|
||||||
|
|
||||||
|
ginkgo.By("Update addon registration with subject for CSR")
|
||||||
|
gomega.Eventually(func() error {
|
||||||
|
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addOn.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
|
{
|
||||||
|
SignerName: signerName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
|
ginkgo.By("Approve CSR and verify client certificate")
|
||||||
|
assertSuccessCSRApproval()
|
||||||
|
assertValidClientCertificate(addOnName, secretName, expectedProxyURL)
|
||||||
|
|
||||||
|
ginkgo.By("Verify CSR credentials exist in secret")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
secret, err := kubeClient.CoreV1().Secrets(addOnName).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, hasCert := secret.Data[csr.TLSCertFile]
|
||||||
|
_, hasKey := secret.Data[csr.TLSKeyFile]
|
||||||
|
// Cert and key should exist (token may remain as leftover)
|
||||||
|
return hasCert && hasKey
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should switch from CSR to token driver successfully", func() {
|
||||||
|
ginkgo.By("Start with CSR driver")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Start agent with CSR driver
|
||||||
|
driverOption := registerfactory.NewOptions()
|
||||||
|
driverOption.AddonKubeClientRegistrationAuth = "csr"
|
||||||
|
|
||||||
|
agentOptions := &spoke.SpokeAgentOptions{
|
||||||
|
BootstrapKubeconfig: bootstrapKubeconfig,
|
||||||
|
HubKubeconfigSecret: hubKubeconfigSecret,
|
||||||
|
ClusterHealthCheckPeriod: 1 * time.Minute,
|
||||||
|
RegisterDriverOption: driverOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
commOptions := commonoptions.NewAgentOptions()
|
||||||
|
commOptions.HubKubeconfigDir = hubKubeconfigDir
|
||||||
|
commOptions.SpokeClusterName = managedClusterName
|
||||||
|
|
||||||
|
cancel = runAgent("csr-to-token-test", agentOptions, commOptions, spokeCfg)
|
||||||
|
|
||||||
|
assertSuccessClusterBootstrap()
|
||||||
|
|
||||||
|
ginkgo.By("Create addon with CSR-based registration")
|
||||||
|
assertSuccessAddOnEnabling()
|
||||||
|
|
||||||
|
gomega.Eventually(func() error {
|
||||||
|
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addOn.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
|
{
|
||||||
|
SignerName: signerName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
|
secretName := getSecretName(addOnName, signerName)
|
||||||
|
|
||||||
|
ginkgo.By("Approve CSR and verify client certificate")
|
||||||
|
assertSuccessCSRApproval()
|
||||||
|
assertValidClientCertificate(addOnName, secretName, expectedProxyURL)
|
||||||
|
|
||||||
|
ginkgo.By("Verify CSR-based secret exists")
|
||||||
|
secret, err := kubeClient.CoreV1().Secrets(addOnName).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
gomega.Expect(secret.Data[csr.TLSCertFile]).NotTo(gomega.BeNil())
|
||||||
|
gomega.Expect(secret.Data[csr.TLSKeyFile]).NotTo(gomega.BeNil())
|
||||||
|
|
||||||
|
ginkgo.By("Stop the agent and restart with token driver")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Restart agent with token driver
|
||||||
|
driverOption = registerfactory.NewOptions()
|
||||||
|
driverOption.AddonKubeClientRegistrationAuth = "token"
|
||||||
|
|
||||||
|
agentOptions = &spoke.SpokeAgentOptions{
|
||||||
|
BootstrapKubeconfig: bootstrapKubeconfig,
|
||||||
|
HubKubeconfigSecret: hubKubeconfigSecret,
|
||||||
|
ClusterHealthCheckPeriod: 1 * time.Minute,
|
||||||
|
RegisterDriverOption: driverOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
commOptions = commonoptions.NewAgentOptions()
|
||||||
|
commOptions.HubKubeconfigDir = hubKubeconfigDir
|
||||||
|
commOptions.SpokeClusterName = managedClusterName
|
||||||
|
|
||||||
|
cancel = runAgent("csr-to-token-test", agentOptions, commOptions, spokeCfg)
|
||||||
|
|
||||||
|
ginkgo.By("Update addon registration to remove subject (for token)")
|
||||||
|
gomega.Eventually(func() error {
|
||||||
|
addOn, err := addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addOn.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
|
{
|
||||||
|
SignerName: signerName,
|
||||||
|
// No subject - will be set by token driver
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = addOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).
|
||||||
|
UpdateStatus(context.TODO(), addOn, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
|
ginkgo.By("Verify token credential is created")
|
||||||
|
assertValidTokenCredential(addOnName, secretName, expectedProxyURL)
|
||||||
|
assertTokenRefreshedCondition(managedClusterName, addOnName)
|
||||||
|
|
||||||
|
ginkgo.By("Verify token credentials exist in secret")
|
||||||
|
gomega.Eventually(func() bool {
|
||||||
|
secret, err := kubeClient.CoreV1().Secrets(addOnName).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, hasToken := secret.Data[token.TokenFile]
|
||||||
|
// Token should exist (cert and key may remain as leftover)
|
||||||
|
return hasToken
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.Context("with http proxy", func() {
|
||||||
|
ginkgo.BeforeEach(func() {
|
||||||
|
bootstrapKubeconfig = bootstrapKubeConfigHTTPProxyFile
|
||||||
|
expectedProxyURL = httpProxyURL
|
||||||
|
})
|
||||||
|
assertRegistrationSucceed()
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.Context("with https proxy", func() {
|
||||||
|
ginkgo.BeforeEach(func() {
|
||||||
|
bootstrapKubeconfig = bootstrapKubeConfigHTTPSProxyFile
|
||||||
|
expectedProxyURL = httpsProxyURL
|
||||||
|
})
|
||||||
|
assertRegistrationSucceed()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -272,10 +272,12 @@ var _ = ginkgo.Describe("Rebootstrap", func() {
|
|||||||
_, err = hubAddOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.TODO(), addOn, metav1.CreateOptions{})
|
_, err = hubAddOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Create(context.TODO(), addOn, metav1.CreateOptions{})
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||||
|
|
||||||
created, err := hubAddOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
gomega.Eventually(func() error {
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
created, err := hubAddOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).Get(context.TODO(), addOnName, metav1.GetOptions{})
|
||||||
created.Status = addonv1alpha1.ManagedClusterAddOnStatus{
|
if err != nil {
|
||||||
Registrations: []addonv1alpha1.RegistrationConfig{
|
return err
|
||||||
|
}
|
||||||
|
created.Status.Registrations = []addonv1alpha1.RegistrationConfig{
|
||||||
{
|
{
|
||||||
SignerName: signerName,
|
SignerName: signerName,
|
||||||
Subject: addonv1alpha1.Subject{
|
Subject: addonv1alpha1.Subject{
|
||||||
@@ -285,10 +287,10 @@ var _ = ginkgo.Describe("Rebootstrap", func() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
_, err = hubAddOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), created, metav1.UpdateOptions{})
|
||||||
_, err = hubAddOnClient.AddonV1alpha1().ManagedClusterAddOns(managedClusterName).UpdateStatus(context.TODO(), created, metav1.UpdateOptions{})
|
return err
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
assertSuccessCSRApproval(managedClusterName, addOnName, hubKubeClient)
|
assertSuccessCSRApproval(managedClusterName, addOnName, hubKubeClient)
|
||||||
assertValidClientCertificate(addOnName, getSecretName(addOnName, signerName), signerName, spokeKubeClient)
|
assertValidClientCertificate(addOnName, getSecretName(addOnName, signerName), signerName, spokeKubeClient)
|
||||||
|
|||||||
@@ -101,18 +101,22 @@ var _ = ginkgo.Describe("Agent Restart", func() {
|
|||||||
|
|
||||||
// remove the join condition. A new join condition will be added once the registration agent
|
// remove the join condition. A new join condition will be added once the registration agent
|
||||||
// is restarted successfully
|
// is restarted successfully
|
||||||
spokeCluster, err := util.GetManagedCluster(clusterClient, managedClusterName)
|
gomega.Eventually(func() error {
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
spokeCluster, err := util.GetManagedCluster(clusterClient, managedClusterName)
|
||||||
var conditions []metav1.Condition
|
if err != nil {
|
||||||
for _, condition := range spokeCluster.Status.Conditions {
|
return err
|
||||||
if condition.Type == clusterv1.ManagedClusterConditionJoined {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
conditions = append(conditions, condition)
|
var conditions []metav1.Condition
|
||||||
}
|
for _, condition := range spokeCluster.Status.Conditions {
|
||||||
spokeCluster.Status.Conditions = conditions
|
if condition.Type == clusterv1.ManagedClusterConditionJoined {
|
||||||
_, err = clusterClient.ClusterV1().ManagedClusters().UpdateStatus(context.TODO(), spokeCluster, metav1.UpdateOptions{})
|
continue
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
}
|
||||||
|
conditions = append(conditions, condition)
|
||||||
|
}
|
||||||
|
spokeCluster.Status.Conditions = conditions
|
||||||
|
_, err = clusterClient.ClusterV1().ManagedClusters().UpdateStatus(context.TODO(), spokeCluster, metav1.UpdateOptions{})
|
||||||
|
return err
|
||||||
|
}, eventuallyTimeout, eventuallyInterval).Should(gomega.Succeed())
|
||||||
|
|
||||||
ginkgo.By("Restart registration agent")
|
ginkgo.By("Restart registration agent")
|
||||||
agentOptions = &spoke.SpokeAgentOptions{
|
agentOptions = &spoke.SpokeAgentOptions{
|
||||||
|
|||||||
Reference in New Issue
Block a user