diff --git a/.github/workflows/cli.yaml b/.github/workflows/cli.yaml index 753e107f1..ebbc6bde7 100644 --- a/.github/workflows/cli.yaml +++ b/.github/workflows/cli.yaml @@ -36,7 +36,6 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: go.mod - - name: run karmadactl init test run: | export CLUSTER_VERSION=kindest/node:${{ matrix.k8s }} @@ -48,7 +47,7 @@ jobs: export KUBECONFIG=${HOME}/karmada/karmada-apiserver.config GO111MODULE=on go install github.com/onsi/ginkgo/v2/ginkgo ginkgo -v --race --trace -p --focus="[BasicPropagation] propagation testing deployment propagation testing" ./test/e2e/ - - name: export logs + - name: export logs if: always() run: | export ARTIFACTS_PATH=${{ github.workspace }}/karmadactl-test-logs/${{ matrix.k8s }}/ diff --git a/hack/verify-license.sh b/hack/verify-license.sh index 6f74c8113..38253c5b1 100755 --- a/hack/verify-license.sh +++ b/hack/verify-license.sh @@ -40,6 +40,7 @@ missing_license_header_files="$($ADDLICENSE_BIN \ -ignore "**/*.yml" \ -ignore "**/*.json" \ -ignore ".idea/**" \ + -ignore ".git/**" .)" || true if [[ "$missing_license_header_files" ]]; then diff --git a/pkg/karmadactl/cmdinit/cmdinit.go b/pkg/karmadactl/cmdinit/cmdinit.go index 730c58843..24e1eb567 100644 --- a/pkg/karmadactl/cmdinit/cmdinit.go +++ b/pkg/karmadactl/cmdinit/cmdinit.go @@ -76,7 +76,10 @@ var ( %[1]s init --karmada-apiserver-replicas 3 --etcd-replicas 3 --etcd-storage-mode PVC --storage-classes-name {StorageClassesName} # Specify external IPs(load balancer or HA IP) which used to sign the certificate - %[1]s init --cert-external-ip 10.235.1.2 --cert-external-dns www.karmada.io`) + %[1]s init --cert-external-ip 10.235.1.2 --cert-external-dns www.karmada.io + + # Install Karmada using a configuration file + %[1]s init --config /path/to/your/config/file.yaml`) ) // NewCmdInit install Karmada on Kubernetes @@ -149,6 +152,7 @@ func NewCmdInit(parentCommand string) *cobra.Command { flags.StringVar(&opts.ExternalEtcdKeyPrefix, "external-etcd-key-prefix", "", "The key prefix to be configured to kube-apiserver through --etcd-prefix.") // karmada flags.StringVar(&opts.CRDs, "crds", kubernetes.DefaultCrdURL, "Karmada crds resource.(local file e.g. --crds /root/crds.tar.gz)") + flags.StringVar(&opts.KarmadaInitFilePath, "config", "", "Karmada init file path") flags.StringVarP(&opts.KarmadaAPIServerAdvertiseAddress, "karmada-apiserver-advertise-address", "", "", "The IP address the Karmada API Server will advertise it's listening on. If not set, the address on the master node will be used.") flags.Int32VarP(&opts.KarmadaAPIServerNodePort, "port", "p", 32443, "Karmada apiserver service node port") flags.StringVarP(&opts.KarmadaDataPath, "karmada-data", "d", "/etc/karmada", "Karmada data path. kubeconfig cert and crds files") @@ -166,6 +170,7 @@ func NewCmdInit(parentCommand string) *cobra.Command { flags.StringVarP(&opts.KarmadaAggregatedAPIServerImage, "karmada-aggregated-apiserver-image", "", kubernetes.DefaultKarmadaAggregatedAPIServerImage, "Karmada aggregated apiserver image") flags.Int32VarP(&opts.KarmadaAggregatedAPIServerReplicas, "karmada-aggregated-apiserver-replicas", "", 1, "Karmada aggregated apiserver replica set") flags.IntVarP(&opts.WaitComponentReadyTimeout, "wait-component-ready-timeout", "", cmdinitoptions.WaitComponentReadyTimeout, "Wait for karmada component ready timeout. 0 means wait forever") + return cmd } diff --git a/pkg/karmadactl/cmdinit/config/config.go b/pkg/karmadactl/cmdinit/config/config.go new file mode 100644 index 000000000..00dac60a9 --- /dev/null +++ b/pkg/karmadactl/cmdinit/config/config.go @@ -0,0 +1,106 @@ +/* +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 config + +import ( + "fmt" + "os" + "sort" + + "k8s.io/apimachinery/pkg/runtime/schema" + yamlserializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/klog/v2" +) + +// LoadInitConfiguration loads the InitConfiguration from the specified file path. +// It delegates the actual loading to the loadInitConfigurationFromFile function. +func LoadInitConfiguration(cfgPath string) (*KarmadaInitConfig, error) { + var config *KarmadaInitConfig + var err error + + config, err = loadInitConfigurationFromFile(cfgPath) + + return config, err +} + +// loadInitConfigurationFromFile reads the file at the specified path and converts it into an InitConfiguration. +// It reads the file contents and then converts the bytes to an InitConfiguration. +func loadInitConfigurationFromFile(cfgPath string) (*KarmadaInitConfig, error) { + klog.V(1).Infof("loading configuration from %q", cfgPath) + + b, err := os.ReadFile(cfgPath) + if err != nil { + return nil, fmt.Errorf("unable to read config from %q: %v", cfgPath, err) + } + gvkmap, err := ParseGVKYamlMap(b) + if err != nil { + return nil, err + } + + return documentMapToInitConfiguration(gvkmap) +} + +// ParseGVKYamlMap parses a single YAML document into a map of GroupVersionKind to byte slices. +// This function is a simplified version that handles only a single YAML document. +func ParseGVKYamlMap(yamlBytes []byte) (map[schema.GroupVersionKind][]byte, error) { + gvkmap := make(map[schema.GroupVersionKind][]byte) + + gvk, err := yamlserializer.DefaultMetaFactory.Interpret(yamlBytes) + if err != nil { + return nil, fmt.Errorf("failed to interpret YAML document: %w", err) + } + if len(gvk.Group) == 0 || len(gvk.Version) == 0 || len(gvk.Kind) == 0 { + return nil, fmt.Errorf("invalid configuration for GroupVersionKind %+v: kind and apiVersion is mandatory information that must be specified", gvk) + } + gvkmap[*gvk] = yamlBytes + + return gvkmap, nil +} + +// documentMapToInitConfiguration processes a map of GroupVersionKind to byte slices to extract the InitConfiguration. +// It iterates over the map, checking for the "InitConfiguration" kind, group, and version, and unmarshals its content into an InitConfiguration object. +func documentMapToInitConfiguration(gvkmap map[schema.GroupVersionKind][]byte) (*KarmadaInitConfig, error) { + var initcfg *KarmadaInitConfig + + gvks := make([]schema.GroupVersionKind, 0, len(gvkmap)) + for gvk := range gvkmap { + gvks = append(gvks, gvk) + } + sort.Slice(gvks, func(i, j int) bool { + return gvks[i].String() < gvks[j].String() + }) + + for _, gvk := range gvks { + fileContent := gvkmap[gvk] + if gvk.Kind == "KarmadaInitConfig" { + if gvk.Group != GroupName || gvk.Version != SchemeGroupVersion.Version { + return nil, fmt.Errorf("invalid Group or Version: expected group %q and version %q, but got group %q and version %q", GroupName, SchemeGroupVersion.Version, gvk.Group, gvk.Version) + } + initcfg = &KarmadaInitConfig{} + if err := yaml.Unmarshal(fileContent, initcfg); err != nil { + return nil, err + } + } + } + + if initcfg == nil { + return nil, fmt.Errorf("no KarmadaInitConfig kind was found in the YAML file") + } + + return initcfg, nil +} diff --git a/pkg/karmadactl/cmdinit/config/config_test.go b/pkg/karmadactl/cmdinit/config/config_test.go new file mode 100644 index 000000000..adac545e0 --- /dev/null +++ b/pkg/karmadactl/cmdinit/config/config_test.go @@ -0,0 +1,426 @@ +/* +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 config + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const testConfig = ` +apiVersion: config.karmada.io/v1alpha1 +kind: KarmadaInitConfig +metadata: + name: karmada-init +spec: + certificates: + caCertFile: "/etc/karmada/pki/ca.crt" + caKeyFile: "/etc/karmada/pki/ca.key" + externalDNS: + - "localhost" + - "example.com" + externalIP: + - "192.168.1.2" + - "172.16.1.2" + validityPeriod: "8760h0m0s" + etcd: + local: + Repository: "registry.k8s.io/etcd" + Tag: "latest" + dataPath: "/var/lib/karmada-etcd" + initImage: + repository: "alpine" + tag: "3.19.1" + nodeSelectorLabels: + karmada.io/etcd: "true" + pvcSize: "5Gi" + replicas: 3 + storageClassesName: "fast" + storageMode: "PVC" + external: + endpoints: + - "https://example.com:8443" + caFile: "/path/to/your/ca.crt" + certFile: "/path/to/your/cert.crt" + keyFile: "/path/to/your/key.key" + keyPrefix: "ext-" + hostCluster: + apiEndpoint: "https://kubernetes.example.com" + kubeconfig: "/root/.kube/config" + context: "karmada-host" + domain: "cluster.local" + images: + imagePullPolicy: "IfNotPresent" + imagePullSecrets: + - "PullSecret1" + - "PullSecret2" + kubeImageMirrorCountry: "cn" + kubeImageRegistry: "registry.cn-hangzhou.aliyuncs.com/google_containers" + kubeImageTag: "v1.29.6" + privateRegistry: + registry: "my.private.registry" + components: + karmadaAPIServer: + repository: "karmada/kube-apiserver" + tag: "v1.29.6" + replicas: 1 + advertiseAddress: "192.168.1.100" + serviceType: "NodePort" + networking: + namespace: "karmada-system" + port: 32443 + karmadaAggregatedAPIServer: + repository: "karmada/karmada-aggregated-apiserver" + tag: "v0.0.0-master" + replicas: 1 + kubeControllerManager: + repository: "karmada/kube-controller-manager" + tag: "v1.29.6" + replicas: 1 + karmadaControllerManager: + repository: "karmada/karmada-controller-manager" + tag: "v0.0.0-master" + replicas: 1 + karmadaScheduler: + repository: "karmada/karmada-scheduler" + tag: "v0.0.0-master" + replicas: 1 + karmadaWebhook: + repository: "karmada/karmada-webhook" + tag: "v0.0.0-master" + replicas: 1 + karmadaDataPath: "/etc/karmada" + karmadaPKIPath: "/etc/karmada/pki" + karmadaCRDs: "https://github.com/karmada-io/karmada/releases/download/test/crds.tar.gz" + waitComponentReadyTimeout: 120 +` + +const invalidTestConfig = ` +apiVersion: v1alpha1 +kind: KarmadaInitConfig +metadata: + name: karmada-init +spec: + waitComponentReadyTimeout: "invalid-int" +` + +func TestLoadInitConfiguration(t *testing.T) { + expectedConfig := &KarmadaInitConfig{ + TypeMeta: metav1.TypeMeta{ + Kind: "KarmadaInitConfig", + APIVersion: "config.karmada.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "karmada-init", + }, + Spec: KarmadaInitSpec{ + WaitComponentReadyTimeout: 120, + KarmadaDataPath: "/etc/karmada", + KarmadaPKIPath: "/etc/karmada/pki", + KarmadaCRDs: "https://github.com/karmada-io/karmada/releases/download/test/crds.tar.gz", + Certificates: Certificates{ + CACertFile: "/etc/karmada/pki/ca.crt", + CAKeyFile: "/etc/karmada/pki/ca.key", + ExternalDNS: []string{ + "localhost", + "example.com", + }, + ExternalIP: []string{ + "192.168.1.2", + "172.16.1.2", + }, + ValidityPeriod: metav1.Duration{Duration: parseDuration("8760h")}, + }, + Etcd: Etcd{ + Local: &LocalEtcd{ + CommonSettings: CommonSettings{ + Image: Image{ + Repository: "registry.k8s.io/etcd", + Tag: "latest", + }, + Replicas: 3, + }, + InitImage: Image{ + Repository: "alpine", + Tag: "3.19.1", + }, + DataPath: "/var/lib/karmada-etcd", + PVCSize: "5Gi", + NodeSelectorLabels: map[string]string{ + "karmada.io/etcd": "true", + }, + StorageClassesName: "fast", + StorageMode: "PVC", + }, + External: &ExternalEtcd{ + Endpoints: []string{ + "https://example.com:8443", + }, + CAFile: "/path/to/your/ca.crt", + CertFile: "/path/to/your/cert.crt", + KeyFile: "/path/to/your/key.key", + KeyPrefix: "ext-", + }, + }, + HostCluster: HostCluster{ + APIEndpoint: "https://kubernetes.example.com", + Kubeconfig: "/root/.kube/config", + Context: "karmada-host", + Domain: "cluster.local", + }, + Images: Images{ + ImagePullPolicy: corev1.PullIfNotPresent, + ImagePullSecrets: []string{"PullSecret1", "PullSecret2"}, + KubeImageMirrorCountry: "cn", + KubeImageRegistry: "registry.cn-hangzhou.aliyuncs.com/google_containers", + KubeImageTag: "v1.29.6", + PrivateRegistry: &ImageRegistry{ + Registry: "my.private.registry", + }, + }, + Components: KarmadaComponents{ + KarmadaAPIServer: &KarmadaAPIServer{ + CommonSettings: CommonSettings{ + Image: Image{ + Repository: "karmada/kube-apiserver", + Tag: "v1.29.6", + }, + Replicas: 1, + }, + AdvertiseAddress: "192.168.1.100", + Networking: Networking{ + Namespace: "karmada-system", + Port: 32443, + }, + }, + KarmadaAggregatedAPIServer: &KarmadaAggregatedAPIServer{ + CommonSettings: CommonSettings{ + Image: Image{ + Repository: "karmada/karmada-aggregated-apiserver", + Tag: "v0.0.0-master", + }, + Replicas: 1, + }, + }, + KubeControllerManager: &KubeControllerManager{ + CommonSettings: CommonSettings{ + Image: Image{ + Repository: "karmada/kube-controller-manager", + Tag: "v1.29.6", + }, + Replicas: 1, + }, + }, + KarmadaControllerManager: &KarmadaControllerManager{ + CommonSettings: CommonSettings{ + Image: Image{ + Repository: "karmada/karmada-controller-manager", + Tag: "v0.0.0-master", + }, + Replicas: 1, + }, + }, + KarmadaScheduler: &KarmadaScheduler{ + CommonSettings: CommonSettings{ + Image: Image{ + Repository: "karmada/karmada-scheduler", + Tag: "v0.0.0-master", + }, + Replicas: 1, + }, + }, + KarmadaWebhook: &KarmadaWebhook{ + CommonSettings: CommonSettings{ + Image: Image{ + Repository: "karmada/karmada-webhook", + Tag: "v0.0.0-master", + }, + Replicas: 1, + }, + }, + }, + }, + } + + t.Run("Test Load Valid Configuration", func(t *testing.T) { + tmpFile, err := os.CreateTemp("", "test-config-*.yaml") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.Write([]byte(testConfig)) + assert.NoError(t, err) + err = tmpFile.Close() + assert.NoError(t, err) + + config, err := LoadInitConfiguration(tmpFile.Name()) + assert.NoError(t, err) + assert.Equal(t, expectedConfig, config) + }) + + t.Run("Test Load Invalid Configuration", func(t *testing.T) { + tmpFile, err := os.CreateTemp("", "invalid-config-*.yaml") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.Write([]byte(invalidTestConfig)) + assert.NoError(t, err) + err = tmpFile.Close() + assert.NoError(t, err) + + _, err = LoadInitConfiguration(tmpFile.Name()) + assert.Error(t, err) + }) + + t.Run("Test Load Non-Existent Configuration", func(t *testing.T) { + _, err := LoadInitConfiguration("non-existent-file.yaml") + assert.Error(t, err) + }) +} + +func TestParseGVKYamlMap(t *testing.T) { + t.Run("Test Parse Valid GVK Yaml", func(t *testing.T) { + gvkmap, err := ParseGVKYamlMap([]byte(testConfig)) + assert.NoError(t, err) + assert.NotEmpty(t, gvkmap) + + // Check if the GVK is correct + for gvk := range gvkmap { + assert.Equal(t, "config.karmada.io", gvk.Group) + assert.Equal(t, "v1alpha1", gvk.Version) + assert.Equal(t, "KarmadaInitConfig", gvk.Kind) + } + }) + + t.Run("Test Parse Invalid GVK Yaml - Incorrect Group/Version/Kind", func(t *testing.T) { + invalidGVKConfig := ` +apiVersion: invalid.group/v1beta1 +kind: InvalidKind +metadata: + name: invalid-config +spec: + key: value +` + gvkmap, err := ParseGVKYamlMap([]byte(invalidGVKConfig)) + assert.NoError(t, err, "Expected error due to invalid Group/Version/Kind") + + for gvk := range gvkmap { + assert.Equal(t, "invalid.group", gvk.Group) + assert.Equal(t, "v1beta1", gvk.Version) + assert.Equal(t, "InvalidKind", gvk.Kind) + } + }) + + t.Run("Test Parse Invalid Yaml - Bad Formatting", func(t *testing.T) { + // This YAML has invalid formatting (bad indentation) + invalidFormattedYAML := ` +apiVersion: config.karmada.io/v1alpha1 +kind: KarmadaInitConfig +metadata: + name: invalid-format +spec: + certificates: + caCertFile: /etc/karmada/pki/ca.crt + caKeyFile: /etc/karmada/pki/ca.key + externalDNS + - "localhost" +` + _, err := ParseGVKYamlMap([]byte(invalidFormattedYAML)) + assert.Error(t, err, "Expected error due to incorrect YAML formatting") + }) + + t.Run("Test Parse Empty Yaml", func(t *testing.T) { + _, err := ParseGVKYamlMap([]byte{}) + assert.Error(t, err, "Expected error due to empty YAML") + }) +} + +func TestDocumentMapToInitConfiguration(t *testing.T) { + t.Run("Test Valid GVK Map to InitConfiguration", func(t *testing.T) { + gvkmap, err := ParseGVKYamlMap([]byte(testConfig)) + assert.NoError(t, err) + + config, err := documentMapToInitConfiguration(gvkmap) + assert.NoError(t, err) + assert.NotNil(t, config) + assert.Equal(t, "KarmadaInitConfig", config.Kind) + }) + + t.Run("Test Invalid GVK Map with Missing Kind", func(t *testing.T) { + // Create a GVK map with an invalid Kind + invalidGVK := map[schema.GroupVersionKind][]byte{ + {Group: "config.karmada.io", Version: "v1alpha1", Kind: "InvalidKind"}: []byte(testConfig), + } + + _, err := documentMapToInitConfiguration(invalidGVK) + assert.Error(t, err, "Expected error due to missing KarmadaInitConfig kind") + }) + + t.Run("Test Invalid GVK with Wrong Group and Version", func(t *testing.T) { + invalidGVKConfig := ` +apiVersion: wrong.group/v0alpha1 +kind: KarmadaInitConfig +metadata: + name: invalid-config +` + gvkmap, err := ParseGVKYamlMap([]byte(invalidGVKConfig)) + assert.NoError(t, err) + + _, err = documentMapToInitConfiguration(gvkmap) + assert.Error(t, err, "Expected error due to incorrect Group or Version") + }) + + t.Run("Test Multiple GVKs with Only One KarmadaInitConfig", func(t *testing.T) { + multiGVKConfig := ` +apiVersion: config.karmada.io/v1alpha1 +kind: KarmadaInitConfig +metadata: + name: valid-config +--- +apiVersion: other.group/v1beta1 +kind: OtherConfig +metadata: + name: other-config +` + gvkmap, err := ParseGVKYamlMap([]byte(multiGVKConfig)) + assert.NoError(t, err) + + config, err := documentMapToInitConfiguration(gvkmap) + assert.NoError(t, err) + assert.NotNil(t, config) + assert.Equal(t, "KarmadaInitConfig", config.Kind) + + // Ensure the other config is ignored + assert.Len(t, gvkmap, 1, fmt.Sprintf("Expect only 1 GVKs in the map, but got %d", len(gvkmap))) + }) +} + +// parseDuration parses a duration string and returns the corresponding time.Duration value. +// If the parsing fails, it returns a duration of 0. +func parseDuration(durationStr string) time.Duration { + duration, err := time.ParseDuration(durationStr) + if err != nil { + return 0 + } + return duration +} diff --git a/pkg/karmadactl/cmdinit/config/types.go b/pkg/karmadactl/cmdinit/config/types.go new file mode 100644 index 000000000..e740f0ac6 --- /dev/null +++ b/pkg/karmadactl/cmdinit/config/types.go @@ -0,0 +1,354 @@ +/* +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 config + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name use in this package +const GroupName = "config.karmada.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +// KarmadaInitConfig defines the configuration for initializing Karmada +type KarmadaInitConfig struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + + // Spec defines the desired state for initializing Karmada + // +optional + Spec KarmadaInitSpec `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +// KarmadaInitSpec is the specification part of KarmadaInitConfig, containing all configurable options +type KarmadaInitSpec struct { + // Certificates configures the certificate information required by Karmada + // +optional + Certificates Certificates `json:"certificates,omitempty" yaml:"certificates,omitempty"` + + // Etcd configures the information of the Etcd cluster + // +optional + Etcd Etcd `json:"etcd,omitempty" yaml:"etcd,omitempty"` + + // HostCluster configures the information of the host cluster + // +optional + HostCluster HostCluster `json:"hostCluster,omitempty" yaml:"hostCluster,omitempty"` + + // Images configures image-related information + // +optional + Images Images `json:"images,omitempty" yaml:"images,omitempty"` + + // Components configures information about Karmada components + // +optional + Components KarmadaComponents `json:"components,omitempty" yaml:"components,omitempty"` + + // KarmadaCRDs configures the Karmada CRDs to be installed + // +optional + KarmadaCRDs string `json:"karmadaCRDs,omitempty" yaml:"karmadaCRDs,omitempty"` + + // KarmadaDataPath configures the data directory for Karmada + // +optional + KarmadaDataPath string `json:"karmadaDataPath,omitempty" yaml:"karmadaDataPath,omitempty"` + + // KarmadaPKIPath configures the PKI directory for Karmada + // +optional + KarmadaPKIPath string `json:"karmadaPKIPath,omitempty" yaml:"karmadaPKIPath,omitempty"` + + // WaitComponentReadyTimeout configures the timeout (in seconds) for waiting for components to be ready + // +optional + WaitComponentReadyTimeout int `json:"waitComponentReadyTimeout,omitempty" yaml:"waitComponentReadyTimeout,omitempty"` +} + +// Certificates defines the configuration related to certificates +type Certificates struct { + // CACertFile is the path to the root CA certificate file + // +optional + CACertFile string `json:"caCertFile,omitempty" yaml:"caCertFile,omitempty"` + + // CAKeyFile is the path to the root CA key file + // +optional + CAKeyFile string `json:"caKeyFile,omitempty" yaml:"caKeyFile,omitempty"` + + // ExternalDNS is the list of external DNS names for the certificate + // +optional + ExternalDNS []string `json:"externalDNS,omitempty" yaml:"externalDNS,omitempty"` + + // ExternalIP is the list of external IPs for the certificate + // +optional + ExternalIP []string `json:"externalIP,omitempty" yaml:"externalIP,omitempty"` + + // ValidityPeriod is the validity period of the certificate + // +optional + ValidityPeriod metav1.Duration `json:"validityPeriod,omitempty" yaml:"validityPeriod,omitempty"` +} + +// Etcd defines the configuration of the Etcd cluster +type Etcd struct { + // Local indicates using a local Etcd cluster + // +optional + Local *LocalEtcd `json:"local,omitempty" yaml:"local,omitempty"` + + // External indicates using an external Etcd cluster + // +optional + External *ExternalEtcd `json:"external,omitempty" yaml:"external,omitempty"` +} + +// LocalEtcd defines the configuration of a local Etcd cluster +type LocalEtcd struct { + // CommonSettings contains common settings like image and resources + CommonSettings `json:",inline" yaml:",inline"` + + // DataPath is the data storage path for Etcd + // +optional + DataPath string `json:"dataPath,omitempty" yaml:"dataPath,omitempty"` + + // InitImage is the image for the Etcd init container + // +optional + InitImage Image `json:"initImage,omitempty" yaml:"initImage,omitempty"` + + // NodeSelectorLabels are the node selector labels for the Etcd pods + // +optional + NodeSelectorLabels map[string]string `json:"nodeSelectorLabels,omitempty" yaml:"nodeSelectorLabels,omitempty"` + + // PVCSize is the size of the PersistentVolumeClaim for Etcd + // +optional + PVCSize string `json:"pvcSize,omitempty" yaml:"pvcSize,omitempty"` + + // StorageMode is the storage mode for Etcd (e.g., emptyDir, hostPath, PVC) + // +optional + StorageMode string `json:"storageMode,omitempty" yaml:"storageMode,omitempty"` + + // StorageClassesName is the name of the storage class for the Etcd PVC + // +optional + StorageClassesName string `json:"storageClassesName,omitempty" yaml:"storageClassesName,omitempty"` +} + +// ExternalEtcd defines the configuration of an external Etcd cluster +type ExternalEtcd struct { + // Endpoints are the server addresses of the external Etcd cluster + // +required + Endpoints []string `json:"endpoints" yaml:"endpoints"` + + // CAFile is the path to the CA certificate for the external Etcd cluster + // +optional + CAFile string `json:"caFile,omitempty" yaml:"caFile,omitempty"` + + // CertFile is the path to the client certificate for the external Etcd cluster + // +optional + CertFile string `json:"certFile,omitempty" yaml:"certFile,omitempty"` + + // KeyFile is the path to the client key for the external Etcd cluster + // +optional + KeyFile string `json:"keyFile,omitempty" yaml:"keyFile,omitempty"` + + // KeyPrefix is the key prefix used in the external Etcd cluster + // +optional + KeyPrefix string `json:"keyPrefix,omitempty" yaml:"keyPrefix,omitempty"` +} + +// HostCluster defines the configuration of the host cluster +type HostCluster struct { + // APIEndpoint is the API server address of the host cluster + // +optional + APIEndpoint string `json:"apiEndpoint,omitempty" yaml:"apiEndpoint,omitempty"` + + // Kubeconfig is the path to the kubeconfig file for the host cluster + // +optional + Kubeconfig string `json:"kubeconfig,omitempty" yaml:"kubeconfig,omitempty"` + + // Context is the context name in the kubeconfig for the host cluster + // +optional + Context string `json:"context,omitempty" yaml:"context,omitempty"` + + // Domain is the domain name of the host cluster + // +optional + Domain string `json:"domain,omitempty" yaml:"domain,omitempty"` + + // SecretRef refers to the credentials needed to access the host cluster + // +optional + SecretRef *LocalSecretReference `json:"secretRef,omitempty" yaml:"secretRef,omitempty"` +} + +// Images defines the configuration related to images +type Images struct { + // ImagePullPolicy is the pull policy for images + // +optional + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty" yaml:"imagePullPolicy,omitempty"` + + // ImagePullSecrets are the secrets used for pulling images + // +optional + ImagePullSecrets []string `json:"imagePullSecrets,omitempty" yaml:"imagePullSecrets,omitempty"` + + // KubeImageMirrorCountry is the country code for the Kubernetes image mirror + // +optional + KubeImageMirrorCountry string `json:"kubeImageMirrorCountry,omitempty" yaml:"kubeImageMirrorCountry,omitempty"` + + // KubeImageRegistry is the registry for Kubernetes images + // +optional + KubeImageRegistry string `json:"kubeImageRegistry,omitempty" yaml:"kubeImageRegistry,omitempty"` + + // KubeImageTag is the tag for Kubernetes images + // +optional + KubeImageTag string `json:"kubeImageTag,omitempty" yaml:"kubeImageTag,omitempty"` + + // PrivateRegistry is the private image registry + // +optional + PrivateRegistry *ImageRegistry `json:"privateRegistry,omitempty" yaml:"privateRegistry,omitempty"` +} + +// KarmadaComponents defines the configuration for all Karmada components +type KarmadaComponents struct { + // KarmadaAPIServer is the configuration for the Karmada API Server + // +optional + KarmadaAPIServer *KarmadaAPIServer `json:"karmadaAPIServer,omitempty" yaml:"karmadaAPIServer,omitempty"` + + // KarmadaAggregatedAPIServer is the configuration for the Karmada Aggregated API Server + // +optional + KarmadaAggregatedAPIServer *KarmadaAggregatedAPIServer `json:"karmadaAggregatedAPIServer,omitempty" yaml:"karmadaAggregatedAPIServer,omitempty"` + + // KubeControllerManager is the configuration for the Kube Controller Manager + // +optional + KubeControllerManager *KubeControllerManager `json:"kubeControllerManager,omitempty" yaml:"kubeControllerManager,omitempty"` + + // KarmadaControllerManager is the configuration for the Karmada Controller Manager + // +optional + KarmadaControllerManager *KarmadaControllerManager `json:"karmadaControllerManager,omitempty" yaml:"karmadaControllerManager,omitempty"` + + // KarmadaScheduler is the configuration for the Karmada Scheduler + // +optional + KarmadaScheduler *KarmadaScheduler `json:"karmadaScheduler,omitempty" yaml:"karmadaScheduler,omitempty"` + + // KarmadaWebhook is the configuration for the Karmada Webhook + // +optional + KarmadaWebhook *KarmadaWebhook `json:"karmadaWebhook,omitempty" yaml:"karmadaWebhook,omitempty"` +} + +// Networking defines network-related configuration +type Networking struct { + // Namespace is the Kubernetes namespace where Karmada is deployed + // +optional + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + + // Port is the port number for the Karmada API Server + // +optional + Port int32 `json:"port,omitempty" yaml:"port,omitempty"` +} + +// CommonSettings defines common settings for components +type CommonSettings struct { + // Image specifies the image to use for the component + Image `json:",inline" yaml:",inline"` + + // Replicas is the number of replicas for the component + // +optional + Replicas int32 `json:"replicas,omitempty" yaml:"replicas,omitempty"` + + // Resources defines resource requests and limits for the component + // +optional + Resources corev1.ResourceRequirements `json:"resources,omitempty" yaml:"resources,omitempty"` + + // NodeSelector defines node selection constraints + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty" yaml:"nodeSelector,omitempty"` + + // Tolerations define pod tolerations + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty" yaml:"tolerations,omitempty"` + + // Affinity defines pod affinity rules + // +optional + Affinity *corev1.Affinity `json:"affinity,omitempty" yaml:"affinity,omitempty"` +} + +// Image defines image information +type Image struct { + // Repository is the repository for the image + // +optional + Repository string `json:"repository,omitempty" yaml:"repository,omitempty"` + + // Tag is the tag for the image + // +optional + Tag string `json:"tag,omitempty" yaml:"tag,omitempty"` +} + +// KarmadaAPIServer defines the configuration for the Karmada API Server +type KarmadaAPIServer struct { + CommonSettings `json:",inline" yaml:",inline"` + + // AdvertiseAddress is the address advertised by the API server + // +optional + AdvertiseAddress string `json:"advertiseAddress,omitempty" yaml:"advertiseAddress,omitempty"` + + // Networking configures network-related information + // +optional + Networking Networking `json:"networking,omitempty" yaml:"networking,omitempty"` + + // ServiceAnnotations are annotations added to the API server service + // +optional + ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty" yaml:"serviceAnnotations,omitempty"` +} + +// KarmadaAggregatedAPIServer defines the configuration for the Karmada Aggregated API Server +type KarmadaAggregatedAPIServer struct { + CommonSettings `json:",inline" yaml:",inline"` +} + +// KubeControllerManager defines the configuration for the Kube Controller Manager +type KubeControllerManager struct { + CommonSettings `json:",inline" yaml:",inline"` +} + +// KarmadaControllerManager defines the configuration for the Karmada Controller Manager +type KarmadaControllerManager struct { + CommonSettings `json:",inline" yaml:",inline"` +} + +// KarmadaScheduler defines the configuration for the Karmada Scheduler +type KarmadaScheduler struct { + CommonSettings `json:",inline" yaml:",inline"` +} + +// KarmadaWebhook defines the configuration for the Karmada Webhook +type KarmadaWebhook struct { + CommonSettings `json:",inline" yaml:",inline"` +} + +// LocalSecretReference is a reference to a secret within the same namespace +type LocalSecretReference struct { + // Name is the name of the referenced secret + Name string `json:"name,omitempty" yaml:"name,omitempty"` +} + +// ImageRegistry represents an image registry +type ImageRegistry struct { + // Registry is the hostname of the image registry + // +required + Registry string `json:"registry" yaml:"registry"` +} + +// GetImage generates the full image string in the format "Repository:Tag" +// by combining the image repository and tag fields. +func (i *Image) GetImage() string { + if i.Tag == "" || i.Repository == "" { + return "" + } + return i.Repository + ":" + i.Tag +} diff --git a/pkg/karmadactl/cmdinit/kubernetes/deploy.go b/pkg/karmadactl/cmdinit/kubernetes/deploy.go index 46ad340b5..cad1a9fce 100644 --- a/pkg/karmadactl/cmdinit/kubernetes/deploy.go +++ b/pkg/karmadactl/cmdinit/kubernetes/deploy.go @@ -36,6 +36,7 @@ import ( netutils "k8s.io/utils/net" "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit/cert" + initConfig "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit/config" "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit/karmada" "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit/options" "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit/utils" @@ -174,6 +175,7 @@ type CommandInitOption struct { WaitComponentReadyTimeout int CaCertFile string CaKeyFile string + KarmadaInitFilePath string } func (i *CommandInitOption) validateLocalEtcd(parentCommand string) error { @@ -219,6 +221,16 @@ func (i *CommandInitOption) isExternalEtcdProvided() bool { // Validate Check that there are enough flags to run the command. func (i *CommandInitOption) Validate(parentCommand string) error { + if i.KarmadaInitFilePath != "" { + cfg, err := initConfig.LoadInitConfiguration(i.KarmadaInitFilePath) + if err != nil { + return fmt.Errorf("failed to load karmada init configuration: %v", err) + } + if err := i.parseInitConfig(cfg); err != nil { + return fmt.Errorf("failed to parse karmada init configuration: %v", err) + } + } + if i.KarmadaAPIServerAdvertiseAddress != "" { if netutils.ParseIPSloppy(i.KarmadaAPIServerAdvertiseAddress) == nil { return fmt.Errorf("karmada apiserver advertise address is not valid") @@ -276,8 +288,11 @@ func (i *CommandInitOption) Complete() error { } if !i.isExternalEtcdProvided() && i.EtcdStorageMode == "hostPath" && i.EtcdNodeSelectorLabels != "" { - if !i.isNodeExist(i.EtcdNodeSelectorLabels) { - return fmt.Errorf("no node found by label %s", i.EtcdNodeSelectorLabels) + labels := strings.Split(i.EtcdNodeSelectorLabels, ",") + for _, label := range labels { + if !i.isNodeExist(label) { + return fmt.Errorf("no node found by label %s", label) + } } } return initializeDirectory(i.KarmadaDataPath) @@ -744,3 +759,225 @@ func generateServerURL(serverIP string, nodePort int32) (string, error) { func SupportedStorageMode() []string { return []string{etcdStorageModeEmptyDir, etcdStorageModeHostPath, etcdStorageModePVC} } + +// parseEtcdNodeSelectorLabelsMap parse etcd node selector labels +func (i *CommandInitOption) parseEtcdNodeSelectorLabelsMap() error { + if i.EtcdNodeSelectorLabels == "" { + return nil + } + // Parse the label selector string into a LabelSelector object + selector, err := metav1.ParseToLabelSelector(i.EtcdNodeSelectorLabels) + if err != nil { + return fmt.Errorf("the etcdNodeSelector format is incorrect: %s", err) + } + // Convert the LabelSelector object into a map[string]string + labelMap, err := metav1.LabelSelectorAsMap(selector) + if err != nil { + return fmt.Errorf("failed to convert etcdNodeSelector labels to map: %v", err) + } + i.EtcdNodeSelectorLabelsMap = labelMap + return nil +} + +// parseInitConfig parses fields from KarmadaInitConfig into CommandInitOption. +// It is responsible for delegating the parsing of various configuration sections, +// such as certificates, etcd, and control plane components. +func (i *CommandInitOption) parseInitConfig(cfg *initConfig.KarmadaInitConfig) error { + spec := cfg.Spec + + i.parseGeneralConfig(spec) + i.parseCertificateConfig(spec.Certificates) + i.parseEtcdConfig(spec.Etcd) + i.parseControlPlaneConfig(spec.Components) + + setIfNotEmpty(&i.KarmadaDataPath, spec.KarmadaDataPath) + setIfNotEmpty(&i.KarmadaPkiPath, spec.KarmadaPKIPath) + setIfNotEmpty(&i.HostClusterDomain, spec.HostCluster.Domain) + setIfNotEmpty(&i.CRDs, spec.KarmadaCRDs) + + return nil +} + +// parseGeneralConfig parses basic configuration related to the host cluster, +// such as namespace, kubeconfig, and image settings from the KarmadaInitConfigSpec. +func (i *CommandInitOption) parseGeneralConfig(spec initConfig.KarmadaInitSpec) { + setIfNotEmpty(&i.KubeConfig, spec.HostCluster.Kubeconfig) + setIfNotEmpty(&i.KubeImageTag, spec.Images.KubeImageTag) + setIfNotEmpty(&i.KubeImageRegistry, spec.Images.KubeImageRegistry) + setIfNotEmpty(&i.KubeImageMirrorCountry, spec.Images.KubeImageMirrorCountry) + + if spec.Images.PrivateRegistry != nil { + setIfNotEmpty(&i.ImageRegistry, spec.Images.PrivateRegistry.Registry) + } + setIfNotEmpty(&i.ImagePullPolicy, string(spec.Images.ImagePullPolicy)) + setIfNotEmpty(&i.Context, spec.HostCluster.Context) + + if len(spec.Images.ImagePullSecrets) != 0 { + i.PullSecrets = spec.Images.ImagePullSecrets + } + setIfNotZero(&i.WaitComponentReadyTimeout, spec.WaitComponentReadyTimeout) +} + +// parseCertificateConfig parses certificate-related configuration, including CA files, +// external DNS, and external IP from the Certificates configuration block. +func (i *CommandInitOption) parseCertificateConfig(certificates initConfig.Certificates) { + setIfNotEmpty(&i.CaKeyFile, certificates.CAKeyFile) + setIfNotEmpty(&i.CaCertFile, certificates.CACertFile) + + if len(certificates.ExternalDNS) > 0 { + i.ExternalDNS = joinStringSlice(certificates.ExternalDNS) + } + + if len(certificates.ExternalIP) > 0 { + i.ExternalIP = joinStringSlice(certificates.ExternalIP) + } + + if certificates.ValidityPeriod.Duration != 0 { + i.CertValidity = certificates.ValidityPeriod.Duration + } +} + +// parseEtcdConfig handles the parsing of both local and external Etcd configurations. +func (i *CommandInitOption) parseEtcdConfig(etcd initConfig.Etcd) { + if etcd.Local != nil { + i.parseLocalEtcdConfig(etcd.Local) + } else if etcd.External != nil { + i.parseExternalEtcdConfig(etcd.External) + } +} + +// parseLocalEtcdConfig parses the local Etcd settings, including image information, +// data path, PVC size, and node selector labels. +func (i *CommandInitOption) parseLocalEtcdConfig(localEtcd *initConfig.LocalEtcd) { + setIfNotEmpty(&i.EtcdImage, localEtcd.CommonSettings.Image.GetImage()) + setIfNotEmpty(&i.EtcdInitImage, localEtcd.InitImage.GetImage()) + setIfNotEmpty(&i.EtcdHostDataPath, localEtcd.DataPath) + setIfNotEmpty(&i.EtcdPersistentVolumeSize, localEtcd.PVCSize) + + if len(localEtcd.NodeSelectorLabels) != 0 { + i.EtcdNodeSelectorLabels = mapToString(localEtcd.NodeSelectorLabels) + } + + setIfNotEmpty(&i.EtcdStorageMode, localEtcd.StorageMode) + setIfNotEmpty(&i.StorageClassesName, localEtcd.StorageClassesName) + setIfNotZeroInt32(&i.EtcdReplicas, localEtcd.Replicas) +} + +// parseExternalEtcdConfig parses the external Etcd configuration, including CA file, +// client certificates, and endpoints. +func (i *CommandInitOption) parseExternalEtcdConfig(externalEtcd *initConfig.ExternalEtcd) { + setIfNotEmpty(&i.ExternalEtcdCACertPath, externalEtcd.CAFile) + setIfNotEmpty(&i.ExternalEtcdClientCertPath, externalEtcd.CertFile) + setIfNotEmpty(&i.ExternalEtcdClientKeyPath, externalEtcd.KeyFile) + + if len(externalEtcd.Endpoints) > 0 { + i.ExternalEtcdServers = strings.Join(externalEtcd.Endpoints, ",") + } + setIfNotEmpty(&i.ExternalEtcdKeyPrefix, externalEtcd.KeyPrefix) +} + +// parseControlPlaneConfig parses the configuration for various control plane components, +// including API Server, Controller Manager, Scheduler, and Webhook. +func (i *CommandInitOption) parseControlPlaneConfig(components initConfig.KarmadaComponents) { + i.parseKarmadaAPIServerConfig(components.KarmadaAPIServer) + i.parseKarmadaControllerManagerConfig(components.KarmadaControllerManager) + i.parseKarmadaSchedulerConfig(components.KarmadaScheduler) + i.parseKarmadaWebhookConfig(components.KarmadaWebhook) + i.parseKarmadaAggregatedAPIServerConfig(components.KarmadaAggregatedAPIServer) + i.parseKubeControllerManagerConfig(components.KubeControllerManager) +} + +// parseKarmadaAPIServerConfig parses the configuration for the Karmada API Server component, +// including image and replica settings, as well as advertise address. +func (i *CommandInitOption) parseKarmadaAPIServerConfig(apiServer *initConfig.KarmadaAPIServer) { + if apiServer != nil { + setIfNotZeroInt32(&i.KarmadaAPIServerNodePort, apiServer.Networking.Port) + setIfNotEmpty(&i.Namespace, apiServer.Networking.Namespace) + setIfNotEmpty(&i.KarmadaAPIServerImage, apiServer.CommonSettings.Image.GetImage()) + setIfNotZeroInt32(&i.KarmadaAPIServerReplicas, apiServer.CommonSettings.Replicas) + setIfNotEmpty(&i.KarmadaAPIServerAdvertiseAddress, apiServer.AdvertiseAddress) + } +} + +// parseKarmadaControllerManagerConfig parses the configuration for the Karmada Controller Manager, +// including image and replica settings. +func (i *CommandInitOption) parseKarmadaControllerManagerConfig(manager *initConfig.KarmadaControllerManager) { + if manager != nil { + setIfNotEmpty(&i.KarmadaControllerManagerImage, manager.CommonSettings.Image.GetImage()) + setIfNotZeroInt32(&i.KarmadaControllerManagerReplicas, manager.CommonSettings.Replicas) + } +} + +// parseKarmadaSchedulerConfig parses the configuration for the Karmada Scheduler, +// including image and replica settings. +func (i *CommandInitOption) parseKarmadaSchedulerConfig(scheduler *initConfig.KarmadaScheduler) { + if scheduler != nil { + setIfNotEmpty(&i.KarmadaSchedulerImage, scheduler.CommonSettings.Image.GetImage()) + setIfNotZeroInt32(&i.KarmadaSchedulerReplicas, scheduler.CommonSettings.Replicas) + } +} + +// parseKarmadaWebhookConfig parses the configuration for the Karmada Webhook, +// including image and replica settings. +func (i *CommandInitOption) parseKarmadaWebhookConfig(webhook *initConfig.KarmadaWebhook) { + if webhook != nil { + setIfNotEmpty(&i.KarmadaWebhookImage, webhook.CommonSettings.Image.GetImage()) + setIfNotZeroInt32(&i.KarmadaWebhookReplicas, webhook.CommonSettings.Replicas) + } +} + +// parseKarmadaAggregatedAPIServerConfig parses the configuration for the Karmada Aggregated API Server, +// including image and replica settings. +func (i *CommandInitOption) parseKarmadaAggregatedAPIServerConfig(aggregatedAPIServer *initConfig.KarmadaAggregatedAPIServer) { + if aggregatedAPIServer != nil { + setIfNotEmpty(&i.KarmadaAggregatedAPIServerImage, aggregatedAPIServer.CommonSettings.Image.GetImage()) + setIfNotZeroInt32(&i.KarmadaAggregatedAPIServerReplicas, aggregatedAPIServer.CommonSettings.Replicas) + } +} + +// parseKubeControllerManagerConfig parses the configuration for the Kube Controller Manager, +// including image and replica settings. +func (i *CommandInitOption) parseKubeControllerManagerConfig(manager *initConfig.KubeControllerManager) { + if manager != nil { + setIfNotEmpty(&i.KubeControllerManagerImage, manager.CommonSettings.Image.GetImage()) + setIfNotZeroInt32(&i.KubeControllerManagerReplicas, manager.CommonSettings.Replicas) + } +} + +// mapToString converts a map to a comma-separated key=value string. +func mapToString(m map[string]string) string { + var builder strings.Builder + for k, v := range m { + if builder.Len() > 0 { + builder.WriteString(",") + } + builder.WriteString(fmt.Sprintf("%s=%s", k, v)) + } + return builder.String() +} + +// setIfNotEmpty checks if the source string is not empty, and if so, assigns its value to the destination string. +func setIfNotEmpty(dest *string, src string) { + if src != "" { + *dest = src + } +} + +// setIfNotZero checks if the source integer is not zero, and if so, assigns its value to the destination integer. +func setIfNotZero(dest *int, src int) { + if src != 0 { + *dest = src + } +} + +// setIfNotZeroInt32 checks if the source int32 is not zero, and if so, assigns its value to the destination int32. +func setIfNotZeroInt32(dest *int32, src int32) { + if src != 0 { + *dest = src + } +} + +// joinStringSlice joins a slice of strings into a single string separated by commas. +func joinStringSlice(slice []string) string { + return strings.Join(slice, ",") +} diff --git a/pkg/karmadactl/cmdinit/kubernetes/deploy_test.go b/pkg/karmadactl/cmdinit/kubernetes/deploy_test.go index 5523a1f47..dba8ccb15 100644 --- a/pkg/karmadactl/cmdinit/kubernetes/deploy_test.go +++ b/pkg/karmadactl/cmdinit/kubernetes/deploy_test.go @@ -20,13 +20,16 @@ import ( "context" "net" "os" + "reflect" "testing" "time" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" + "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit/config" "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit/utils" ) @@ -496,3 +499,237 @@ func TestKarmadaSchedulerImage(t *testing.T) { }) } } + +func TestCommandInitOption_parseEtcdNodeSelectorLabelsMap(t *testing.T) { + tests := []struct { + name string + opt CommandInitOption + wantErr bool + expected map[string]string + }{ + { + name: "Valid labels", + opt: CommandInitOption{ + EtcdNodeSelectorLabels: "kubernetes.io/os=linux,hello=world", + }, + wantErr: false, + expected: map[string]string{ + "kubernetes.io/os": "linux", + "hello": "world", + }, + }, + { + name: "Invalid labels without equal sign", + opt: CommandInitOption{ + EtcdNodeSelectorLabels: "invalidlabel", + }, + wantErr: true, + expected: nil, + }, + { + name: "Labels with extra spaces", + opt: CommandInitOption{ + EtcdNodeSelectorLabels: " key1 = value1 , key2=value2 ", + }, + wantErr: false, + expected: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.opt.parseEtcdNodeSelectorLabelsMap() + if (err != nil) != tt.wantErr { + t.Errorf("parseEtcdNodeSelectorLabelsMap() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(tt.opt.EtcdNodeSelectorLabelsMap, tt.expected) { + t.Errorf("parseEtcdNodeSelectorLabelsMap() = %v, want %v", tt.opt.EtcdNodeSelectorLabelsMap, tt.expected) + } + }) + } +} + +func TestParseInitConfig(t *testing.T) { + cfg := &config.KarmadaInitConfig{ + Spec: config.KarmadaInitSpec{ + WaitComponentReadyTimeout: 200, + KarmadaDataPath: "/etc/karmada", + KarmadaPKIPath: "/etc/karmada/pki", + KarmadaCRDs: "https://github.com/karmada-io/karmada/releases/download/test/crds.tar.gz", + Certificates: config.Certificates{ + CACertFile: "/path/to/ca.crt", + CAKeyFile: "/path/to/ca.key", + ExternalDNS: []string{"dns1", "dns2"}, + ExternalIP: []string{"1.2.3.4", "5.6.7.8"}, + ValidityPeriod: metav1.Duration{Duration: parseDuration("8760h")}, + }, + Etcd: config.Etcd{ + Local: &config.LocalEtcd{ + CommonSettings: config.CommonSettings{ + Image: config.Image{ + Repository: "etcd-image", + Tag: "latest", + }, + Replicas: 3, + }, + InitImage: config.Image{ + Repository: "init-image", + Tag: "latest", + }, + DataPath: "/data/dir", + PVCSize: "5Gi", + NodeSelectorLabels: map[string]string{ + "key": "value", + }, + StorageClassesName: "fast", + StorageMode: "PVC", + }, + External: &config.ExternalEtcd{ + CAFile: "/etc/ssl/certs/ca-certificates.crt", + CertFile: "/path/to/certificate.pem", + KeyFile: "/path/to/privatekey.pem", + Endpoints: []string{"https://example.com:8443"}, + KeyPrefix: "ext-", + }, + }, + HostCluster: config.HostCluster{ + Kubeconfig: "/path/to/kubeconfig", + Context: "test-context", + Domain: "cluster.local", + }, + Images: config.Images{ + KubeImageTag: "v1.21.0", + KubeImageRegistry: "registry", + KubeImageMirrorCountry: "cn", + ImagePullPolicy: corev1.PullIfNotPresent, + ImagePullSecrets: []string{"secret1", "secret2"}, + PrivateRegistry: &config.ImageRegistry{ + Registry: "test-registry", + }, + }, + Components: config.KarmadaComponents{ + KarmadaAPIServer: &config.KarmadaAPIServer{ + CommonSettings: config.CommonSettings{ + Image: config.Image{ + Repository: "apiserver-image", + Tag: "latest", + }, + Replicas: 2, + }, + AdvertiseAddress: "192.168.1.1", + Networking: config.Networking{ + Namespace: "test-namespace", + Port: 32443, + }, + }, + KarmadaControllerManager: &config.KarmadaControllerManager{ + CommonSettings: config.CommonSettings{ + Image: config.Image{ + Repository: "controller-manager-image", + Tag: "latest", + }, + Replicas: 2, + }, + }, + KarmadaScheduler: &config.KarmadaScheduler{ + CommonSettings: config.CommonSettings{ + Image: config.Image{ + Repository: "scheduler-image", + Tag: "latest", + }, + Replicas: 2, + }, + }, + KarmadaWebhook: &config.KarmadaWebhook{ + CommonSettings: config.CommonSettings{ + Image: config.Image{ + Repository: "webhook-image", + Tag: "latest", + }, + Replicas: 2, + }, + }, + KarmadaAggregatedAPIServer: &config.KarmadaAggregatedAPIServer{ + CommonSettings: config.CommonSettings{ + Image: config.Image{ + Repository: "aggregated-apiserver-image", + Tag: "latest", + }, + Replicas: 2, + }, + }, + KubeControllerManager: &config.KubeControllerManager{ + CommonSettings: config.CommonSettings{ + Image: config.Image{ + Repository: "kube-controller-manager-image", + Tag: "latest", + }, + Replicas: 2, + }, + }, + }, + }, + } + + opt := &CommandInitOption{} + err := opt.parseInitConfig(cfg) + assert.NoError(t, err) + assert.Equal(t, "test-namespace", opt.Namespace) + assert.Equal(t, "/path/to/kubeconfig", opt.KubeConfig) + assert.Equal(t, "test-registry", opt.ImageRegistry) + assert.Equal(t, 200, opt.WaitComponentReadyTimeout) + assert.Equal(t, "dns1,dns2", opt.ExternalDNS) + assert.Equal(t, "1.2.3.4,5.6.7.8", opt.ExternalIP) + assert.Equal(t, parseDuration("8760h"), opt.CertValidity) + assert.Equal(t, "etcd-image:latest", opt.EtcdImage) + assert.Equal(t, "init-image:latest", opt.EtcdInitImage) + assert.Equal(t, "/data/dir", opt.EtcdHostDataPath) + assert.Equal(t, "5Gi", opt.EtcdPersistentVolumeSize) + assert.Equal(t, "key=value", opt.EtcdNodeSelectorLabels) + assert.Equal(t, "fast", opt.StorageClassesName) + assert.Equal(t, "PVC", opt.EtcdStorageMode) + assert.Equal(t, int32(3), opt.EtcdReplicas) + assert.Equal(t, "apiserver-image:latest", opt.KarmadaAPIServerImage) + assert.Equal(t, "192.168.1.1", opt.KarmadaAPIServerAdvertiseAddress) + assert.Equal(t, int32(2), opt.KarmadaAPIServerReplicas) + assert.Equal(t, "registry", opt.KubeImageRegistry) + assert.Equal(t, "cn", opt.KubeImageMirrorCountry) + assert.Equal(t, "IfNotPresent", opt.ImagePullPolicy) + assert.Equal(t, []string{"secret1", "secret2"}, opt.PullSecrets) + assert.Equal(t, "https://github.com/karmada-io/karmada/releases/download/test/crds.tar.gz", opt.CRDs) +} + +func TestParseInitConfig_MissingFields(t *testing.T) { + cfg := &config.KarmadaInitConfig{ + Spec: config.KarmadaInitSpec{ + Components: config.KarmadaComponents{ + KarmadaAPIServer: &config.KarmadaAPIServer{ + Networking: config.Networking{ + Namespace: "test-namespace", + }, + }, + }, + }, + } + + opt := &CommandInitOption{} + err := opt.parseInitConfig(cfg) + assert.NoError(t, err) + assert.Equal(t, "test-namespace", opt.Namespace) + assert.Empty(t, opt.KubeConfig) + assert.Empty(t, opt.KubeImageTag) +} + +// parseDuration parses a duration string and returns the corresponding time.Duration value. +// If the parsing fails, it returns a duration of 0. +func parseDuration(durationStr string) time.Duration { + duration, err := time.ParseDuration(durationStr) + if err != nil { + return 0 + } + return duration +} diff --git a/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/meta.go b/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/meta.go new file mode 100644 index 000000000..407a7419a --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/meta.go @@ -0,0 +1,50 @@ +/* +Copyright 2019 The Kubernetes 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 yaml + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/yaml" +) + +// DefaultMetaFactory is a default factory for versioning objects in JSON or +// YAML. The object in memory and in the default serialization will use the +// "kind" and "apiVersion" fields. +var DefaultMetaFactory = SimpleMetaFactory{} + +// SimpleMetaFactory provides default methods for retrieving the type and version of objects +// that are identified with an "apiVersion" and "kind" fields in their JSON +// serialization. It may be parameterized with the names of the fields in memory, or an +// optional list of base structs to search for those fields in memory. +type SimpleMetaFactory struct{} + +// Interpret will return the APIVersion and Kind of the JSON wire-format +// encoding of an object, or an error. +func (SimpleMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind, error) { + gvk := runtime.TypeMeta{} + if err := yaml.Unmarshal(data, &gvk); err != nil { + return nil, fmt.Errorf("could not interpret GroupVersionKind; unmarshal error: %v", err) + } + gv, err := schema.ParseGroupVersion(gvk.APIVersion) + if err != nil { + return nil, err + } + return &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: gvk.Kind}, nil +} diff --git a/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/yaml.go b/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/yaml.go new file mode 100644 index 000000000..2fdd1d43d --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/runtime/serializer/yaml/yaml.go @@ -0,0 +1,46 @@ +/* +Copyright 2014 The Kubernetes 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 yaml + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/yaml" +) + +// yamlSerializer converts YAML passed to the Decoder methods to JSON. +type yamlSerializer struct { + // the nested serializer + runtime.Serializer +} + +// yamlSerializer implements Serializer +var _ runtime.Serializer = yamlSerializer{} + +// NewDecodingSerializer adds YAML decoding support to a serializer that supports JSON. +func NewDecodingSerializer(jsonSerializer runtime.Serializer) runtime.Serializer { + return &yamlSerializer{jsonSerializer} +} + +func (c yamlSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + out, err := yaml.ToJSON(data) + if err != nil { + return nil, nil, err + } + data = out + return c.Serializer.Decode(data, gvk, into) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 288c3285e..236f7e7c6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -980,6 +980,7 @@ k8s.io/apimachinery/pkg/runtime/serializer/protobuf k8s.io/apimachinery/pkg/runtime/serializer/recognizer k8s.io/apimachinery/pkg/runtime/serializer/streaming k8s.io/apimachinery/pkg/runtime/serializer/versioning +k8s.io/apimachinery/pkg/runtime/serializer/yaml k8s.io/apimachinery/pkg/selection k8s.io/apimachinery/pkg/types k8s.io/apimachinery/pkg/util/cache