Skip to content

Commit

Permalink
feat(kubernetes): add provider.kubernetes.delimiter config property
Browse files Browse the repository at this point in the history
the delimiter configuration property for kubernetes allows you to change the default "_" delimiter.

By specifying "/" or "." as a delimiter you ensure that you won't collide with objects names that contains the default delimiter "_".

Fixes #207
  • Loading branch information
acouvreur committed Feb 5, 2024
1 parent 9947881 commit 60b2704
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"ghcr.io/devcontainers/features/node:1": {
"version": "lts"
},
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/go:1": {}
},
Expand Down
44 changes: 25 additions & 19 deletions app/providers/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ import (
"k8s.io/client-go/tools/cache"
)

// Delimiter is used to split name into kind,namespace,name,replicacount
const Delimiter = "_"

type Config struct {
OriginalName string
Kind string // deployment or statefulset
Expand All @@ -38,11 +35,10 @@ type Workload interface {
UpdateScale(ctx context.Context, workloadName string, scale *autoscalingv1.Scale, opts metav1.UpdateOptions) (*autoscalingv1.Scale, error)
}

func convertName(name string) (*Config, error) {
// name format kind_namespace_name_replicas
s := strings.Split(name, Delimiter)
func (provider *KubernetesProvider) convertName(name string) (*Config, error) {
s := strings.Split(name, provider.delimiter)
if len(s) < 4 {
return nil, errors.New("invalid name should be: kind" + Delimiter + "namespace" + Delimiter + "name" + Delimiter + "replicas")
return nil, errors.New("invalid name should be: kind" + provider.delimiter + "namespace" + provider.delimiter + "name" + provider.delimiter + "replicas")
}
replicas, err := strconv.Atoi(s[3])
if err != nil {
Expand All @@ -58,8 +54,18 @@ func convertName(name string) (*Config, error) {
}, nil
}

func (provider *KubernetesProvider) convertStatefulset(ss *appsv1.StatefulSet, replicas int32) string {
return fmt.Sprintf("statefulset%s%s%s%s%s%d", provider.delimiter, ss.Namespace, provider.delimiter, ss.Name, provider.delimiter, replicas)
}

func (provider *KubernetesProvider) convertDeployment(d *appsv1.Deployment, replicas int32) string {
return fmt.Sprintf("statefulset%s%s%s%s%s%d", provider.delimiter, d.Namespace, provider.delimiter, d.Name, provider.delimiter, replicas)

}

type KubernetesProvider struct {
Client kubernetes.Interface
Client kubernetes.Interface
delimiter string
}

func NewKubernetesProvider(providerConfig providerConfig.Kubernetes) (*KubernetesProvider, error) {
Expand All @@ -79,13 +85,14 @@ func NewKubernetesProvider(providerConfig providerConfig.Kubernetes) (*Kubernete
}

return &KubernetesProvider{
Client: client,
Client: client,
delimiter: providerConfig.Delimiter,
}, nil

}

func (provider *KubernetesProvider) Start(ctx context.Context, name string) (instance.State, error) {
config, err := convertName(name)
config, err := provider.convertName(name)
if err != nil {
return instance.UnrecoverableInstanceState(name, err.Error(), int(config.Replicas))
}
Expand All @@ -94,7 +101,7 @@ func (provider *KubernetesProvider) Start(ctx context.Context, name string) (ins
}

func (provider *KubernetesProvider) Stop(ctx context.Context, name string) (instance.State, error) {
config, err := convertName(name)
config, err := provider.convertName(name)
if err != nil {
return instance.UnrecoverableInstanceState(name, err.Error(), int(config.Replicas))
}
Expand All @@ -121,12 +128,11 @@ func (provider *KubernetesProvider) GetGroups(ctx context.Context) (map[string][

group := groups[groupName]
// TOOD: Use annotation for scale
name := fmt.Sprintf("%s_%s_%s_%d", "deployment", deployment.Namespace, deployment.Name, 1)
name := provider.convertDeployment(&deployment, 1)
group = append(group, name)
groups[groupName] = group
}


statefulSets, err := provider.Client.AppsV1().StatefulSets(core_v1.NamespaceAll).List(ctx, metav1.ListOptions{
LabelSelector: enableLabel,
})
Expand All @@ -143,7 +149,7 @@ func (provider *KubernetesProvider) GetGroups(ctx context.Context) (map[string][

group := groups[groupName]
// TOOD: Use annotation for scale
name := fmt.Sprintf("%s_%s_%s_%d", "statefulset", statefulSet.Namespace, statefulSet.Name, 1)
name := provider.convertStatefulset(&statefulSet, 1)
group = append(group, name)
groups[groupName] = group
}
Expand Down Expand Up @@ -179,7 +185,7 @@ func (provider *KubernetesProvider) scale(ctx context.Context, config *Config, r
}

func (provider *KubernetesProvider) GetState(ctx context.Context, name string) (instance.State, error) {
config, err := convertName(name)
config, err := provider.convertName(name)
if err != nil {
return instance.UnrecoverableInstanceState(name, err.Error(), int(config.Replicas))
}
Expand Down Expand Up @@ -243,12 +249,12 @@ func (provider *KubernetesProvider) watchDeployents(instance chan<- string) cach
}

if *newDeployment.Spec.Replicas == 0 {
instance <- fmt.Sprintf("deployment_%s_%s_%d", newDeployment.Namespace, newDeployment.Name, *oldDeployment.Spec.Replicas)
instance <- provider.convertDeployment(newDeployment, *oldDeployment.Spec.Replicas)
}
},
DeleteFunc: func(obj interface{}) {
deletedDeployment := obj.(*appsv1.Deployment)
instance <- fmt.Sprintf("deployment_%s_%s_%d", deletedDeployment.Namespace, deletedDeployment.Name, *deletedDeployment.Spec.Replicas)
instance <- provider.convertDeployment(deletedDeployment, *deletedDeployment.Spec.Replicas)
},
}
factory := informers.NewSharedInformerFactoryWithOptions(provider.Client, 2*time.Second, informers.WithNamespace(core_v1.NamespaceAll))
Expand All @@ -269,12 +275,12 @@ func (provider *KubernetesProvider) watchStatefulSets(instance chan<- string) ca
}

if *newStatefulSet.Spec.Replicas == 0 {
instance <- fmt.Sprintf("statefulset_%s_%s_%d", newStatefulSet.Namespace, newStatefulSet.Name, *oldStatefulSet.Spec.Replicas)
instance <- provider.convertStatefulset(newStatefulSet, *oldStatefulSet.Spec.Replicas)
}
},
DeleteFunc: func(obj interface{}) {
deletedStatefulSet := obj.(*appsv1.StatefulSet)
instance <- fmt.Sprintf("statefulset__%s_%s_%d", deletedStatefulSet.Namespace, deletedStatefulSet.Name, *deletedStatefulSet.Spec.Replicas)
instance <- provider.convertStatefulset(deletedStatefulSet, *deletedStatefulSet.Spec.Replicas)
},
}
factory := informers.NewSharedInformerFactoryWithOptions(provider.Client, 2*time.Second, informers.WithNamespace(core_v1.NamespaceAll))
Expand Down
9 changes: 6 additions & 3 deletions app/providers/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ func TestKubernetesProvider_Start(t *testing.T) {
deploymentAPI := mocks.DeploymentMock{}
statefulsetAPI := mocks.StatefulSetsMock{}
provider := KubernetesProvider{
Client: mocks.NewKubernetesAPIClientMock(&deploymentAPI, &statefulsetAPI),
Client: mocks.NewKubernetesAPIClientMock(&deploymentAPI, &statefulsetAPI),
delimiter: "_",
}

deploymentAPI.On("GetScale", mock.Anything, tt.data.name, metav1.GetOptions{}).Return(tt.data.get, nil)
Expand Down Expand Up @@ -188,7 +189,8 @@ func TestKubernetesProvider_Stop(t *testing.T) {
deploymentAPI := mocks.DeploymentMock{}
statefulsetAPI := mocks.StatefulSetsMock{}
provider := KubernetesProvider{
Client: mocks.NewKubernetesAPIClientMock(&deploymentAPI, &statefulsetAPI),
Client: mocks.NewKubernetesAPIClientMock(&deploymentAPI, &statefulsetAPI),
delimiter: "_",
}

deploymentAPI.On("GetScale", mock.Anything, tt.data.name, metav1.GetOptions{}).Return(tt.data.get, nil)
Expand Down Expand Up @@ -316,7 +318,8 @@ func TestKubernetesProvider_GetState(t *testing.T) {
deploymentAPI := mocks.DeploymentMock{}
statefulsetAPI := mocks.StatefulSetsMock{}
provider := KubernetesProvider{
Client: mocks.NewKubernetesAPIClientMock(&deploymentAPI, &statefulsetAPI),
Client: mocks.NewKubernetesAPIClientMock(&deploymentAPI, &statefulsetAPI),
delimiter: "_",
}

deploymentAPI.On("Get", mock.Anything, tt.data.name, metav1.GetOptions{}).Return(tt.data.getDeployment, nil)
Expand Down
6 changes: 4 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ It provides an integrations with multiple reverse proxies and different loading
// Provider flags
startCmd.Flags().StringVar(&conf.Provider.Name, "provider.name", "docker", fmt.Sprintf("Provider to use to manage containers %v", config.GetProviders()))
viper.BindPFlag("provider.name", startCmd.Flags().Lookup("provider.name"))
startCmd.Flags().Float32Var(&conf.Provider.Kubernetes.QPS, "provider.kubernetes.qps", 5, fmt.Sprintf("QPS limit for K8S API access client-side throttling"))
startCmd.Flags().Float32Var(&conf.Provider.Kubernetes.QPS, "provider.kubernetes.qps", 5, "QPS limit for K8S API access client-side throttling")
viper.BindPFlag("provider.kubernetes.qps", startCmd.Flags().Lookup("provider.kubernetes.qps"))
startCmd.Flags().IntVar(&conf.Provider.Kubernetes.Burst, "provider.kubernetes.burst", 10, fmt.Sprintf("Maximum burst for K8S API acees client-side throttling"))
startCmd.Flags().IntVar(&conf.Provider.Kubernetes.Burst, "provider.kubernetes.burst", 10, "Maximum burst for K8S API acees client-side throttling")
viper.BindPFlag("provider.kubernetes.burst", startCmd.Flags().Lookup("provider.kubernetes.burst"))
startCmd.Flags().StringVar(&conf.Provider.Kubernetes.Delimiter, "provider.kubernetes.delimiter", "_", "Delimiter used for namespace/resource type/name resolution. Defaults to \"_\" for backward compatibility. But you should use \"/\" or \".\"")
viper.BindPFlag("provider.kubernetes.delimiter", startCmd.Flags().Lookup("provider.kubernetes.delimiter"))
// Server flags
startCmd.Flags().IntVar(&conf.Server.Port, "server.port", 10000, "The server port to use")
viper.BindPFlag("server.port", startCmd.Flags().Lookup("server.port"))
Expand Down
27 changes: 27 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ import (
"gotest.tools/v3/assert"
)

func TestDefault(t *testing.T) {
testDir, err := os.Getwd()
require.NoError(t, err, "error getting the current working directory")

wantConfig, err := ioutil.ReadFile(filepath.Join(testDir, "testdata", "config_default.json"))
require.NoError(t, err, "error reading test config file")

// CHANGE `startCmd` behavior to only print the config, this is for testing purposes only
newStartCommand = mockStartCommand

t.Run("config file", func(t *testing.T) {
conf = config.NewConfig()
cmd := NewRootCommand()
output := &bytes.Buffer{}
cmd.SetOut(output)
cmd.SetArgs([]string{
"start",
})
cmd.Execute()

gotOutput := output.String()

assert.Equal(t, string(wantConfig), gotOutput)
})
}

func TestPrecedence(t *testing.T) {
testDir, err := os.Getwd()
require.NoError(t, err, "error getting the current working directory")
Expand Down Expand Up @@ -82,6 +108,7 @@ func TestPrecedence(t *testing.T) {
"--provider.name", "cli",
"--provider.kubernetes.qps", "256",
"--provider.kubernetes.burst", "512",
"--provider.kubernetes.delimiter", "_",
"--server.port", "3333",
"--server.base-path", "/cli/",
"--storage.file", "/tmp/cli.json",
Expand Down
1 change: 1 addition & 0 deletions cmd/testdata/config.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PROVIDER_NAME=envvar
PROVIDER_KUBERNETES_QPS=16
PROVIDER_KUBERNETES_BURST=32
PROVIDER_KUBERNETES_DELIMITER=/
SERVER_PORT=2222
SERVER_BASE_PATH=/envvar/
STORAGE_FILE=/tmp/envvar.json
Expand Down
1 change: 1 addition & 0 deletions cmd/testdata/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ provider:
kubernetes:
qps: 64
burst: 128
delimiter: .
server:
port: 1111
base-path: /configfile/
Expand Down
3 changes: 2 additions & 1 deletion cmd/testdata/config_cli_wanted.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"Name": "cli",
"Kubernetes": {
"QPS": 256,
"Burst": 512
"Burst": 512,
"Delimiter": "_"
}
},
"Sessions": {
Expand Down
35 changes: 35 additions & 0 deletions cmd/testdata/config_default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"Server": {
"Port": 10000,
"BasePath": "/"
},
"Storage": {
"File": ""
},
"Provider": {
"Name": "docker",
"Kubernetes": {
"QPS": 5,
"Burst": 10,
"Delimiter": "_"
}
},
"Sessions": {
"DefaultDuration": 300000000000,
"ExpirationInterval": 20000000000
},
"Logging": {
"Level": "info"
},
"Strategy": {
"Dynamic": {
"CustomThemesPath": "",
"ShowDetailsByDefault": true,
"DefaultTheme": "hacker-terminal",
"DefaultRefreshFrequency": 5000000000
},
"Blocking": {
"DefaultTimeout": 60000000000
}
}
}
3 changes: 2 additions & 1 deletion cmd/testdata/config_env_wanted.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"Name": "envvar",
"Kubernetes": {
"QPS": 16,
"Burst": 32
"Burst": 32,
"Delimiter": "/"
}
},
"Sessions": {
Expand Down
3 changes: 2 additions & 1 deletion cmd/testdata/config_yaml_wanted.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"Name": "configfile",
"Kubernetes": {
"QPS": 64,
"Burst": 128
"Burst": 128,
"Delimiter": "."
}
},
"Sessions": {
Expand Down
15 changes: 10 additions & 5 deletions config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import (
"fmt"
)

// Provider holds the provider description
// It can be either docker, swarm or kubernetes
// Provider holds the provider configurations
type Provider struct {
Name string `mapstructure:"NAME" yaml:"provider,omitempty"`
// The provider name to use
// It can be either docker, swarm or kubernetes. Defaults to "docker"
Name string `mapstructure:"NAME" yaml:"provider,omitempty" default:"docker"`
Kubernetes Kubernetes
}

Expand All @@ -16,16 +17,20 @@ type Kubernetes struct {
QPS float32 `mapstructure:"QPS" yaml:"QPS" default:"5"`
//Maximum burst for client-side throttle
Burst int `mapstructure:"BURST" yaml:"Burst" default:"10"`
//Delimiter used for namespace/resource type/name resolution. Defaults to "_" for backward compatibility. But you should use "/" or ".".
Delimiter string `mapstructure:"DELIMITER" yaml:"Delimiter" default:"_"`
}

var providers = []string{"docker", "swarm", "kubernetes"}

func NewProviderConfig() Provider {
return Provider{

Name: "docker",
Kubernetes: Kubernetes{
QPS: 5,
Burst: 10,
QPS: 5,
Burst: 10,
Delimiter: "_", //Delimiter used for namespace/resource type/name resolution. Defaults to "_" for backward compatibility. But you should use "/" or ".".
},
}
}
Expand Down
Loading

0 comments on commit 60b2704

Please sign in to comment.