diff --git a/pkg/resourceinterpreter/customized/webhook/configmanager/accessor_test.go b/pkg/resourceinterpreter/customized/webhook/configmanager/accessor_test.go new file mode 100644 index 000000000000..ab48e491f8f5 --- /dev/null +++ b/pkg/resourceinterpreter/customized/webhook/configmanager/accessor_test.go @@ -0,0 +1,217 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmanager + +import ( + "testing" + + "github.com/stretchr/testify/assert" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + webhookutil "k8s.io/apiserver/pkg/util/webhook" + "k8s.io/utils/pointer" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" +) + +func TestNewResourceExploringAccessor(t *testing.T) { + testCases := []struct { + name string + uid string + configurationName string + webhook *configv1alpha1.ResourceInterpreterWebhook + }{ + { + name: "basic webhook accessor", + uid: "test-uid-1", + configurationName: "test-config-1", + webhook: &configv1alpha1.ResourceInterpreterWebhook{ + Name: "test-webhook", + }, + }, + { + name: "webhook accessor with empty fields", + uid: "", + configurationName: "", + webhook: &configv1alpha1.ResourceInterpreterWebhook{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + accessor := NewResourceExploringAccessor(tc.uid, tc.configurationName, tc.webhook) + + assert.NotNil(t, accessor, "accessor should not be nil") + assert.Equal(t, tc.uid, accessor.GetUID(), "uid should match") + assert.Equal(t, tc.configurationName, accessor.GetConfigurationName(), "configuration name should match") + assert.Equal(t, tc.webhook.Name, accessor.GetName(), "webhook name should match") + }) + } +} + +func TestResourceExploringAccessor_Getters(t *testing.T) { + timeoutSeconds := int32(10) + testWebhook := &configv1alpha1.ResourceInterpreterWebhook{ + Name: "test-webhook", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: pointer.String("https://test-webhook.example.com"), + Service: &admissionregistrationv1.ServiceReference{ + Name: "test-service", + Namespace: "test-namespace", + Port: pointer.Int32(443), + Path: pointer.String("/validate"), + }, + CABundle: []byte("test-ca-bundle"), + }, + Rules: []configv1alpha1.RuleWithOperations{ + { + Operations: []configv1alpha1.InterpreterOperation{"interpret"}, + Rule: configv1alpha1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Kinds: []string{"Pod"}, + }, + }, + }, + TimeoutSeconds: &timeoutSeconds, + InterpreterContextVersions: []string{"v1", "v2"}, + } + + testCases := []struct { + name string + uid string + configurationName string + webhook *configv1alpha1.ResourceInterpreterWebhook + }{ + { + name: "complete webhook configuration", + uid: "test-uid-1", + configurationName: "test-config-1", + webhook: testWebhook, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + accessor := NewResourceExploringAccessor(tc.uid, tc.configurationName, tc.webhook) + + // Test all getters + assert.Equal(t, tc.webhook.ClientConfig, accessor.GetClientConfig(), "client config should match") + assert.Equal(t, tc.webhook.Rules, accessor.GetRules(), "rules should match") + assert.Equal(t, tc.webhook.TimeoutSeconds, accessor.GetTimeoutSeconds(), "timeout seconds should match") + assert.Equal(t, tc.webhook.InterpreterContextVersions, accessor.GetInterpreterContextVersions(), "interpreter context versions should match") + }) + } +} + +func TestHookClientConfigForWebhook(t *testing.T) { + testCases := []struct { + name string + hookName string + clientConfig admissionregistrationv1.WebhookClientConfig + expected webhookutil.ClientConfig + }{ + { + name: "URL configuration", + hookName: "test-webhook", + clientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: pointer.String("https://test-webhook.example.com"), + CABundle: []byte("test-ca-bundle"), + }, + expected: webhookutil.ClientConfig{ + Name: "test-webhook", + URL: "https://test-webhook.example.com", + CABundle: []byte("test-ca-bundle"), + }, + }, + { + name: "Service configuration with defaults", + hookName: "test-webhook", + clientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: "test-service", + Namespace: "test-namespace", + }, + CABundle: []byte("test-ca-bundle"), + }, + expected: webhookutil.ClientConfig{ + Name: "test-webhook", + CABundle: []byte("test-ca-bundle"), + Service: &webhookutil.ClientConfigService{ + Name: "test-service", + Namespace: "test-namespace", + Port: 443, + }, + }, + }, + { + name: "Service configuration with custom values", + hookName: "test-webhook", + clientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: "test-service", + Namespace: "test-namespace", + Port: pointer.Int32(8443), + Path: pointer.String("/validate"), + }, + CABundle: []byte("test-ca-bundle"), + }, + expected: webhookutil.ClientConfig{ + Name: "test-webhook", + CABundle: []byte("test-ca-bundle"), + Service: &webhookutil.ClientConfigService{ + Name: "test-service", + Namespace: "test-namespace", + Port: 8443, + Path: "/validate", + }, + }, + }, + { + name: "Empty service configuration", + hookName: "test-webhook", + clientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{}, + CABundle: []byte("test-ca-bundle"), + }, + expected: webhookutil.ClientConfig{ + Name: "test-webhook", + CABundle: []byte("test-ca-bundle"), + Service: &webhookutil.ClientConfigService{ + Port: 443, + }, + }, + }, + { + name: "Nil service and URL configuration", + hookName: "test-webhook", + clientConfig: admissionregistrationv1.WebhookClientConfig{ + CABundle: []byte("test-ca-bundle"), + }, + expected: webhookutil.ClientConfig{ + Name: "test-webhook", + CABundle: []byte("test-ca-bundle"), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := hookClientConfigForWebhook(tc.hookName, tc.clientConfig) + assert.Equal(t, tc.expected, result, "webhook client config should match expected configuration") + }) + } +} diff --git a/pkg/resourceinterpreter/customized/webhook/configmanager/manager_test.go b/pkg/resourceinterpreter/customized/webhook/configmanager/manager_test.go new file mode 100644 index 000000000000..5ff52d90cf0b --- /dev/null +++ b/pkg/resourceinterpreter/customized/webhook/configmanager/manager_test.go @@ -0,0 +1,267 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package configmanager + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" + "github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager" +) + +func TestNewExploreConfigManager(t *testing.T) { + tests := []struct { + name string + initObjs []runtime.Object + }{ + { + name: "empty initial objects", + initObjs: []runtime.Object{}, + }, + { + name: "with initial configurations", + initObjs: []runtime.Object{ + &configv1alpha1.ResourceInterpreterWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: "test-config"}, + Webhooks: []configv1alpha1.ResourceInterpreterWebhook{ + {Name: "webhook1"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + informerManager := &mockInformerManager{ + lister: &mockLister{items: tt.initObjs}, + } + manager := NewExploreConfigManager(informerManager) + + assert.NotNil(t, manager, "Manager should not be nil") + assert.NotNil(t, manager.HookAccessors(), "Accessors should be initialized") + }) + } +} + +func TestHasSynced(t *testing.T) { + tests := []struct { + name string + initialSynced bool + listErr error + listResult []runtime.Object + expectedSynced bool + }{ + { + name: "already synced", + initialSynced: true, + expectedSynced: true, + }, + { + name: "not synced but empty list", + initialSynced: false, + listResult: []runtime.Object{}, + expectedSynced: true, + }, + { + name: "not synced with items", + initialSynced: false, + listResult: []runtime.Object{ + &configv1alpha1.ResourceInterpreterWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + }, + }, + expectedSynced: false, + }, + { + name: "list error", + initialSynced: false, + listErr: fmt.Errorf("test error"), + expectedSynced: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := &interpreterConfigManager{ + lister: &mockLister{ + err: tt.listErr, + items: tt.listResult, + }, + } + manager.initialSynced.Store(tt.initialSynced) + assert.Equal(t, tt.expectedSynced, manager.HasSynced()) + }) + } +} + +func TestMergeResourceExploreWebhookConfigurations(t *testing.T) { + tests := []struct { + name string + configurations []*configv1alpha1.ResourceInterpreterWebhookConfiguration + expectedLen int + }{ + { + name: "empty configurations", + configurations: []*configv1alpha1.ResourceInterpreterWebhookConfiguration{}, + expectedLen: 0, + }, + { + name: "single configuration with one webhook", + configurations: []*configv1alpha1.ResourceInterpreterWebhookConfiguration{ + { + ObjectMeta: metav1.ObjectMeta{Name: "config1"}, + Webhooks: []configv1alpha1.ResourceInterpreterWebhook{ + {Name: "webhook1"}, + }, + }, + }, + expectedLen: 1, + }, + { + name: "multiple configurations with multiple webhooks", + configurations: []*configv1alpha1.ResourceInterpreterWebhookConfiguration{ + { + ObjectMeta: metav1.ObjectMeta{Name: "config1"}, + Webhooks: []configv1alpha1.ResourceInterpreterWebhook{ + {Name: "webhook1"}, + {Name: "webhook2"}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "config2"}, + Webhooks: []configv1alpha1.ResourceInterpreterWebhook{ + {Name: "webhook3"}, + }, + }, + }, + expectedLen: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mergeResourceExploreWebhookConfigurations(tt.configurations) + assert.Equal(t, tt.expectedLen, len(result)) + + if len(result) > 1 { + for i := 1; i < len(result); i++ { + assert.True(t, result[i-1].GetUID() <= result[i].GetUID(), + "Webhooks should be sorted by UID") + } + } + }) + } +} + +func TestUpdateConfiguration(t *testing.T) { + tests := []struct { + name string + configs []runtime.Object + listErr error + expectedCount int + wantSynced bool + }{ + { + name: "empty configuration", + configs: []runtime.Object{}, + expectedCount: 0, + wantSynced: true, + }, + { + name: "valid configurations", + configs: []runtime.Object{ + &configv1alpha1.ResourceInterpreterWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: "config1"}, + Webhooks: []configv1alpha1.ResourceInterpreterWebhook{ + {Name: "webhook1"}, + }, + }, + }, + expectedCount: 1, + wantSynced: true, + }, + { + name: "list error", + configs: []runtime.Object{}, + listErr: fmt.Errorf("test error"), + expectedCount: 0, + wantSynced: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := &interpreterConfigManager{ + lister: &mockLister{ + items: tt.configs, + err: tt.listErr, + }, + } + manager.configuration.Store([]WebhookAccessor{}) + manager.initialSynced.Store(false) + + manager.updateConfiguration() + + accessors := manager.HookAccessors() + assert.Equal(t, tt.expectedCount, len(accessors)) + assert.Equal(t, tt.wantSynced, manager.HasSynced()) + }) + } +} + +// Mock Implementations + +type mockLister struct { + items []runtime.Object + err error +} + +func (m *mockLister) List(_ labels.Selector) ([]runtime.Object, error) { + if m.err != nil { + return nil, m.err + } + return m.items, nil +} + +func (m *mockLister) Get(_ string) (runtime.Object, error) { + return nil, nil +} + +func (m *mockLister) ByNamespace(_ string) cache.GenericNamespaceLister { + return nil +} + +type mockInformerManager struct { + genericmanager.SingleClusterInformerManager + lister cache.GenericLister +} + +func (m *mockInformerManager) Lister(_ schema.GroupVersionResource) cache.GenericLister { + return m.lister +} + +func (m *mockInformerManager) ForResource(_ schema.GroupVersionResource, _ cache.ResourceEventHandler) { +}