diff --git a/chart/values.schema.json b/chart/values.schema.json index 1c14d6c3a..edb3236ec 100755 --- a/chart/values.schema.json +++ b/chart/values.schema.json @@ -1360,6 +1360,10 @@ "type": "boolean", "description": "Enabled specifies if multi namespace mode should get enabled" }, + "namespaceNameFormat": { + "$ref": "#/$defs/ExperimentalMultiNamespaceNameFormat", + "description": "NamespaceNameFormat allows you to customize the name of the physical namespaces." + }, "namespaceLabels": { "additionalProperties": { "type": "string" @@ -1371,6 +1375,28 @@ "additionalProperties": false, "type": "object" }, + "ExperimentalMultiNamespaceNameFormat": { + "properties": { + "prefix": { + "type": "string", + "description": "Prefix is the prefix added to the physical namespaces.\nIf empty, the default is to use \"vcluster\"" + }, + "rawBase": { + "type": "boolean", + "description": "If RawBase is true, use the virtual namespace as is, otherwise hash it." + }, + "rawSuffix": { + "type": "boolean", + "description": "If RawSuffix is true, use the cluster name as is, otherwise hash it." + }, + "avoidRedundantFormatting": { + "type": "boolean", + "description": "If AvoidRedundantFormatting is true, we check if base (the name between prefix and suffix)\nalready contains the prefix and suffix. In that case, we just return base, instead of\nformatting again. Otherwise, we always add prefix and suffix." + } + }, + "additionalProperties": false, + "type": "object" + }, "ExperimentalSyncSettings": { "properties": { "disableSync": { diff --git a/chart/values.yaml b/chart/values.yaml index 87e0ba75e..63af184c7 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -821,6 +821,8 @@ experimental: multiNamespaceMode: # Enabled specifies if multi namespace mode should get enabled enabled: false + # NamespaceNameFormat allows you to customize the name of the physical namespaces. + namespaceNameFormat: {} # SyncSettings are advanced settings for the syncer controller. syncSettings: diff --git a/config/config.go b/config/config.go index 9695424f3..2a6760916 100644 --- a/config/config.go +++ b/config/config.go @@ -1679,10 +1679,30 @@ type ExperimentalMultiNamespaceMode struct { // Enabled specifies if multi namespace mode should get enabled Enabled bool `json:"enabled,omitempty"` + // NamespaceNameFormat allows you to customize the name of the physical namespaces. + NamespaceNameFormat ExperimentalMultiNamespaceNameFormat `json:"namespaceNameFormat,omitempty"` + // NamespaceLabels are extra labels that will be added by vCluster to each created namespace. NamespaceLabels map[string]string `json:"namespaceLabels,omitempty"` } +type ExperimentalMultiNamespaceNameFormat struct { + // Prefix is the prefix added to the physical namespaces. + // If empty, the default is to use "vcluster" + Prefix string `json:"prefix,omitempty"` + + // If RawBase is true, use the virtual namespace as is, otherwise hash it. + RawBase bool `json:"rawBase,omitempty"` + + // If RawSuffix is true, use the cluster name as is, otherwise hash it. + RawSuffix bool `json:"rawSuffix,omitempty"` + + // If AvoidRedundantFormatting is true, we check if base (the name between prefix and suffix) + // already contains the prefix and suffix. In that case, we just return base, instead of + // formatting again. Otherwise, we always add prefix and suffix. + AvoidRedundantFormatting bool `json:"avoidRedundantFormatting,omitempty"` +} + type ExperimentalIsolatedControlPlane struct { // Enabled specifies if the isolated control plane feature should be enabled. Enabled bool `json:"enabled,omitempty" product:"pro"` diff --git a/config/config_test.go b/config/config_test.go index 8e3639331..cea77c4e1 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -334,6 +334,23 @@ func TestConfig_IsProFeatureEnabled(t *testing.T) { }, expected: true, }, + { + name: "Namespace reformatting on multinamespace mode does not use Pro features", + config: &Config{ + Experimental: Experimental{ + MultiNamespaceMode: ExperimentalMultiNamespaceMode{ + Enabled: true, + NamespaceNameFormat: ExperimentalMultiNamespaceNameFormat{ + Prefix: "foo", + RawBase: true, + RawSuffix: true, + AvoidRedundantFormatting: true, + }, + }, + }, + }, + expected: false, + }, } for _, tt := range tests { diff --git a/config/values.yaml b/config/values.yaml index f80b2907f..1d35b0784 100644 --- a/config/values.yaml +++ b/config/values.yaml @@ -491,6 +491,7 @@ plugins: {} experimental: multiNamespaceMode: enabled: false + namespaceNameFormat: {} syncSettings: disableSync: false diff --git a/pkg/config/validation.go b/pkg/config/validation.go index 9185654ca..e96301f0b 100644 --- a/pkg/config/validation.go +++ b/pkg/config/validation.go @@ -100,6 +100,11 @@ func ValidateConfigAndSetDefaults(config *VirtualClusterConfig) error { } } + // set multi namespace mode name format + if config.Experimental.MultiNamespaceMode.NamespaceNameFormat.Prefix == "" { + config.Experimental.MultiNamespaceMode.NamespaceNameFormat.Prefix = "vcluster" + } + // check resolve dns err = validateMappings(config.Networking.ResolveDNS) if err != nil { diff --git a/pkg/config/validation_test.go b/pkg/config/validation_test.go index 7178eae00..cd99633d0 100644 --- a/pkg/config/validation_test.go +++ b/pkg/config/validation_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "testing" "github.com/loft-sh/vcluster/config" @@ -125,3 +126,58 @@ func mutHook(clientCfg config.ValidatingWebhookClientConfig) config.MutatingWebh } return hook } + +func TestValidateConfigAndSetDefaults(t *testing.T) { + testCases := []struct { + name string + wantErr string + config VirtualClusterConfig + checkFunc func(config *VirtualClusterConfig) error + }{ + { + name: "multi-namespace namespace name formatting default prefix", + wantErr: "", + config: VirtualClusterConfig{}, + checkFunc: func(config *VirtualClusterConfig) error { + if prefix := config.Experimental.MultiNamespaceMode.NamespaceNameFormat.Prefix; prefix != "vcluster" { + return fmt.Errorf("unexpected prefix %q", prefix) + } + return nil + }, + }, + { + name: "multi-namespace namespace name formatting custom prefix", + wantErr: "", + config: VirtualClusterConfig{ + Config: config.Config{ + Experimental: config.Experimental{ + MultiNamespaceMode: config.ExperimentalMultiNamespaceMode{ + NamespaceNameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: "foo", + }, + }, + }, + }, + }, + checkFunc: func(config *VirtualClusterConfig) error { + if prefix := config.Experimental.MultiNamespaceMode.NamespaceNameFormat.Prefix; prefix != "foo" { + return fmt.Errorf("unexpected prefix %q", prefix) + } + return nil + }, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + outputConfig := tt.config + err := ValidateConfigAndSetDefaults(&outputConfig) + if err != nil && (tt.wantErr == "" || tt.wantErr != err.Error()) { + t.Errorf("wanted err to be %s but got %s", tt.wantErr, err.Error()) + } else if err == nil && tt.wantErr != "" { + t.Errorf("wanted err to be %s but got nil", tt.wantErr) + } else if err := tt.checkFunc(&outputConfig); err != nil { + t.Errorf("wanted check err to be nil but got %s", err.Error()) + } + }) + } +} diff --git a/pkg/setup/config.go b/pkg/setup/config.go index ca1f510c4..96e38a2a0 100644 --- a/pkg/setup/config.go +++ b/pkg/setup/config.go @@ -47,7 +47,7 @@ func InitAndValidateConfig(ctx context.Context, vConfig *config.VirtualClusterCo // get workload target namespace if vConfig.Experimental.MultiNamespaceMode.Enabled { - translate.Default = translate.NewMultiNamespaceTranslator(vConfig.WorkloadNamespace) + translate.Default = translate.NewMultiNamespaceTranslator(vConfig.WorkloadNamespace, vConfig.Experimental.MultiNamespaceMode.NamespaceNameFormat) } else { // ensure target namespace vConfig.WorkloadTargetNamespace = vConfig.Experimental.SyncSettings.TargetNamespace diff --git a/pkg/util/translate/multi_namespace.go b/pkg/util/translate/multi_namespace.go index d264aec4c..a881463d5 100644 --- a/pkg/util/translate/multi_namespace.go +++ b/pkg/util/translate/multi_namespace.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/loft-sh/vcluster/config" "github.com/loft-sh/vcluster/pkg/scheme" "github.com/loft-sh/vcluster/pkg/syncer/synccontext" "sigs.k8s.io/controller-runtime/pkg/client" @@ -14,14 +15,16 @@ import ( var _ Translator = &multiNamespace{} -func NewMultiNamespaceTranslator(currentNamespace string) Translator { +func NewMultiNamespaceTranslator(currentNamespace string, nameFormat config.ExperimentalMultiNamespaceNameFormat) Translator { return &multiNamespace{ currentNamespace: currentNamespace, + nameFormat: nameFormat, } } type multiNamespace struct { currentNamespace string + nameFormat config.ExperimentalMultiNamespaceNameFormat } func (s *multiNamespace) SingleNamespaceTarget() bool { @@ -68,25 +71,34 @@ func (s *multiNamespace) IsManaged(_ *synccontext.SyncContext, pObj client.Objec } func (s *multiNamespace) IsTargetedNamespace(ns string) bool { - return strings.HasPrefix(ns, s.getNamespacePrefix()) && strings.HasSuffix(ns, getNamespaceSuffix(s.currentNamespace, VClusterName)) + return strings.HasPrefix(ns, s.getNamespacePrefix()) && strings.HasSuffix(ns, s.getNamespaceSuffix()) } func (s *multiNamespace) getNamespacePrefix() string { - return "vcluster" + return s.nameFormat.Prefix } func (s *multiNamespace) HostNamespace(vNamespace string) string { - return hostNamespace(s.currentNamespace, vNamespace, s.getNamespacePrefix(), VClusterName) -} - -func hostNamespace(currentNamespace, vNamespace, prefix, suffix string) string { - sha := sha256.Sum256([]byte(vNamespace)) - return fmt.Sprintf("%s-%s-%s", prefix, hex.EncodeToString(sha[0:])[0:8], getNamespaceSuffix(currentNamespace, suffix)) + base := vNamespace + if !s.nameFormat.RawBase { + sha := sha256.Sum256([]byte(base)) + base = hex.EncodeToString(sha[0:])[0:8] + } + if s.nameFormat.AvoidRedundantFormatting && s.IsTargetedNamespace(base) { + return base + } + prefix := s.getNamespacePrefix() + suffix := s.getNamespaceSuffix() + return fmt.Sprintf("%s-%s-%s", prefix, base, suffix) } -func getNamespaceSuffix(currentNamespace, suffix string) string { - sha := sha256.Sum256([]byte(currentNamespace + "x" + suffix)) - return hex.EncodeToString(sha[0:])[0:8] +func (s *multiNamespace) getNamespaceSuffix() string { + suffix := VClusterName + if !s.nameFormat.RawSuffix { + sha := sha256.Sum256([]byte(s.currentNamespace + "x" + suffix)) + suffix = hex.EncodeToString(sha[0:])[0:8] + } + return suffix } func (s *multiNamespace) MarkerLabelCluster() string { diff --git a/pkg/util/translate/multi_namespace_test.go b/pkg/util/translate/multi_namespace_test.go new file mode 100644 index 000000000..df6fa6fac --- /dev/null +++ b/pkg/util/translate/multi_namespace_test.go @@ -0,0 +1,175 @@ +package translate + +import ( + "testing" + + "github.com/loft-sh/vcluster/config" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + testNamespace = "multi" + testPrefix = "prefix" + testSuffix = "suffix" + testSuffixSha256 = "8a237015" // sha256("multixsuffix") + testBase = "foo-bar-baz" + testBaseSha256 = "269dce1a" +) + +func TestMultiNamespaceTranslator_IsManaged(t *testing.T) { + testCases := []struct { + name string + nameFormat config.ExperimentalMultiNamespaceNameFormat + obj client.Object + wantRes bool + }{ + { + name: "managed - default namespace name format", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + }, + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-name", + Namespace: testPrefix + "-base-" + testSuffixSha256, + Annotations: map[string]string{ + NameAnnotation: "foobar", + }, + }, + }, + wantRes: true, + }, + { + name: "unmanaged - default namespace name format (different prefix)", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + }, + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-name", + Namespace: "other-base-" + testSuffixSha256, + Annotations: map[string]string{ + NameAnnotation: "foobar", + }, + }, + }, + wantRes: false, + }, + { + name: "unmanaged - default namespace name format (different suffix)", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + }, + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-name", + Namespace: testPrefix + "-base-aaaaaaaa", + Annotations: map[string]string{ + NameAnnotation: "foobar", + }, + }, + }, + wantRes: false, + }, + { + name: "managed - custom namespace name format (raw suffix)", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + RawSuffix: true, + }, + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-name", + Namespace: testPrefix + "-base-" + testSuffix, + Annotations: map[string]string{ + NameAnnotation: "foobar", + }, + }, + }, + wantRes: true, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + translator := &multiNamespace{ + currentNamespace: testNamespace, + nameFormat: tt.nameFormat, + } + res := translator.IsManaged(nil, tt.obj) + if tt.wantRes != res { + t.Errorf("wanted result to be %v but got %v", tt.wantRes, res) + } + }) + } +} + +func TestMultiNamespaceTranslator_HostNamespace(t *testing.T) { + testCases := []struct { + name string + nameFormat config.ExperimentalMultiNamespaceNameFormat + namespace string + wantRes string + }{ + { + name: "default namespace name format", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + }, + namespace: testBase, + wantRes: testPrefix + "-" + testBaseSha256 + "-" + testSuffixSha256, + }, + { + name: "custom namespace name format (raw base)", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + RawBase: true, + }, + namespace: testBase, + wantRes: testPrefix + "-" + testBase + "-" + testSuffixSha256, + }, + { + name: "custom namespace name format (raw suffix)", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + RawSuffix: true, + }, + namespace: testBase, + wantRes: testPrefix + "-" + testBaseSha256 + "-" + testSuffix, + }, + { + name: "custom namespace name format (raw base and suffix)", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + RawBase: true, + RawSuffix: true, + }, + namespace: testBase, + wantRes: testPrefix + "-" + testBase + "-" + testSuffix, + }, + { + name: "custom namespace name format without redundancy", + nameFormat: config.ExperimentalMultiNamespaceNameFormat{ + Prefix: testPrefix, + RawBase: true, + RawSuffix: true, + AvoidRedundantFormatting: true, + }, + namespace: testPrefix + "-" + testBase + "-" + testSuffix, + wantRes: testPrefix + "-" + testBase + "-" + testSuffix, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + translator := &multiNamespace{ + currentNamespace: testNamespace, + nameFormat: tt.nameFormat, + } + res := translator.HostNamespace(tt.namespace) + if tt.wantRes != res { + t.Errorf("wanted result to be %q but got %q", tt.wantRes, res) + } + }) + } +} diff --git a/test/framework/framework.go b/test/framework/framework.go index 526edfecb..2eaecbeb6 100644 --- a/test/framework/framework.go +++ b/test/framework/framework.go @@ -9,6 +9,7 @@ import ( "github.com/loft-sh/log" "github.com/loft-sh/vcluster/cmd/vclusterctl/cmd" + "github.com/loft-sh/vcluster/config" "github.com/loft-sh/vcluster/pkg/cli" "github.com/loft-sh/vcluster/pkg/cli/flags" logutil "github.com/loft-sh/vcluster/pkg/util/log" @@ -119,7 +120,9 @@ func CreateFramework(ctx context.Context, scheme *runtime.Scheme) error { var multiNamespaceMode bool if os.Getenv("MULTINAMESPACE_MODE") == "true" { - translate.Default = translate.NewMultiNamespaceTranslator(ns) + translate.Default = translate.NewMultiNamespaceTranslator(ns, config.ExperimentalMultiNamespaceNameFormat{ + Prefix: "vcluster", + }) multiNamespaceMode = true } else { translate.Default = translate.NewSingleNamespaceTranslator(ns)