knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/leaderelection/config_test.go (about)

     1  /*
     2  Copyright 2020 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package leaderelection
    18  
    19  import (
    20  	"fmt"
    21  	"strconv"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	corev1 "k8s.io/api/core/v1"
    28  
    29  	"knative.dev/pkg/kmap"
    30  )
    31  
    32  const (
    33  	controllerOrdinalEnv = "STATEFUL_CONTROLLER_ORDINAL"
    34  	serviceNameEnv       = "STATEFUL_SERVICE_NAME"
    35  	servicePortEnv       = "STATEFUL_SERVICE_PORT"
    36  	serviceProtocolEnv   = "STATEFUL_SERVICE_PROTOCOL"
    37  )
    38  
    39  func okConfig() *Config {
    40  	return &Config{
    41  		Buckets:       1,
    42  		LeaseDuration: 15 * time.Second,
    43  		RenewDeadline: 10 * time.Second,
    44  		RetryPeriod:   2 * time.Second,
    45  	}
    46  }
    47  
    48  func okData() map[string]string {
    49  	return map[string]string{
    50  		"buckets": "1",
    51  		// values in this data come from the defaults suggested in the
    52  		// code:
    53  		// https://github.com/kubernetes/client-go/blob/kubernetes-1.16.0/tools/leaderelection/leaderelection.go
    54  		"lease-duration": "15s",
    55  		"renew-deadline": "10s",
    56  		"retry-period":   "2s",
    57  	}
    58  }
    59  
    60  func TestNewConfigMapFromData(t *testing.T) {
    61  	cases := []struct {
    62  		name     string
    63  		data     map[string]string
    64  		expected *Config
    65  		err      string
    66  	}{{
    67  		name:     "OK config - controller enabled",
    68  		data:     okData(),
    69  		expected: okConfig(),
    70  	}, {
    71  		name: "OK config - controller enabled with multiple buckets",
    72  		data: kmap.Union(okData(), map[string]string{
    73  			"buckets": "5",
    74  		}),
    75  		expected: func() *Config {
    76  			config := okConfig()
    77  			config.Buckets = 5
    78  			return config
    79  		}(),
    80  	}, {
    81  		name: "invalid lease-duration",
    82  		data: kmap.Union(okData(), map[string]string{
    83  			"lease-duration": "flops",
    84  		}),
    85  		err: `failed to parse "lease-duration": time: invalid duration`,
    86  	}, {
    87  		name: "invalid renew-deadline",
    88  		data: kmap.Union(okData(), map[string]string{
    89  			"renew-deadline": "flops",
    90  		}),
    91  		err: `failed to parse "renew-deadline": time: invalid duration`,
    92  	}, {
    93  		name: "invalid retry-period",
    94  		data: kmap.Union(okData(), map[string]string{
    95  			"retry-period": "flops",
    96  		}),
    97  		err: `failed to parse "retry-period": time: invalid duration`,
    98  	}, {
    99  		name: "invalid buckets - not an int",
   100  		data: kmap.Union(okData(), map[string]string{
   101  			"buckets": "not-an-int",
   102  		}),
   103  		err: `failed to parse "buckets": strconv.ParseUint: parsing "not-an-int": invalid syntax`,
   104  	}, {
   105  		name: "invalid buckets - too small",
   106  		data: kmap.Union(okData(), map[string]string{
   107  			"buckets": "0",
   108  		}),
   109  		err: fmt.Sprint("buckets: value must be between 1 <= 0 <= ", MaxBuckets),
   110  	}, {
   111  		name: "invalid buckets - too large",
   112  		data: kmap.Union(okData(), map[string]string{
   113  			"buckets": strconv.Itoa(int(MaxBuckets + 1)),
   114  		}),
   115  		err: fmt.Sprintf("buckets: value must be between 1 <= %d <= %d", MaxBuckets+1, MaxBuckets),
   116  	}, {
   117  		name: "legacy keys",
   118  		data: map[string]string{
   119  			"leaseDuration": "2s",
   120  			"renewDeadline": "3s",
   121  			"retryPeriod":   "4s",
   122  			"buckets":       "5",
   123  		},
   124  		expected: &Config{
   125  			Buckets:       5,
   126  			LeaseDuration: 2 * time.Second,
   127  			RenewDeadline: 3 * time.Second,
   128  			RetryPeriod:   4 * time.Second,
   129  		},
   130  	}, {
   131  		name: "prioritize new keys",
   132  		data: map[string]string{
   133  			"lease-duration": "1s",
   134  			"renew-deadline": "2s",
   135  			"retry-period":   "3s",
   136  			"leaseDuration":  "4s",
   137  			"renewDeadline":  "5s",
   138  			"retryPeriod":    "6s",
   139  			"buckets":        "7",
   140  		},
   141  		expected: &Config{
   142  			Buckets:       7,
   143  			LeaseDuration: 1 * time.Second,
   144  			RenewDeadline: 2 * time.Second,
   145  			RetryPeriod:   3 * time.Second,
   146  		},
   147  	}}
   148  
   149  	for _, tc := range cases {
   150  		t.Run(tc.name, func(t *testing.T) {
   151  			actualConfig, actualErr := NewConfigFromConfigMap(
   152  				&corev1.ConfigMap{
   153  					Data: tc.data,
   154  				})
   155  
   156  			if actualErr != nil {
   157  				if got, want := actualErr.Error(), tc.err; !strings.HasPrefix(got, want) {
   158  					t.Fatalf("Err = '%s', want: '%s'", got, want)
   159  				}
   160  			} else if tc.err != "" {
   161  				t.Fatal("Expected an error, got none")
   162  			}
   163  
   164  			if got, want := actualConfig, tc.expected; !cmp.Equal(got, want) {
   165  				t.Errorf("Config = %#v, want: %#v, diff(-want,+got):\n%s", got, want, cmp.Diff(want, got))
   166  			}
   167  		})
   168  	}
   169  }
   170  
   171  func TestNewConfigFromMap(t *testing.T) {
   172  	tt := []struct {
   173  		name    string
   174  		data    map[string]string
   175  		want    Config
   176  		wantErr bool
   177  	}{{
   178  		name: "ok config",
   179  		data: map[string]string{
   180  			"lease-duration": "15s",
   181  			"buckets":        "5",
   182  		},
   183  		want: Config{
   184  			Buckets:       5,
   185  			LeaseDuration: 15 * time.Second,
   186  			RenewDeadline: 40 * time.Second,
   187  			RetryPeriod:   10 * time.Second,
   188  		},
   189  	}, {
   190  		name: "ok config, prefix map",
   191  		data: map[string]string{
   192  			"lease-duration":              "15s",
   193  			"buckets":                     "5",
   194  			"map-lease-prefix.reconciler": "reconciler1",
   195  		},
   196  		want: Config{
   197  			Buckets:       5,
   198  			LeaseDuration: 15 * time.Second,
   199  			RenewDeadline: 40 * time.Second,
   200  			RetryPeriod:   10 * time.Second,
   201  			LeaseNamesPrefixMapping: map[string]string{
   202  				"reconciler": "reconciler1",
   203  			},
   204  		},
   205  	}}
   206  
   207  	for _, tc := range tt {
   208  		t.Run(tc.name, func(t *testing.T) {
   209  			c, err := NewConfigFromMap(tc.data)
   210  			if tc.wantErr != (err != nil) {
   211  				t.Fatalf("want err %v got %v", tc.wantErr, err)
   212  			}
   213  			if diff := cmp.Diff(tc.want, *c); diff != "" {
   214  				t.Fatal("(-want, +got)", diff)
   215  			}
   216  		})
   217  	}
   218  }
   219  
   220  func TestGetComponentConfig(t *testing.T) {
   221  	const expectedName = "the-component"
   222  	cases := []struct {
   223  		name     string
   224  		config   Config
   225  		expected ComponentConfig
   226  	}{{
   227  		name: "component enabled",
   228  		config: Config{
   229  			LeaseDuration: 15 * time.Second,
   230  			RenewDeadline: 10 * time.Second,
   231  			RetryPeriod:   2 * time.Second,
   232  		},
   233  		expected: ComponentConfig{
   234  			Component:     expectedName,
   235  			LeaseDuration: 15 * time.Second,
   236  			RenewDeadline: 10 * time.Second,
   237  			RetryPeriod:   2 * time.Second,
   238  		},
   239  	}}
   240  
   241  	for _, tc := range cases {
   242  		t.Run(tc.name, func(t *testing.T) {
   243  			actual := tc.config.GetComponentConfig(expectedName)
   244  			if got, want := actual, tc.expected; !cmp.Equal(got, want) {
   245  				t.Errorf("Incorrect config: diff(-want,+got):\n%s", cmp.Diff(want, got))
   246  			}
   247  		})
   248  	}
   249  }
   250  
   251  func TestNewStatefulSetConfig(t *testing.T) {
   252  	cases := []struct {
   253  		name     string
   254  		pod      string
   255  		service  string
   256  		port     string
   257  		protocol string
   258  		wantErr  string
   259  		expected statefulSetConfig
   260  	}{{
   261  		name:    "success with default",
   262  		pod:     "as-42",
   263  		service: "autoscaler",
   264  		expected: statefulSetConfig{
   265  			StatefulSetID: statefulSetID{
   266  				ssName:  "as",
   267  				ordinal: 42,
   268  			},
   269  			ServiceName: "autoscaler",
   270  			Port:        "80",
   271  			Protocol:    "http",
   272  		},
   273  	}, {
   274  		name:     "success with overriding",
   275  		pod:      "as-42",
   276  		service:  "autoscaler",
   277  		port:     "8080",
   278  		protocol: "ws",
   279  		expected: statefulSetConfig{
   280  			StatefulSetID: statefulSetID{
   281  				ssName:  "as",
   282  				ordinal: 42,
   283  			},
   284  			ServiceName: "autoscaler",
   285  			Port:        "8080",
   286  			Protocol:    "ws",
   287  		},
   288  	}, {
   289  		name:    "failure with empty envs",
   290  		wantErr: "required key STATEFUL_CONTROLLER_ORDINAL missing value",
   291  	}, {
   292  		name:    "failure with invalid name",
   293  		pod:     "as-abcd",
   294  		wantErr: `envconfig.Process: assigning STATEFUL_CONTROLLER_ORDINAL to StatefulSetID: converting 'as-abcd' to type leaderelection.statefulSetID. details: strconv.Atoi: parsing "abcd": invalid syntax`,
   295  	}}
   296  
   297  	for _, tc := range cases {
   298  		t.Run(tc.name, func(t *testing.T) {
   299  			if tc.pod != "" {
   300  				t.Setenv(controllerOrdinalEnv, tc.pod)
   301  			}
   302  			if tc.service != "" {
   303  				t.Setenv(serviceNameEnv, tc.service)
   304  			}
   305  			if tc.port != "" {
   306  				t.Setenv(servicePortEnv, tc.port)
   307  			}
   308  			if tc.protocol != "" {
   309  				t.Setenv(serviceProtocolEnv, tc.protocol)
   310  			}
   311  
   312  			ssc, err := newStatefulSetConfig()
   313  			if err != nil {
   314  				if got, want := err.Error(), tc.wantErr; got != want {
   315  					t.Errorf("Got error: %s. want: %s", got, want)
   316  				}
   317  			} else {
   318  				if got, want := *ssc, tc.expected; !cmp.Equal(got, want, cmp.AllowUnexported(statefulSetID{})) {
   319  					t.Errorf("Incorrect config: diff(-want,+got):\n%s", cmp.Diff(want, got))
   320  				}
   321  			}
   322  		})
   323  	}
   324  }