github.com/smoorpal/witchcraft-go-server@v1.12.0/integration/runtime_test.go (about) 1 // Copyright (c) 2018 Palantir Technologies. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package integration 16 17 import ( 18 "context" 19 "fmt" 20 "io/ioutil" 21 "os" 22 "path" 23 "testing" 24 "time" 25 26 "github.com/nmiyake/pkg/dirs" 27 "github.com/palantir/pkg/httpserver" 28 "github.com/palantir/witchcraft-go-logging/wlog" 29 "github.com/palantir/witchcraft-go-server/config" 30 "github.com/palantir/witchcraft-go-server/status" 31 "github.com/palantir/witchcraft-go-server/witchcraft" 32 "github.com/stretchr/testify/assert" 33 "github.com/stretchr/testify/require" 34 "gopkg.in/yaml.v2" 35 ) 36 37 type testRuntimeConfig struct { 38 config.Runtime `yaml:",inline"` 39 SecretGreeting string `yaml:"secret-greeting"` 40 Exclamations int `yaml:"exclamations"` 41 } 42 43 // TestRuntimeReloadWithEncryptedConfig verifies behavior of refreshable configuration. 44 // 1. assert that the configuration printed at startup is that passed to the server's InitFunc 45 // 2. assert that removing the configuration does not send an update with empty values. 46 // 3. assert that writing a changed configuration does trigger subscribers to the refreshable. 47 func TestRuntimeReloadWithEncryptedConfig(t *testing.T) { 48 testDir, cleanup, err := dirs.TempDir("", "") 49 require.NoError(t, err) 50 defer cleanup() 51 52 wd, err := os.Getwd() 53 require.NoError(t, err) 54 defer func() { 55 err := os.Chdir(wd) 56 require.NoError(t, err) 57 }() 58 59 err = os.Chdir(testDir) 60 require.NoError(t, err) 61 62 port, err := httpserver.AvailablePort() 63 require.NoError(t, err) 64 65 err = os.MkdirAll("var/conf", 0755) 66 require.NoError(t, err) 67 68 installCfgYml := fmt.Sprintf(`product-name: %s 69 use-console-log: true 70 server: 71 address: localhost 72 port: %d 73 context-path: %s`, 74 productName, port, basePath) 75 err = ioutil.WriteFile(installYML, []byte(installCfgYml), 0644) 76 require.NoError(t, err) 77 78 const ecvKey = `AES:Nu2OInDbOHhXCNqqt1yyDuPwZwaJrSjV+IAypbZhw6Y=` 79 err = ioutil.WriteFile("var/conf/encrypted-config-value.key", []byte(ecvKey), 0644) 80 require.NoError(t, err) 81 82 cfg1 := testRuntimeConfig{SecretGreeting: "hello, world!", Exclamations: 3} 83 const cfg1YML = ` 84 secret-greeting: ${enc:/pSQ0v8R3QR8WOLnxoAWTsnI6kkjGgQMbqFcU9UC+LxStdGbfg1i3R9mlVZjEuXuecVG5AK1Sq109YxUcg==} 85 exclamations: 3 86 ` 87 cfg2 := testRuntimeConfig{SecretGreeting: "hello, world!", Exclamations: 4} 88 const cfg2YML = ` 89 secret-greeting: ${enc:/pSQ0v8R3QR8WOLnxoAWTsnI6kkjGgQMbqFcU9UC+LxStdGbfg1i3R9mlVZjEuXuecVG5AK1Sq109YxUcg==} 90 exclamations: 4 91 ` 92 err = ioutil.WriteFile(runtimeYML, []byte(cfg1YML), 0644) 93 require.NoError(t, err) 94 95 var currCfg testRuntimeConfig 96 97 server := witchcraft.NewServer(). 98 WithRuntimeConfigType(testRuntimeConfig{}). 99 WithDisableGoRuntimeMetrics(). 100 WithSelfSignedCertificate(). 101 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (cleanupFn func(), rErr error) { 102 setCfg := func(cfgI interface{}) { 103 cfg, ok := cfgI.(testRuntimeConfig) 104 if !ok { 105 panic(fmt.Errorf("unable to cast runtime config of type %T to testRuntimeConfig", cfgI)) 106 } 107 currCfg = cfg 108 } 109 setCfg(info.RuntimeConfig.Current()) 110 info.RuntimeConfig.Subscribe(setCfg) 111 return nil, nil 112 }) 113 114 serverChan := make(chan error) 115 go func() { 116 serverChan <- server.Start() 117 }() 118 119 select { 120 case err := <-serverChan: 121 require.NoError(t, err) 122 default: 123 } 124 125 ready := <-waitForTestServerReady(port, path.Join(basePath, status.LivenessEndpoint), 5*time.Second) 126 if !ready { 127 errMsg := "timed out waiting for server to start" 128 select { 129 case err := <-serverChan: 130 errMsg = fmt.Sprintf("%s: %+v", errMsg, err) 131 } 132 require.Fail(t, errMsg) 133 } 134 135 defer func() { 136 require.NoError(t, server.Close()) 137 }() 138 139 // Assert our configuration was set to the initial values 140 assert.Equal(t, cfg1, currCfg) 141 142 // Remove file and assert that we do not change the stored config 143 err = os.Remove(runtimeYML) 144 require.NoError(t, err) 145 time.Sleep(100 * time.Millisecond) 146 assert.Equal(t, cfg1, currCfg) 147 148 // Update config and assert that our subscription overwrites the value 149 err = ioutil.WriteFile(runtimeYML, []byte(cfg2YML), 0644) 150 require.NoError(t, err) 151 time.Sleep(100 * time.Millisecond) 152 assert.Equal(t, cfg2, currCfg) 153 } 154 155 // TestRuntimeReloadWithNilLoggerConfig verifies that reloading runtime configuration with nil logger config works. 156 func TestRuntimeReloadWithNilLoggerConfig(t *testing.T) { 157 testDir, cleanup, err := dirs.TempDir("", "") 158 require.NoError(t, err) 159 defer cleanup() 160 161 wd, err := os.Getwd() 162 require.NoError(t, err) 163 defer func() { 164 err := os.Chdir(wd) 165 require.NoError(t, err) 166 }() 167 168 err = os.Chdir(testDir) 169 require.NoError(t, err) 170 171 port, err := httpserver.AvailablePort() 172 require.NoError(t, err) 173 174 err = os.MkdirAll("var/conf", 0755) 175 require.NoError(t, err) 176 177 runtimeConfigWithLoggingYML, err := yaml.Marshal(config.Runtime{ 178 LoggerConfig: &config.LoggerConfig{ 179 Level: wlog.DebugLevel, 180 }, 181 }) 182 require.NoError(t, err) 183 184 err = ioutil.WriteFile(runtimeYML, runtimeConfigWithLoggingYML, 0644) 185 require.NoError(t, err) 186 187 runtimeConfigUpdatedChan := make(chan struct{}) 188 189 server := witchcraft.NewServer(). 190 WithInstallConfig(config.Install{ 191 ProductName: productName, 192 UseConsoleLog: true, 193 Server: config.Server{ 194 Address: "localhost", 195 Port: port, 196 ContextPath: basePath, 197 }, 198 }). 199 WithDisableGoRuntimeMetrics(). 200 WithSelfSignedCertificate(). 201 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (cleanupFn func(), rErr error) { 202 info.RuntimeConfig.Subscribe(func(cfgI interface{}) { 203 runtimeConfigUpdatedChan <- struct{}{} 204 }) 205 return nil, nil 206 }) 207 208 serverChan := make(chan error) 209 go func() { 210 serverChan <- server.Start() 211 }() 212 213 select { 214 case err := <-serverChan: 215 require.NoError(t, err) 216 default: 217 } 218 219 ready := <-waitForTestServerReady(port, path.Join(basePath, status.LivenessEndpoint), 5*time.Second) 220 if !ready { 221 errMsg := "timed out waiting for server to start" 222 select { 223 case err := <-serverChan: 224 errMsg = fmt.Sprintf("%s: %+v", errMsg, err) 225 } 226 require.Fail(t, errMsg) 227 } 228 229 defer func() { 230 require.NoError(t, server.Close()) 231 }() 232 233 err = ioutil.WriteFile(runtimeYML, []byte(""), 0644) 234 require.NoError(t, err) 235 236 select { 237 case <-runtimeConfigUpdatedChan: 238 break 239 case <-time.After(5 * time.Second): 240 assert.Fail(t, "timed out waiting for runtime configuration to be updated") 241 } 242 } 243 244 // TestRuntimeReloadWithConfigWithExtraKeyDefaultUnmarshal verifies that reloading runtime configuration with an unknown 245 // key succeeds when server is in its default mode. 246 func TestRuntimeReloadWithConfigWithUnknownKeyDefaultUnmarshal(t *testing.T) { 247 testDir, cleanup, err := dirs.TempDir("", "") 248 require.NoError(t, err) 249 defer cleanup() 250 251 wd, err := os.Getwd() 252 require.NoError(t, err) 253 defer func() { 254 err := os.Chdir(wd) 255 require.NoError(t, err) 256 }() 257 258 err = os.Chdir(testDir) 259 require.NoError(t, err) 260 261 port, err := httpserver.AvailablePort() 262 require.NoError(t, err) 263 264 err = os.MkdirAll("var/conf", 0755) 265 require.NoError(t, err) 266 267 const validCfg1YML = ` 268 logging: 269 level: debug 270 exclamations: 3 271 ` 272 validCfg1 := testRuntimeConfig{ 273 Runtime: config.Runtime{ 274 LoggerConfig: &config.LoggerConfig{ 275 Level: wlog.DebugLevel, 276 }, 277 }, 278 Exclamations: 3, 279 } 280 const invalidYML = ` 281 invalid-key: invalid-value 282 ` 283 const validCfg2YML = ` 284 logging: 285 level: info 286 exclamations: 4 287 ` 288 validCfg2 := testRuntimeConfig{ 289 Runtime: config.Runtime{ 290 LoggerConfig: &config.LoggerConfig{ 291 Level: wlog.InfoLevel, 292 }, 293 }, 294 Exclamations: 4, 295 } 296 297 err = ioutil.WriteFile(runtimeYML, []byte(validCfg1YML), 0644) 298 require.NoError(t, err) 299 300 var currCfg testRuntimeConfig 301 302 server := witchcraft.NewServer(). 303 WithRuntimeConfigType(testRuntimeConfig{}). 304 WithInstallConfig(config.Install{ 305 ProductName: productName, 306 UseConsoleLog: true, 307 Server: config.Server{ 308 Address: "localhost", 309 Port: port, 310 ContextPath: basePath, 311 }, 312 }). 313 WithDisableGoRuntimeMetrics(). 314 WithSelfSignedCertificate(). 315 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (cleanupFn func(), rErr error) { 316 setCfg := func(cfgI interface{}) { 317 cfg, ok := cfgI.(testRuntimeConfig) 318 if !ok { 319 panic(fmt.Errorf("unable to cast runtime config of type %T to testRuntimeConfig", cfgI)) 320 } 321 currCfg = cfg 322 } 323 setCfg(info.RuntimeConfig.Current()) 324 info.RuntimeConfig.Subscribe(setCfg) 325 return nil, nil 326 }) 327 328 serverChan := make(chan error) 329 go func() { 330 serverChan <- server.Start() 331 }() 332 333 select { 334 case err := <-serverChan: 335 require.NoError(t, err) 336 default: 337 } 338 339 ready := <-waitForTestServerReady(port, path.Join(basePath, status.LivenessEndpoint), 5*time.Second) 340 if !ready { 341 errMsg := "timed out waiting for server to start" 342 select { 343 case err := <-serverChan: 344 errMsg = fmt.Sprintf("%s: %+v", errMsg, err) 345 } 346 require.Fail(t, errMsg) 347 } 348 349 defer func() { 350 require.NoError(t, server.Close()) 351 }() 352 353 // Assert our configuration was set to the initial values 354 assert.Equal(t, validCfg1, currCfg) 355 356 // Assert that, in default mode, configuration with extra key is considered valid: extra value should be ignored, 357 // and "missing" values should be default values 358 invalidRuntimeConfig := testRuntimeConfig{ 359 Runtime: config.Runtime{}, 360 SecretGreeting: "", 361 Exclamations: 0, 362 } 363 364 err = ioutil.WriteFile(runtimeYML, []byte("invalid-key: \"invalid-value\""), 0644) 365 require.NoError(t, err) 366 time.Sleep(100 * time.Millisecond) 367 assert.Equal(t, invalidRuntimeConfig, currCfg) 368 369 // Update config to different config and assert that our subscription overwrites the value 370 err = ioutil.WriteFile(runtimeYML, []byte(validCfg2YML), 0644) 371 require.NoError(t, err) 372 time.Sleep(100 * time.Millisecond) 373 assert.Equal(t, validCfg2, currCfg) 374 } 375 376 // TestRuntimeReloadWithConfigWithExtraKeyStrictUnmarshal verifies that reloading runtime configuration with an unknown 377 // key fails and uses last known valid config when server is in strict unmarshal mode. 378 func TestRuntimeReloadWithConfigWithExtraKeyStrictUnmarshal(t *testing.T) { 379 testDir, cleanup, err := dirs.TempDir("", "") 380 require.NoError(t, err) 381 defer cleanup() 382 383 wd, err := os.Getwd() 384 require.NoError(t, err) 385 defer func() { 386 err := os.Chdir(wd) 387 require.NoError(t, err) 388 }() 389 390 err = os.Chdir(testDir) 391 require.NoError(t, err) 392 393 port, err := httpserver.AvailablePort() 394 require.NoError(t, err) 395 396 err = os.MkdirAll("var/conf", 0755) 397 require.NoError(t, err) 398 399 const validCfg1YML = ` 400 logging: 401 level: debug 402 exclamations: 3 403 ` 404 validCfg1 := testRuntimeConfig{ 405 Runtime: config.Runtime{ 406 LoggerConfig: &config.LoggerConfig{ 407 Level: wlog.DebugLevel, 408 }, 409 }, 410 Exclamations: 3, 411 } 412 const invalidYML = ` 413 invalid-key: invalid-value 414 ` 415 const validCfg2YML = ` 416 logging: 417 level: info 418 exclamations: 4 419 ` 420 validCfg2 := testRuntimeConfig{ 421 Runtime: config.Runtime{ 422 LoggerConfig: &config.LoggerConfig{ 423 Level: wlog.InfoLevel, 424 }, 425 }, 426 Exclamations: 4, 427 } 428 429 err = ioutil.WriteFile(runtimeYML, []byte(validCfg1YML), 0644) 430 require.NoError(t, err) 431 432 var currCfg testRuntimeConfig 433 434 server := witchcraft.NewServer(). 435 WithRuntimeConfigType(testRuntimeConfig{}). 436 WithInstallConfig(config.Install{ 437 ProductName: productName, 438 UseConsoleLog: true, 439 Server: config.Server{ 440 Address: "localhost", 441 Port: port, 442 ContextPath: basePath, 443 }, 444 }). 445 WithDisableGoRuntimeMetrics(). 446 WithSelfSignedCertificate(). 447 WithInitFunc(func(ctx context.Context, info witchcraft.InitInfo) (cleanupFn func(), rErr error) { 448 setCfg := func(cfgI interface{}) { 449 cfg, ok := cfgI.(testRuntimeConfig) 450 if !ok { 451 panic(fmt.Errorf("unable to cast runtime config of type %T to testRuntimeConfig", cfgI)) 452 } 453 currCfg = cfg 454 } 455 setCfg(info.RuntimeConfig.Current()) 456 info.RuntimeConfig.Subscribe(setCfg) 457 return nil, nil 458 }). 459 WithStrictUnmarshalConfig() 460 461 serverChan := make(chan error) 462 go func() { 463 serverChan <- server.Start() 464 }() 465 466 select { 467 case err := <-serverChan: 468 require.NoError(t, err) 469 default: 470 } 471 472 ready := <-waitForTestServerReady(port, path.Join(basePath, status.LivenessEndpoint), 5*time.Second) 473 if !ready { 474 errMsg := "timed out waiting for server to start" 475 select { 476 case err := <-serverChan: 477 errMsg = fmt.Sprintf("%s: %+v", errMsg, err) 478 } 479 require.Fail(t, errMsg) 480 } 481 482 defer func() { 483 require.NoError(t, server.Close()) 484 }() 485 486 // Assert our configuration was set to the initial values 487 assert.Equal(t, validCfg1, currCfg) 488 489 // Assert that introducing invalid config does not change the stored config 490 err = ioutil.WriteFile(runtimeYML, []byte("invalid-key: \"invalid-value\""), 0644) 491 require.NoError(t, err) 492 time.Sleep(100 * time.Millisecond) 493 assert.Equal(t, validCfg1, currCfg) 494 495 // Update config to a valid, but different config and assert that our subscription overwrites the value 496 err = ioutil.WriteFile(runtimeYML, []byte(validCfg2YML), 0644) 497 require.NoError(t, err) 498 time.Sleep(100 * time.Millisecond) 499 assert.Equal(t, validCfg2, currCfg) 500 }