Skip to content

Commit caf6844

Browse files
committed
Add config flag support to installers
1 parent 7483c85 commit caf6844

8 files changed

Lines changed: 247 additions & 10 deletions

File tree

cmd/state-installer/cmd.go

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type Params struct {
4848
activateDefault *project.Namespaced
4949
showVersion bool
5050
nonInteractive bool
51+
configSettings []string
5152
}
5253

5354
func newParams() *Params {
@@ -71,13 +72,14 @@ func main() {
7172
exitCode = 1
7273
}
7374

75+
if err := events.WaitForEvents(5*time.Second, rollbar.Wait, an.Wait, logging.Close); err != nil {
76+
logging.Warning("state-installer failed to wait for events: %v", err)
77+
}
78+
7479
if cfg != nil {
7580
events.Close("config", cfg.Close)
7681
}
7782

78-
if err := events.WaitForEvents(5*time.Second, rollbar.Wait, an.Wait, logging.Close); err != nil {
79-
logging.Warning("state-installer failed to wait for events: %v", err)
80-
}
8183
os.Exit(exitCode)
8284
}()
8385

@@ -194,6 +196,11 @@ func main() {
194196
Value: &params.showVersion,
195197
},
196198
{Name: "non-interactive", Shorthand: "n", Hidden: true, Value: &params.nonInteractive}, // don't prompt
199+
{
200+
Name: "config-set",
201+
Description: "Set config values in 'key=value' format, can be specified multiple times",
202+
Value: &params.configSettings,
203+
},
197204
// The remaining flags are for backwards compatibility (ie. we don't want to error out when they're provided)
198205
{Name: "channel", Hidden: true, Value: &garbageString},
199206
{Name: "bbb", Shorthand: "b", Hidden: true, Value: &garbageString},
@@ -328,13 +335,26 @@ func execute(out output.Outputer, cfg *config.Instance, an analytics.Dispatcher,
328335
out.Print(fmt.Sprintf("State Tool Package Manager is already installed at [NOTICE]%s[/RESET]. To reinstall use the [ACTIONABLE]--force[/RESET] flag.", installPath))
329336
an.Event(anaConst.CatInstallerFunnel, "already-installed")
330337
params.isUpdate = true
338+
339+
// Apply config settings even when already installed
340+
if err := applyConfigSettings(cfg, params.configSettings); err != nil {
341+
return errs.Wrap(err, "Failed to apply config settings")
342+
}
343+
331344
return postInstallEvents(out, cfg, an, params)
332345
}
333346

334347
if err := installOrUpdateFromLocalSource(out, cfg, an, payloadPath, params); err != nil {
335348
return err
336349
}
337350
storeInstallSource(params.sourceInstaller)
351+
352+
// Apply config settings after installation but before post-install events
353+
// This ensures the State Tool's config is properly set up
354+
if err := applyConfigSettings(cfg, params.configSettings); err != nil {
355+
return errs.Wrap(err, "Failed to apply config settings")
356+
}
357+
338358
return postInstallEvents(out, cfg, an, params)
339359
}
340360

@@ -501,3 +521,44 @@ func assertCompatibility() error {
501521

502522
return nil
503523
}
524+
525+
func applyConfigSettings(cfg *config.Instance, configSettings []string) error {
526+
for _, setting := range configSettings {
527+
setting = strings.TrimSpace(setting)
528+
if setting == "" {
529+
continue // Skip empty settings
530+
}
531+
if err := applyConfigSetting(cfg, setting); err != nil {
532+
return errs.Wrap(err, "Failed to apply config setting: %s", setting)
533+
}
534+
}
535+
return nil
536+
}
537+
538+
func applyConfigSetting(cfg *config.Instance, setting string) error {
539+
var key, valueStr string
540+
541+
if strings.Contains(setting, "=") {
542+
parts := strings.SplitN(setting, "=", 2)
543+
if len(parts) == 2 {
544+
key = strings.TrimSpace(parts[0])
545+
valueStr = strings.TrimSpace(parts[1])
546+
}
547+
}
548+
549+
if key == "" || valueStr == "" {
550+
return locale.NewInputError("err_config_invalid_format", "Config setting must be in 'key=value' format: {{.V0}}", setting)
551+
}
552+
553+
// Store the raw string value without type validation since config options
554+
// are not yet registered in the installer context
555+
err := cfg.Set(key, valueStr)
556+
if err != nil {
557+
// Log the error but don't fail the installation for config issues
558+
logging.Warning("Could not set config value %s=%s: %s", key, valueStr, errs.JoinMessage(err))
559+
return locale.WrapError(err, "err_config_set", "Could not set value {{.V0}} for key {{.V1}}", valueStr, key)
560+
}
561+
562+
logging.Debug("Config setting applied: %s=%s", key, valueStr)
563+
return nil
564+
}

installers/install.ps1

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,29 @@ function getopt([string] $opt, [string] $default, [string[]] $arr)
4141
return $default
4242
}
4343

44+
# Collect all instances of a flag (for flags that can appear multiple times)
45+
function getopt_all([string] $opt, [string[]] $arr)
46+
{
47+
$result = @()
48+
for ($i = 0; $i -lt $arr.Length; $i++)
49+
{
50+
$arg = $arr[$i]
51+
if ($arg -eq $opt -and ($i + 1) -lt $arr.Length)
52+
{
53+
$value = $arr[$i + 1]
54+
if ($value -and -not $value.StartsWith("-"))
55+
{
56+
$result += $opt
57+
$result += $value
58+
}
59+
}
60+
}
61+
return $result
62+
}
63+
4464
$script:CHANNEL = getopt "-b" $script:CHANNEL $args
4565
$script:VERSION = getopt "-v" $script:VERSION $args
66+
$script:CONFIG_SET_ARGS = getopt_all "--config-set" $args
4667

4768
function download([string] $url, [string] $out)
4869
{
@@ -269,7 +290,30 @@ $PSDefaultParameterValues['*:Encoding'] = 'utf8'
269290
# Run the installer.
270291
$env:ACTIVESTATE_SESSION_TOKEN = $script:SESSION_TOKEN_VALUE
271292
setShellOverride
272-
& $exePath $args --source-installer="install.ps1"
293+
294+
# Build installer arguments by reconstructing the command line properly
295+
$cmdArgs = @()
296+
297+
# Add non-config-set arguments first
298+
for ($i = 0; $i -lt $args.Length; $i++) {
299+
if ($args[$i] -eq "--config-set" -and ($i + 1) -lt $args.Length) {
300+
# Skip the --config-set flag and its value, we'll add them back from our processed array
301+
$i++ # Skip the value too
302+
} else {
303+
$cmdArgs += $args[$i]
304+
}
305+
}
306+
307+
# Add the processed config-set arguments (pass through without comma splitting - let Go handle it)
308+
if ($script:CONFIG_SET_ARGS -and $script:CONFIG_SET_ARGS.Length -gt 0) {
309+
$cmdArgs += $script:CONFIG_SET_ARGS
310+
}
311+
312+
# Add the source installer flag
313+
$cmdArgs += "--source-installer=install.ps1"
314+
315+
# Execute with the properly constructed arguments
316+
& $exePath @cmdArgs
273317
$success = $?
274318
if (Test-Path env:ACTIVESTATE_SESSION_TOKEN)
275319
{

installers/install.sh

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,30 @@ getopt() {
3636
echo $default
3737
}
3838

39+
# Collect all instances of a flag (for flags that can appear multiple times)
40+
getopt_all() {
41+
opt=$1; shift
42+
result=""
43+
i=0
44+
for arg in $@; do
45+
i=$((i + 1))
46+
if [ "${arg}" = "$opt" ]; then
47+
value=$(echo "$@" | cut -d' ' -f$(($i + 1)))
48+
if [ -n "$value" ] && [ "${value#-}" = "$value" ]; then # ensure value doesn't start with -
49+
if [ -n "$result" ]; then
50+
result="$result $opt $value"
51+
else
52+
result="$opt $value"
53+
fi
54+
fi
55+
fi
56+
done
57+
echo $result
58+
}
59+
3960
CHANNEL=$(getopt "-b" "$CHANNEL" $@)
4061
VERSION=$(getopt "-v" "$VERSION" $@)
62+
CONFIG_SET_ARGS=$(getopt_all "--config-set" $@)
4163

4264
if [ -z "${TERM}" ] || [ "${TERM}" = "dumb" ]; then
4365
OUTPUT_OK=""
@@ -205,7 +227,7 @@ progress_done
205227
echo ""
206228

207229
# Run the installer.
208-
ACTIVESTATE_SESSION_TOKEN=$SESSION_TOKEN_VALUE $INSTALLERTMPDIR/$INSTALLERNAME$BINARYEXT "$@" --source-installer="install.sh"
230+
ACTIVESTATE_SESSION_TOKEN=$SESSION_TOKEN_VALUE $INSTALLERTMPDIR/$INSTALLERNAME$BINARYEXT "$@" $CONFIG_SET_ARGS --source-installer="install.sh"
209231

210232
# Remove temp files
211233
rm -r $INSTALLERTMPDIR

internal/keypairs/rsa.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
// MinimumRSABitLength is the minimum allowed bit-length when generating RSA keys.
16-
const MinimumRSABitLength int = 12
16+
const MinimumRSABitLength int = 2048
1717

1818
type ErrKeypairPassphrase struct{ *locale.LocalizedError }
1919

internal/keypairs/rsa_publickey_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (suite *RSAPublicKeyTestSuite) TestEncryptsAndEncodes() {
5050
}
5151

5252
func (suite *RSAPublicKeyTestSuite) TestParsePublicKey() {
53-
kp, err := keypairs.GenerateRSA(1024)
53+
kp, err := keypairs.GenerateRSA(keypairs.MinimumRSABitLength)
5454
suite.Require().Nil(err)
5555
pubKeyPEM, err := kp.EncodePublicKey()
5656
suite.Require().Nil(err)

internal/secrets/share_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ type SecretsSharingTestSuite struct {
2323
func (suite *SecretsSharingTestSuite) SetupSuite() {
2424
var err error
2525

26-
suite.sourceKeypair, err = keypairs.GenerateRSA(1024)
26+
suite.sourceKeypair, err = keypairs.GenerateRSA(2048)
2727
suite.Require().NoError(err)
2828

29-
suite.targetKeypair, err = keypairs.GenerateRSA(1024)
29+
suite.targetKeypair, err = keypairs.GenerateRSA(2048)
3030
suite.Require().NoError(err)
3131

3232
suite.targetPubKey, err = suite.targetKeypair.EncodePublicKey()

internal/testhelpers/e2e/session.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ type Session struct {
6666
ignoreLogErrors bool
6767
cfg *config.Instance
6868
cache keyCache
69-
cfg *config.Instance
7069
}
7170

7271
type keyCache map[string]string

test/integration/install_scripts_int_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,117 @@ func (suite *InstallScriptsIntegrationTestSuite) assertCorrectVersion(ts *e2e.Se
281281
}
282282
}
283283

284+
func (suite *InstallScriptsIntegrationTestSuite) TestInstall_ConfigSet() {
285+
suite.OnlyRunForTags(tagsuite.InstallScripts)
286+
ts := e2e.New(suite.T(), false)
287+
defer ts.Close()
288+
289+
baseUrl := "https://state-tool.s3.amazonaws.com/update/state/"
290+
scriptBaseName := "install."
291+
if runtime.GOOS != "windows" {
292+
scriptBaseName += "sh"
293+
} else {
294+
scriptBaseName += "ps1"
295+
}
296+
scriptUrl := baseUrl + constants.ChannelName + "/" + scriptBaseName
297+
298+
b, err := httputil.GetDirect(scriptUrl)
299+
suite.Require().NoError(err)
300+
script := filepath.Join(ts.Dirs.Work, scriptBaseName)
301+
suite.Require().NoError(fileutils.WriteFile(script, b))
302+
303+
installDir := filepath.Join(ts.Dirs.Work, "install")
304+
args := []string{script}
305+
args = append(args, "-t", installDir)
306+
args = append(args, "-n") // non-interactive
307+
args = append(args, "-f") // force (like other working tests)
308+
args = append(args, "-b", constants.ChannelName)
309+
310+
args = append(args, "--config-set", "analytics.enabled=false")
311+
args = append(args, "--config-set", "output.format=json")
312+
args = append(args, "--config-set", "test.key1=value1")
313+
args = append(args, "--config-set", "test.key2=value2")
314+
315+
appInstallDir := filepath.Join(ts.Dirs.Work, "app")
316+
suite.NoError(fileutils.Mkdir(appInstallDir))
317+
318+
cmd := "bash"
319+
opts := []e2e.SpawnOptSetter{
320+
e2e.OptArgs(args...),
321+
e2e.OptAppendEnv(constants.DisableRuntime + "=false"),
322+
e2e.OptAppendEnv(fmt.Sprintf("%s=%s", constants.AppInstallDirOverrideEnvVarName, appInstallDir)),
323+
e2e.OptAppendEnv(fmt.Sprintf("%s=FOO", constants.OverrideSessionTokenEnvVarName)),
324+
e2e.OptAppendEnv(fmt.Sprintf("%s=false", constants.DisableActivateEventsEnvVarName)),
325+
}
326+
if runtime.GOOS == "windows" {
327+
cmd = "powershell.exe"
328+
opts = append(opts,
329+
e2e.OptAppendEnv("SHELL="),
330+
e2e.OptAppendEnv(constants.OverrideShellEnvVarName+"="),
331+
)
332+
}
333+
cp := ts.SpawnCmdWithOpts(cmd, opts...)
334+
cp.Expect("Preparing Installer for State Tool Package Manager")
335+
if runtime.GOOS == "windows" {
336+
cp.Expect("Continuing because the '--force' flag is set") // admin prompt
337+
}
338+
cp.Expect("Installation Complete", e2e.RuntimeSourcingTimeoutOpt)
339+
340+
cp.SendLine("exit")
341+
cp.ExpectExitCode(0)
342+
343+
stateExec, err := installation.StateExecFromDir(installDir)
344+
suite.NoError(err)
345+
suite.FileExists(stateExec)
346+
347+
suite.verifyConfigValue(ts, stateExec, "analytics.enabled", "false")
348+
suite.verifyConfigValue(ts, stateExec, "output.format", "json")
349+
suite.verifyConfigValue(ts, stateExec, "test.key1", "value1")
350+
suite.verifyConfigValue(ts, stateExec, "test.key2", "value2")
351+
352+
suite.verifyConfigValueDirect(ts, "analytics.enabled", "false")
353+
suite.verifyConfigValueDirect(ts, "output.format", "json")
354+
suite.verifyConfigValueDirect(ts, "test.key1", "value1")
355+
suite.verifyConfigValueDirect(ts, "test.key2", "value2")
356+
}
357+
358+
func (suite *InstallScriptsIntegrationTestSuite) verifyConfigValue(ts *e2e.Session, stateExec, key, expectedValue string) {
359+
cp := ts.SpawnCmd(stateExec, "config", "get", key, "--output=json")
360+
cp.ExpectExitCode(0)
361+
output := strings.TrimSpace(cp.StrippedSnapshot())
362+
363+
var result map[string]interface{}
364+
err := json.Unmarshal([]byte(output), &result)
365+
suite.Require().NoError(err, "Failed to parse JSON output: %s", output)
366+
367+
// Extract the value field from the JSON object
368+
value, exists := result["value"]
369+
suite.Require().True(exists, "JSON output missing 'value' field: %s", output)
370+
371+
var actualValue string
372+
switch v := value.(type) {
373+
case string:
374+
actualValue = v
375+
case bool:
376+
actualValue = fmt.Sprintf("%t", v)
377+
case float64:
378+
actualValue = fmt.Sprintf("%.0f", v)
379+
default:
380+
actualValue = fmt.Sprintf("%v", v)
381+
}
382+
383+
suite.Equal(expectedValue, actualValue, "Config value for key %s should be %s, got %s", key, expectedValue, actualValue)
384+
}
385+
386+
func (suite *InstallScriptsIntegrationTestSuite) verifyConfigValueDirect(ts *e2e.Session, key, expectedValue string) {
387+
cfg, err := config.NewCustom(ts.Dirs.Config, singlethread.New(), true)
388+
suite.Require().NoError(err)
389+
defer cfg.Close()
390+
391+
actualValue := cfg.GetString(key)
392+
suite.Equal(expectedValue, actualValue, "Config value for key %s should be %s, got %s (via config library)", key, expectedValue, actualValue)
393+
}
394+
284395
func (suite *InstallScriptsIntegrationTestSuite) assertAnalytics(ts *e2e.Session) {
285396
// Verify analytics reported a non-empty sessionToken.
286397
sessionTokenFound := false

0 commit comments

Comments
 (0)