Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ Examples:
# Login on a headless Linux host using an in-memory /dev/shm credential store
$ bk auth login --device --credential-store shm

# Or set the default once so 'bk auth login' uses /dev/shm without flags:
$ bk config set credential_store shm
$ bk auth login --device

# Login with read-only access
$ bk auth login --scopes read_only

Expand Down
22 changes: 14 additions & 8 deletions cmd/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ Examples:
type ConfigKey string

const (
KeySelectedOrg ConfigKey = "selected_org"
KeyOutputFormat ConfigKey = "output_format"
KeyNoPager ConfigKey = "no_pager"
KeyQuiet ConfigKey = "quiet"
KeyNoInput ConfigKey = "no_input"
KeyPager ConfigKey = "pager"
KeyTelemetry ConfigKey = "telemetry"
KeyExperiments ConfigKey = "experiments"
KeySelectedOrg ConfigKey = "selected_org"
KeyOutputFormat ConfigKey = "output_format"
KeyNoPager ConfigKey = "no_pager"
KeyQuiet ConfigKey = "quiet"
KeyNoInput ConfigKey = "no_input"
KeyPager ConfigKey = "pager"
KeyTelemetry ConfigKey = "telemetry"
KeyExperiments ConfigKey = "experiments"
KeyCredentialStore ConfigKey = "credential_store"
)

// AllKeys returns all valid configuration keys
Expand All @@ -59,6 +60,7 @@ func AllKeys() []ConfigKey {
KeyPager,
KeyTelemetry,
KeyExperiments,
KeyCredentialStore,
}
}

Expand Down Expand Up @@ -103,6 +105,8 @@ func (k ConfigKey) ValidValues() []string {
return []string{"json", "yaml", "text"}
case KeyNoPager, KeyQuiet, KeyNoInput, KeyTelemetry:
return []string{"true", "false"}
case KeyCredentialStore:
return []string{"auto", "keyring", "shm"}
default:
return nil
}
Expand Down Expand Up @@ -150,6 +154,8 @@ func SetConfigValue(conf *config.Config, key ConfigKey, value string, local bool
return conf.SetTelemetry(v)
case KeyExperiments:
return conf.SetExperiments(value)
case KeyCredentialStore:
return conf.SetCredentialStore(value, local)
}

return nil
Expand Down
19 changes: 19 additions & 0 deletions cmd/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestValidateKey(t *testing.T) {
"no_input",
"pager",
"experiments",
"credential_store",
}

for _, key := range validKeys {
Expand Down Expand Up @@ -131,4 +132,22 @@ func TestConfigKeyValidValues(t *testing.T) {
t.Errorf("KeyPager.ValidValues() = %v, want nil", values)
}
})

t.Run("credential_store has valid values", func(t *testing.T) {
t.Parallel()

values := KeyCredentialStore.ValidValues()
if values == nil {
t.Fatal("expected valid values for credential_store")
}
expected := []string{"auto", "keyring", "shm"}
if len(values) != len(expected) {
t.Fatalf("got %d values, want %d", len(values), len(expected))
}
for i, want := range expected {
if values[i] != want {
t.Errorf("values[%d] = %q, want %q", i, values[i], want)
}
}
})
}
20 changes: 12 additions & 8 deletions cmd/config/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ Returns the effective value after applying precedence rules:
Environment variable > Local config (.bk.yaml) > User config (~/.config/bk.yaml) > Default

Valid keys:
selected_org Organization slug to use
output_format Default output format (json, yaml, text)
no_pager Disable pager for text output (true, false)
quiet Suppress progress output (true, false)
no_input Disable interactive prompts (true, false)
pager Custom pager command
experiments Enabled experiment flags
selected_org Organization slug to use
output_format Default output format (json, yaml, text)
no_pager Disable pager for text output (true, false)
quiet Suppress progress output (true, false)
no_input Disable interactive prompts (true, false)
pager Custom pager command
experiments Enabled experiment flags
credential_store Default credential store for tokens (auto, keyring, shm)

Examples:
$ bk config get output_format
$ bk config get pager`
$ bk config get pager
$ bk config get credential_store`
}

func (c *GetCmd) Run() error {
Expand Down Expand Up @@ -71,6 +73,8 @@ func (c *GetCmd) Run() error {
value = conf.Pager()
case KeyExperiments:
value = conf.Experiments()
case KeyCredentialStore:
value = conf.CredentialStore()
}

if value != "" {
Expand Down
4 changes: 4 additions & 0 deletions cmd/config/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/cli/v3/pkg/keyring"
)

type ListCmd struct {
Expand Down Expand Up @@ -63,6 +64,9 @@ func (c *ListCmd) Run() error {
if v := conf.Experiments(); v != "" {
items = append(items, configItem{string(KeyExperiments), v, "effective"})
}
if v := conf.CredentialStore(); v != "" && v != keyring.StoreAuto {
items = append(items, configItem{string(KeyCredentialStore), v, "effective"})
}
}

if c.Local && !inGitRepo {
Expand Down
20 changes: 12 additions & 8 deletions cmd/config/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ func (c *SetCmd) Help() string {
return `Set a configuration value.

Valid keys:
selected_org Organization slug to use
output_format Default output format (json, yaml, text)
no_pager Disable pager for text output (true, false)
quiet Suppress progress output (true, false)
no_input Disable interactive prompts (true, false) [user config only]
pager Custom pager command [user config only]
telemetry Enable anonymous usage telemetry (true, false) [user config only]
selected_org Organization slug to use
output_format Default output format (json, yaml, text)
no_pager Disable pager for text output (true, false)
quiet Suppress progress output (true, false)
no_input Disable interactive prompts (true, false) [user config only]
pager Custom pager command [user config only]
telemetry Enable anonymous usage telemetry (true, false) [user config only]
credential_store Default credential store for tokens (auto, keyring, shm)

Examples:
# Set default output format to YAML
Expand All @@ -36,7 +37,10 @@ Examples:
$ bk config set output_format text --local

# Set a custom pager
$ bk config set pager "less -RS"`
$ bk config set pager "less -RS"

# Pin token storage to /dev/shm (recommended for headless Linux dev hosts)
$ bk config set credential_store shm`
}

func (c *SetCmd) Run() error {
Expand Down
48 changes: 38 additions & 10 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@ type orgConfig struct {
}

type fileConfig struct {
SelectedOrg string `yaml:"selected_org"`
Organizations map[string]orgConfig `yaml:"organizations,omitempty"`
Pipelines []string `yaml:"pipelines,omitempty"`
NoPager bool `yaml:"no_pager,omitempty"`
OutputFormat string `yaml:"output_format,omitempty"`
Quiet bool `yaml:"quiet,omitempty"`
NoInput bool `yaml:"no_input,omitempty"`
Pager string `yaml:"pager,omitempty"`
Telemetry *bool `yaml:"telemetry,omitempty"`
Experiments string `yaml:"experiments,omitempty"`
SelectedOrg string `yaml:"selected_org"`
Organizations map[string]orgConfig `yaml:"organizations,omitempty"`
Pipelines []string `yaml:"pipelines,omitempty"`
NoPager bool `yaml:"no_pager,omitempty"`
OutputFormat string `yaml:"output_format,omitempty"`
Quiet bool `yaml:"quiet,omitempty"`
NoInput bool `yaml:"no_input,omitempty"`
Pager string `yaml:"pager,omitempty"`
Telemetry *bool `yaml:"telemetry,omitempty"`
Experiments string `yaml:"experiments,omitempty"`
CredentialStore string `yaml:"credential_store,omitempty"`
}

// Config contains the configuration for the currently selected organization
Expand Down Expand Up @@ -393,6 +394,33 @@ func (conf *Config) SetExperiments(v string) error {
return conf.writeUser()
}

// CredentialStore returns the configured credential store for token storage.
// Precedence: env > local > user > "auto".
func (conf *Config) CredentialStore() string {
return firstNonEmpty(
os.Getenv(keyring.CredentialStoreEnv),
conf.local.CredentialStore,
conf.user.CredentialStore,
keyring.StoreAuto,
)
}

// SetCredentialStore writes the credential store preference. An empty value
// clears it. Invalid stores are rejected before any write.
func (conf *Config) SetCredentialStore(v string, saveLocal bool) error {
if v != "" {
if err := keyring.ValidateCredentialStore(v); err != nil {
return err
}
}
if !saveLocal {
conf.user.CredentialStore = v
return conf.writeUser()
}
conf.local.CredentialStore = v
return conf.writeLocal()
}

func lookupBoolEnv(key string) (bool, bool) {
v := os.Getenv(key)
if v == "" {
Expand Down
104 changes: 104 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,107 @@ func TestHasExperiment(t *testing.T) {
})
}
}

func TestCredentialStore(t *testing.T) {
t.Run("defaults to auto", func(t *testing.T) {
unsetEnv(t, keyring.CredentialStoreEnv)

fs := afero.NewMemMapFs()
conf := New(fs, nil)

if got := conf.CredentialStore(); got != keyring.StoreAuto {
t.Errorf("CredentialStore() = %q, want %q", got, keyring.StoreAuto)
}
})

t.Run("env overrides user config", func(t *testing.T) {
setEnv(t, keyring.CredentialStoreEnv, keyring.StoreKeyring)

fs := afero.NewMemMapFs()
conf := New(fs, nil)
if err := conf.SetCredentialStore(keyring.StoreSHM, false); err != nil {
t.Fatalf("SetCredentialStore: %v", err)
}

if got := conf.CredentialStore(); got != keyring.StoreKeyring {
t.Errorf("CredentialStore() = %q, want %q (env should override)", got, keyring.StoreKeyring)
}
})

t.Run("user config overrides default", func(t *testing.T) {
unsetEnv(t, keyring.CredentialStoreEnv)

fs := afero.NewMemMapFs()
conf := New(fs, nil)
if err := conf.SetCredentialStore(keyring.StoreSHM, false); err != nil {
t.Fatalf("SetCredentialStore: %v", err)
}

if got := conf.CredentialStore(); got != keyring.StoreSHM {
t.Errorf("CredentialStore() = %q, want %q", got, keyring.StoreSHM)
}
})

t.Run("local config overrides user config", func(t *testing.T) {
unsetEnv(t, keyring.CredentialStoreEnv)

fs := afero.NewMemMapFs()
conf := New(fs, nil)
if err := conf.SetCredentialStore(keyring.StoreKeyring, false); err != nil {
t.Fatalf("SetCredentialStore(user): %v", err)
}
if err := conf.SetCredentialStore(keyring.StoreSHM, true); err != nil {
t.Fatalf("SetCredentialStore(local): %v", err)
}

if got := conf.CredentialStore(); got != keyring.StoreSHM {
t.Errorf("CredentialStore() = %q, want %q (local should win over user)", got, keyring.StoreSHM)
}
})

t.Run("rejects unknown values", func(t *testing.T) {
unsetEnv(t, keyring.CredentialStoreEnv)

fs := afero.NewMemMapFs()
conf := New(fs, nil)

if err := conf.SetCredentialStore("vault", false); err == nil {
t.Error("SetCredentialStore(\"vault\") expected error, got nil")
}
if got := conf.CredentialStore(); got != keyring.StoreAuto {
t.Errorf("CredentialStore() after rejected write = %q, want %q", got, keyring.StoreAuto)
}
})

t.Run("empty value clears the preference", func(t *testing.T) {
unsetEnv(t, keyring.CredentialStoreEnv)

fs := afero.NewMemMapFs()
conf := New(fs, nil)
if err := conf.SetCredentialStore(keyring.StoreSHM, false); err != nil {
t.Fatalf("SetCredentialStore: %v", err)
}
if err := conf.SetCredentialStore("", false); err != nil {
t.Fatalf("SetCredentialStore(\"\") error: %v", err)
}

if got := conf.CredentialStore(); got != keyring.StoreAuto {
t.Errorf("CredentialStore() after clear = %q, want %q", got, keyring.StoreAuto)
}
})

t.Run("setter persists across reload", func(t *testing.T) {
unsetEnv(t, keyring.CredentialStoreEnv)

fs := afero.NewMemMapFs()
conf := New(fs, nil)
if err := conf.SetCredentialStore(keyring.StoreSHM, false); err != nil {
t.Fatalf("SetCredentialStore: %v", err)
}

conf2 := New(fs, nil)
if got := conf2.CredentialStore(); got != keyring.StoreSHM {
t.Errorf("CredentialStore() after reload = %q, want %q", got, keyring.StoreSHM)
}
})
}
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/buildkite/cli/v3/internal/config"
bkErrors "github.com/buildkite/cli/v3/internal/errors"
"github.com/buildkite/cli/v3/pkg/analytics"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
)

// Kong CLI structure, with base commands defined as additional commands are defined in their respective files
Expand Down Expand Up @@ -302,6 +303,10 @@ func run() int {

conf := config.New(nil, nil)

// Must run before kong dispatches: bk auth login builds its keyring
// before factory.New() does.
factory.ApplyCredentialStoreFromConfig(conf)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think this would recognise a local config unless --credential-store is also passed, eg bk auth login --local wouldn't respect a local .bk.yaml config being present.

We'd need to ensure login respects --local config, or only support user configs.


tracker := analytics.Init("dev", conf.TelemetryEnabled())
defer tracker.Close()
tracker.SetOrg(conf.OrganizationSlug())
Expand Down
Loading