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  }