Skip to content

Commit 2089117

Browse files
MarinXMarinkangasta
authored
feat(config): added initial keyring support (#350)
Co-authored-by: Marin <marin.basic@medisante-group.com> Co-authored-by: Toni Kangas <toni.kangas@upcloud.com>
1 parent 1487526 commit 2089117

File tree

7 files changed

+83
-4
lines changed

7 files changed

+83
-4
lines changed

.github/workflows/test.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,20 @@ jobs:
1919
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
2020
with:
2121
go-version-file: 'go.mod'
22+
- name: Install keyring test dependencies on Ubuntu
23+
run: |
24+
sudo apt-get update
25+
sudo apt-get install -y gnome-keyring build-essential ca-certificates
26+
if: matrix.os == 'ubuntu-latest'
27+
- name: Run tests on Ubuntu with keyring
28+
run: |
29+
echo 'somecredstorepass' | gnome-keyring-daemon --unlock
30+
make test
31+
shell: dbus-run-session -- bash --noprofile --norc -eo pipefail {0}
32+
if: matrix.os == 'ubuntu-latest'
2233
- name: Run tests
2334
run: make test
35+
if: matrix.os != 'ubuntu-latest'
2436
- name: Test build
2537
run: make build
2638
- name: Test Runtime

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Experimental support for reading password or token from system keyring.
13+
1014
## [3.15.0] - 2025-02-26
1115

1216
### Added
@@ -41,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4145
- Prevent filename completion of flags that don't take filename args.
4246

4347
### Deprecated
48+
4449
- Deprecation of some commands and aliases ( new command names added to improve consistency )
4550
- Deprecated commands:
4651
- loadbalancer

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@ require (
1515
github.com/spf13/pflag v1.0.5
1616
github.com/spf13/viper v1.7.1
1717
github.com/stretchr/testify v1.9.0
18+
github.com/zalando/go-keyring v0.2.6
1819
golang.org/x/crypto v0.31.0
1920
golang.org/x/term v0.27.0
2021
gopkg.in/yaml.v3 v3.0.1
2122
k8s.io/client-go v0.28.4
2223
)
2324

2425
require (
26+
al.essio.dev/pkg/shellescape v1.5.1 // indirect
2527
github.com/ansel1/merry v1.5.0 // indirect
28+
github.com/danieljoos/wincred v1.2.2 // indirect
2629
github.com/davecgh/go-spew v1.1.1 // indirect
2730
github.com/fsnotify/fsnotify v1.4.9 // indirect
2831
github.com/go-logr/logr v1.2.4 // indirect
32+
github.com/godbus/dbus/v5 v5.1.0 // indirect
2933
github.com/gogo/protobuf v1.3.2 // indirect
3034
github.com/golang/protobuf v1.5.3 // indirect
3135
github.com/google/gofuzz v1.2.0 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
2+
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
13
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
24
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
35
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -44,6 +46,8 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
4446
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
4547
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
4648
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
49+
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
50+
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
4751
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4852
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4953
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -74,6 +78,8 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En
7478
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
7579
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
7680
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
81+
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
82+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
7783
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
7884
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
7985
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -105,6 +111,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
105111
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
106112
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
107113
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
114+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
115+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
108116
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
109117
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
110118
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -273,6 +281,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
273281
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
274282
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
275283
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
284+
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
285+
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
276286
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
277287
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
278288
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=

internal/clierrors/missing_credentials.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import "fmt"
55
var _ ClientError = MissingCredentialsError{}
66

77
type MissingCredentialsError struct {
8-
ConfigFile string
8+
ConfigFile string
9+
ServiceName string
910
}
1011

1112
func (err MissingCredentialsError) ErrorCode() int {
1213
return MissingCredentials
1314
}
1415

1516
func (err MissingCredentialsError) Error() string {
16-
return fmt.Sprintf("user credentials not found, these must be set in config file (%s) or via environment variables", err.ConfigFile)
17+
return fmt.Sprintf("user credentials not found, these must be set in config file (%s) or via environment variables or in a keyring (%s)", err.ConfigFile, err.ServiceName)
1718
}

internal/config/config.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/UpCloudLtd/upcloud-cli/v3/internal/clierrors"
1313
internal "github.com/UpCloudLtd/upcloud-cli/v3/internal/service"
14+
"github.com/zalando/go-keyring"
1415

1516
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/client"
1617
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/service"
@@ -34,6 +35,9 @@ const (
3435

3536
// env vars custom prefix
3637
envPrefix = "UPCLOUD"
38+
39+
// keyringServiceName is the name of the service to use when using the system keyring
40+
keyringServiceName = "UpCloud"
3741
)
3842

3943
var (
@@ -96,6 +100,26 @@ func (s *Config) Load() error {
96100
}
97101
}
98102

103+
// If no credentials are provided, check if token is stored in keyring
104+
if v.GetString("token") == "" && v.GetString("username") == "" && v.GetString("password") == "" {
105+
token, err := keyring.Get(keyringServiceName, "")
106+
if err == nil {
107+
if err := v.MergeConfigMap(map[string]interface{}{"token": token}); err != nil {
108+
return fmt.Errorf("unable to merge token from keyring: %w", err)
109+
}
110+
}
111+
}
112+
113+
// If only username is provided, check if password is stored in keyring
114+
if v.GetString("username") != "" && v.GetString("token") == "" && v.GetString("password") == "" {
115+
password, err := keyring.Get(keyringServiceName, v.GetString("username"))
116+
if err == nil {
117+
if err := v.MergeConfigMap(map[string]interface{}{"password": password}); err != nil {
118+
return fmt.Errorf("unable to merge password from keyring: %w", err)
119+
}
120+
}
121+
}
122+
99123
v.Set("config", v.ConfigFileUsed())
100124

101125
settings := v.AllSettings()
@@ -197,7 +221,7 @@ func (s *Config) CreateService() (internal.AllServices, error) {
197221
if s.GetString("config") != "" {
198222
configDetails = fmt.Sprintf("used %s", s.GetString("config"))
199223
}
200-
return nil, clierrors.MissingCredentialsError{ConfigFile: configDetails}
224+
return nil, clierrors.MissingCredentialsError{ConfigFile: configDetails, ServiceName: keyringServiceName}
201225
}
202226

203227
configs := []client.ConfigFn{

internal/config/config_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/stretchr/testify/assert"
9+
"github.com/zalando/go-keyring"
910
)
1011

1112
func TestConfig_LoadInvalidYAML(t *testing.T) {
@@ -34,7 +35,7 @@ func TestConfig_Load(t *testing.T) {
3435
assert.NotEmpty(t, cfg.GetString("password"))
3536
}
3637

37-
func TestVersion(t *testing.T) {
38+
func TestConfig_GetVersion(t *testing.T) {
3839
tests := []struct {
3940
name string
4041
expected string
@@ -61,3 +62,25 @@ func TestVersion(t *testing.T) {
6162
})
6263
}
6364
}
65+
66+
func TestConfig_LoadKeyring(t *testing.T) {
67+
// Note that configs defined in environment variables will override configs defined in the config file. Thus, this test will fail if credentials are currently defined as environment variables.
68+
cfg := New()
69+
tmpFile, err := os.CreateTemp(os.TempDir(), "")
70+
assert.NoError(t, err)
71+
_, err = tmpFile.WriteString("username: unittest")
72+
assert.NoError(t, err)
73+
74+
err = keyring.Set("UpCloud", "unittest", "unittest_password")
75+
assert.NoError(t, err)
76+
77+
cfg.GlobalFlags.ConfigFile = tmpFile.Name()
78+
err = cfg.Load()
79+
assert.NoError(t, err)
80+
assert.Equal(t, cfg.GetString("username"), "unittest")
81+
assert.Equal(t, "unittest_password", cfg.GetString("password"))
82+
t.Cleanup(func() {
83+
// remove test user from keyring
84+
assert.NoError(t, keyring.Delete("UpCloud", "unittest"))
85+
})
86+
}

0 commit comments

Comments
 (0)