Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multinamespace name config #1786

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions chart/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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": {
Expand Down
2 changes: 2 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
17 changes: 17 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions config/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ plugins: {}
experimental:
multiNamespaceMode:
enabled: false
namespaceNameFormat: {}

syncSettings:
disableSync: false
Expand Down
5 changes: 5 additions & 0 deletions pkg/config/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
56 changes: 56 additions & 0 deletions pkg/config/validation_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"fmt"
"testing"

"github.com/loft-sh/vcluster/config"
Expand Down Expand Up @@ -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())
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/setup/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 24 additions & 12 deletions pkg/util/translate/multi_namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading