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 }