Feature: karmadactl init supports deployment through configuration files

Signed-off-by: tiansuo114 <1729765480@qq.com>

fix lint

Signed-off-by: tiansuo114 <1729765480@qq.com>
This commit is contained in:
tiansuo114 2024-08-07 15:29:59 +08:00
parent f7d6da341e
commit 9b9847e3f7
No known key found for this signature in database
11 changed files with 1467 additions and 5 deletions

View File

@ -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 }}/

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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, ",")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

1
vendor/modules.txt vendored
View File

@ -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