From 0958e7262c10e81e6258b72d4ce2b82b58f6ca48 Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Tue, 2 Sep 2025 14:59:55 +0100 Subject: [PATCH 1/2] removes config files covered in core library --- internal/config/config.go | 44 -- internal/config/identifiers.go | 84 --- internal/config/mocks.go | 306 ----------- internal/config/profile.go | 598 ---------------------- internal/config/profile_test.go | 440 ---------------- internal/config/proxy_store.go | 146 ------ internal/config/proxy_store_test.go | 256 --------- internal/config/secure/go_keyring.go | 231 --------- internal/config/secure/go_keyring_test.go | 332 ------------ internal/config/secure/mocks.go | 97 ---- internal/config/store.go | 130 ----- internal/config/viper_store.go | 237 --------- 12 files changed, 2901 deletions(-) delete mode 100644 internal/config/config.go delete mode 100644 internal/config/identifiers.go delete mode 100644 internal/config/mocks.go delete mode 100644 internal/config/profile.go delete mode 100644 internal/config/profile_test.go delete mode 100644 internal/config/proxy_store.go delete mode 100644 internal/config/proxy_store_test.go delete mode 100644 internal/config/secure/go_keyring.go delete mode 100644 internal/config/secure/go_keyring_test.go delete mode 100644 internal/config/secure/mocks.go delete mode 100644 internal/config/store.go delete mode 100644 internal/config/viper_store.go diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 02f3ce54e8..0000000000 --- a/internal/config/config.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "bytes" - "os" - "path" -) - -// CLIConfigHome retrieves configHome path. -func CLIConfigHome() (string, error) { - home, err := os.UserConfigDir() - if err != nil { - return "", err - } - - return path.Join(home, "atlascli"), nil -} - -func Path(f string) (string, error) { - var p bytes.Buffer - - h, err := CLIConfigHome() - if err != nil { - return "", err - } - - p.WriteString(h) - p.WriteString(f) - return p.String(), nil -} diff --git a/internal/config/identifiers.go b/internal/config/identifiers.go deleted file mode 100644 index 5485ee79ba..0000000000 --- a/internal/config/identifiers.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "fmt" - "os" - "runtime" - "strings" - - "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/version" -) - -var ( - CLIUserType = newCLIUserTypeFromEnvs() - HostName = getConfigHostnameFromEnvs() - UserAgent = fmt.Sprintf("%s/%s (%s;%s;%s)", AtlasCLI, version.Version, runtime.GOOS, runtime.GOARCH, HostName) -) - -// newCLIUserTypeFromEnvs patches the user type information based on set env vars. -func newCLIUserTypeFromEnvs() string { - if value, ok := os.LookupEnv(CLIUserTypeEnv); ok { - return value - } - - return DefaultUser -} - -// getConfigHostnameFromEnvs patches the agent hostname based on set env vars. -func getConfigHostnameFromEnvs() string { - var builder strings.Builder - - envVars := []struct { - envName string - hostName string - }{ - {AtlasActionHostNameEnv, AtlasActionHostName}, - {GitHubActionsHostNameEnv, GitHubActionsHostName}, - {ContainerizedHostNameEnv, DockerContainerHostName}, - } - - for _, envVar := range envVars { - if envIsTrue(envVar.envName) { - appendToHostName(&builder, envVar.hostName) - } else { - appendToHostName(&builder, "-") - } - } - configHostName := builder.String() - - if isDefaultHostName(configHostName) { - return NativeHostName - } - return configHostName -} - -func appendToHostName(builder *strings.Builder, configVal string) { - if builder.Len() > 0 { - builder.WriteString("|") - } - builder.WriteString(configVal) -} - -// isDefaultHostName checks if the hostname is the default placeholder. -func isDefaultHostName(hostname string) bool { - // Using strings.Count for a more dynamic approach. - return strings.Count(hostname, "-") == strings.Count(hostname, "|")+1 -} - -func envIsTrue(env string) bool { - return IsTrue(os.Getenv(env)) -} diff --git a/internal/config/mocks.go b/internal/config/mocks.go deleted file mode 100644 index 59f15917c0..0000000000 --- a/internal/config/mocks.go +++ /dev/null @@ -1,306 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mongodb/atlas-cli-core/config (interfaces: Store,SecureStore) -// -// Generated by this command: -// -// mockgen -destination=./mocks.go -package=config github.com/mongodb/atlas-cli-core/config Store,SecureStore -// - -// Package config is a generated GoMock package. -package config - -import ( - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockStore is a mock of Store interface. -type MockStore struct { - ctrl *gomock.Controller - recorder *MockStoreMockRecorder - isgomock struct{} -} - -// MockStoreMockRecorder is the mock recorder for MockStore. -type MockStoreMockRecorder struct { - mock *MockStore -} - -// NewMockStore creates a new mock instance. -func NewMockStore(ctrl *gomock.Controller) *MockStore { - mock := &MockStore{ctrl: ctrl} - mock.recorder = &MockStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStore) EXPECT() *MockStoreMockRecorder { - return m.recorder -} - -// DeleteProfile mocks base method. -func (m *MockStore) DeleteProfile(profileName string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteProfile", profileName) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteProfile indicates an expected call of DeleteProfile. -func (mr *MockStoreMockRecorder) DeleteProfile(profileName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProfile", reflect.TypeOf((*MockStore)(nil).DeleteProfile), profileName) -} - -// GetGlobalValue mocks base method. -func (m *MockStore) GetGlobalValue(propertyName string) any { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetGlobalValue", propertyName) - ret0, _ := ret[0].(any) - return ret0 -} - -// GetGlobalValue indicates an expected call of GetGlobalValue. -func (mr *MockStoreMockRecorder) GetGlobalValue(propertyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGlobalValue", reflect.TypeOf((*MockStore)(nil).GetGlobalValue), propertyName) -} - -// GetHierarchicalValue mocks base method. -func (m *MockStore) GetHierarchicalValue(profileName, propertyName string) any { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetHierarchicalValue", profileName, propertyName) - ret0, _ := ret[0].(any) - return ret0 -} - -// GetHierarchicalValue indicates an expected call of GetHierarchicalValue. -func (mr *MockStoreMockRecorder) GetHierarchicalValue(profileName, propertyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHierarchicalValue", reflect.TypeOf((*MockStore)(nil).GetHierarchicalValue), profileName, propertyName) -} - -// GetProfileNames mocks base method. -func (m *MockStore) GetProfileNames() []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProfileNames") - ret0, _ := ret[0].([]string) - return ret0 -} - -// GetProfileNames indicates an expected call of GetProfileNames. -func (mr *MockStoreMockRecorder) GetProfileNames() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileNames", reflect.TypeOf((*MockStore)(nil).GetProfileNames)) -} - -// GetProfileStringMap mocks base method. -func (m *MockStore) GetProfileStringMap(profileName string) map[string]string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProfileStringMap", profileName) - ret0, _ := ret[0].(map[string]string) - return ret0 -} - -// GetProfileStringMap indicates an expected call of GetProfileStringMap. -func (mr *MockStoreMockRecorder) GetProfileStringMap(profileName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileStringMap", reflect.TypeOf((*MockStore)(nil).GetProfileStringMap), profileName) -} - -// GetProfileValue mocks base method. -func (m *MockStore) GetProfileValue(profileName, propertyName string) any { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProfileValue", profileName, propertyName) - ret0, _ := ret[0].(any) - return ret0 -} - -// GetProfileValue indicates an expected call of GetProfileValue. -func (mr *MockStoreMockRecorder) GetProfileValue(profileName, propertyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileValue", reflect.TypeOf((*MockStore)(nil).GetProfileValue), profileName, propertyName) -} - -// IsSecure mocks base method. -func (m *MockStore) IsSecure() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsSecure") - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsSecure indicates an expected call of IsSecure. -func (mr *MockStoreMockRecorder) IsSecure() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSecure", reflect.TypeOf((*MockStore)(nil).IsSecure)) -} - -// IsSetGlobal mocks base method. -func (m *MockStore) IsSetGlobal(propertyName string) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsSetGlobal", propertyName) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsSetGlobal indicates an expected call of IsSetGlobal. -func (mr *MockStoreMockRecorder) IsSetGlobal(propertyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSetGlobal", reflect.TypeOf((*MockStore)(nil).IsSetGlobal), propertyName) -} - -// RenameProfile mocks base method. -func (m *MockStore) RenameProfile(oldProfileName, newProfileName string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RenameProfile", oldProfileName, newProfileName) - ret0, _ := ret[0].(error) - return ret0 -} - -// RenameProfile indicates an expected call of RenameProfile. -func (mr *MockStoreMockRecorder) RenameProfile(oldProfileName, newProfileName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenameProfile", reflect.TypeOf((*MockStore)(nil).RenameProfile), oldProfileName, newProfileName) -} - -// Save mocks base method. -func (m *MockStore) Save() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Save") - ret0, _ := ret[0].(error) - return ret0 -} - -// Save indicates an expected call of Save. -func (mr *MockStoreMockRecorder) Save() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockStore)(nil).Save)) -} - -// SetGlobalValue mocks base method. -func (m *MockStore) SetGlobalValue(propertyName string, value any) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetGlobalValue", propertyName, value) -} - -// SetGlobalValue indicates an expected call of SetGlobalValue. -func (mr *MockStoreMockRecorder) SetGlobalValue(propertyName, value any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGlobalValue", reflect.TypeOf((*MockStore)(nil).SetGlobalValue), propertyName, value) -} - -// SetProfileValue mocks base method. -func (m *MockStore) SetProfileValue(profileName, propertyName string, value any) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetProfileValue", profileName, propertyName, value) -} - -// SetProfileValue indicates an expected call of SetProfileValue. -func (mr *MockStoreMockRecorder) SetProfileValue(profileName, propertyName, value any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProfileValue", reflect.TypeOf((*MockStore)(nil).SetProfileValue), profileName, propertyName, value) -} - -// MockSecureStore is a mock of SecureStore interface. -type MockSecureStore struct { - ctrl *gomock.Controller - recorder *MockSecureStoreMockRecorder - isgomock struct{} -} - -// MockSecureStoreMockRecorder is the mock recorder for MockSecureStore. -type MockSecureStoreMockRecorder struct { - mock *MockSecureStore -} - -// NewMockSecureStore creates a new mock instance. -func NewMockSecureStore(ctrl *gomock.Controller) *MockSecureStore { - mock := &MockSecureStore{ctrl: ctrl} - mock.recorder = &MockSecureStoreMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockSecureStore) EXPECT() *MockSecureStoreMockRecorder { - return m.recorder -} - -// Available mocks base method. -func (m *MockSecureStore) Available() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Available") - ret0, _ := ret[0].(bool) - return ret0 -} - -// Available indicates an expected call of Available. -func (mr *MockSecureStoreMockRecorder) Available() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Available", reflect.TypeOf((*MockSecureStore)(nil).Available)) -} - -// DeleteKey mocks base method. -func (m *MockSecureStore) DeleteKey(profileName, propertyName string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteKey", profileName, propertyName) -} - -// DeleteKey indicates an expected call of DeleteKey. -func (mr *MockSecureStoreMockRecorder) DeleteKey(profileName, propertyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteKey", reflect.TypeOf((*MockSecureStore)(nil).DeleteKey), profileName, propertyName) -} - -// DeleteProfile mocks base method. -func (m *MockSecureStore) DeleteProfile(profileName string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "DeleteProfile", profileName) -} - -// DeleteProfile indicates an expected call of DeleteProfile. -func (mr *MockSecureStoreMockRecorder) DeleteProfile(profileName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProfile", reflect.TypeOf((*MockSecureStore)(nil).DeleteProfile), profileName) -} - -// Get mocks base method. -func (m *MockSecureStore) Get(profileName, propertyName string) string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", profileName, propertyName) - ret0, _ := ret[0].(string) - return ret0 -} - -// Get indicates an expected call of Get. -func (mr *MockSecureStoreMockRecorder) Get(profileName, propertyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSecureStore)(nil).Get), profileName, propertyName) -} - -// Save mocks base method. -func (m *MockSecureStore) Save() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Save") - ret0, _ := ret[0].(error) - return ret0 -} - -// Save indicates an expected call of Save. -func (mr *MockSecureStoreMockRecorder) Save() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockSecureStore)(nil).Save)) -} - -// Set mocks base method. -func (m *MockSecureStore) Set(profileName, propertyName, value string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Set", profileName, propertyName, value) -} - -// Set indicates an expected call of Set. -func (mr *MockSecureStoreMockRecorder) Set(profileName, propertyName, value any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockSecureStore)(nil).Set), profileName, propertyName, value) -} diff --git a/internal/config/profile.go b/internal/config/profile.go deleted file mode 100644 index e4023499ce..0000000000 --- a/internal/config/profile.go +++ /dev/null @@ -1,598 +0,0 @@ -// Copyright 2020 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "context" - "errors" - "fmt" - "os" - "slices" - "sort" - "strings" - "time" - - "github.com/golang-jwt/jwt/v5" - "go.mongodb.org/atlas/auth" -) - -const ( - MongoCLIEnvPrefix = "MCLI" // MongoCLIEnvPrefix prefix for MongoCLI ENV variables - AtlasCLIEnvPrefix = "MONGODB_ATLAS" // AtlasCLIEnvPrefix prefix for AtlasCLI ENV variables - DefaultProfile = "default" // DefaultProfile default - CloudService = "cloud" // CloudService setting when using Atlas API - CloudGovService = "cloudgov" // CloudGovService setting when using Atlas API for Government - projectID = "project_id" - orgID = "org_id" - mongoShellPath = "mongosh_path" - configType = "toml" - service = "service" - AuthTypeField = "auth_type" - publicAPIKey = "public_api_key" - privateAPIKey = "private_api_key" - AccessTokenField = "access_token" - RefreshTokenField = "refresh_token" - ClientIDField = "client_id" - ClientSecretField = "client_secret" - OpsManagerURLField = "ops_manager_url" - AccountURLField = "account_url" - baseURL = "base_url" - apiVersion = "api_version" - output = "output" - fileFlags = os.O_CREATE | os.O_TRUNC | os.O_WRONLY - configPerm = 0600 - defaultPermissions = 0700 - skipUpdateCheck = "skip_update_check" - TelemetryEnabledProperty = "telemetry_enabled" - AtlasCLI = "atlascli" - ContainerizedHostNameEnv = "MONGODB_ATLAS_IS_CONTAINERIZED" - GitHubActionsHostNameEnv = "GITHUB_ACTIONS" - AtlasActionHostNameEnv = "ATLAS_GITHUB_ACTION" - CLIUserTypeEnv = "CLI_USER_TYPE" // CLIUserTypeEnv is used to separate MongoDB University users from default users - DefaultUser = "default" // Users that do NOT use ATLAS CLI with MongoDB University - UniversityUser = "university" // Users that uses ATLAS CLI with MongoDB University - NativeHostName = "native" - DockerContainerHostName = "container" - GitHubActionsHostName = "all_github_actions" - AtlasActionHostName = "atlascli_github_action" - LocalDeploymentImage = "local_deployment_image" // LocalDeploymentImage is the config key for the MongoDB Local Dev Docker image - Version = "version" // versionField is the key for the configuration version -) - -// Workaround to keep existing code working -// We cannot set the profile immediately because of a race condition which breaks all the unit tests -// -// The goal is to get rid of this, but we will need to do this gradually, since it's a large change that affects almost every command -func SetProfile(profile *Profile) { - defaultProfile = profile -} - -var ( - defaultProfile = &Profile{ - name: DefaultProfile, - configStore: NewInMemoryStore(), - } - profileContextKey = profileKey{} -) - -type Profile struct { - name string - configStore Store -} - -func NewProfile(name string, configStore Store) *Profile { - return &Profile{ - name: name, - configStore: configStore, - } -} - -type profileKey struct{} - -// Setting a value -func WithProfile(ctx context.Context, profile *Profile) context.Context { - return context.WithValue(ctx, profileContextKey, profile) -} - -// Getting a value -func ProfileFromContext(ctx context.Context) (*Profile, bool) { - if ctx == nil { - return nil, false - } - - profile, ok := ctx.Value(profileContextKey).(*Profile) - return profile, ok -} - -func AllProperties() []string { - return append(ProfileProperties(), GlobalProperties()...) -} - -func BooleanProperties() []string { - return []string{ - skipUpdateCheck, - TelemetryEnabledProperty, - } -} - -func ProfileProperties() []string { - return []string{ - AccessTokenField, - apiVersion, - baseURL, - OpsManagerURLField, - orgID, - output, - privateAPIKey, - projectID, - publicAPIKey, - ClientIDField, - ClientSecretField, - RefreshTokenField, - service, - } -} - -func GlobalProperties() []string { - return []string{ - Version, - LocalDeploymentImage, - mongoShellPath, - skipUpdateCheck, - TelemetryEnabledProperty, - } -} - -func IsTrue(s string) bool { - switch s { - case "t", "T", "true", "True", "TRUE", "y", "Y", "yes", "Yes", "YES", "1": - return true - default: - return false - } -} - -func Default() *Profile { - return defaultProfile -} - -func SetDefaultProfile(profile *Profile) { - defaultProfile = profile -} - -// List returns the names of available profiles. -func List() []string { return Default().List() } -func (p *Profile) List() []string { - return p.configStore.GetProfileNames() -} - -// Exists returns true if a profile with the give name exists. -func Exists(name string) bool { - return slices.Contains(List(), name) -} - -func Name() string { return Default().Name() } -func (p *Profile) Name() string { - return p.name -} - -var ErrProfileNameHasDots = errors.New("profile should not contain '.'") - -func validateName(name string) error { - if strings.Contains(name, ".") { - return fmt.Errorf("%w: %q", ErrProfileNameHasDots, name) - } - - return nil -} - -func SetName(name string) error { return Default().SetName(name) } -func (p *Profile) SetName(name string) error { - if err := validateName(name); err != nil { - return err - } - - p.name = strings.ToLower(name) - - return nil -} - -func Set(name string, value any) { Default().Set(name, value) } -func (p *Profile) Set(name string, value any) { - p.configStore.SetProfileValue(p.Name(), name, value) -} - -func SetGlobal(name string, value any) { Default().SetGlobal(name, value) } -func (p *Profile) SetGlobal(name string, value any) { - p.configStore.SetGlobalValue(name, value) -} - -func Get(name string) any { return Default().Get(name) } -func (p *Profile) Get(name string) any { - return p.configStore.GetHierarchicalValue(p.Name(), name) -} - -func GetString(name string) string { return Default().GetString(name) } -func (p *Profile) GetString(name string) string { - value := p.Get(name) - if value == nil { - return "" - } - return value.(string) -} - -func GetBool(name string) bool { return Default().GetBool(name) } -func (p *Profile) GetBool(name string) bool { - return p.GetBoolWithDefault(name, false) -} -func (p *Profile) GetBoolWithDefault(name string, defaultValue bool) bool { - value := p.Get(name) - switch v := value.(type) { - case bool: - return v - case string: - return IsTrue(v) - default: - return defaultValue - } -} - -func GetInt64(name string) int64 { return Default().GetInt64(name) } -func (p *Profile) GetInt64(name string) int64 { - value := p.Get(name) - if value == nil { - return 0 - } - return value.(int64) -} - -// Service get configured service. -func Service() string { return Default().Service() } -func (p *Profile) Service() string { - if p.configStore.IsSetGlobal(service) { - serviceValue, _ := p.configStore.GetGlobalValue(service).(string) - return serviceValue - } - - serviceValue, _ := p.configStore.GetProfileValue(p.Name(), service).(string) - return serviceValue -} - -func IsCloud() bool { - profile := Default() - return profile.Service() == "" || profile.Service() == CloudService || profile.Service() == CloudGovService -} - -// SetService set configured service. -func SetService(v string) { Default().SetService(v) } -func (p *Profile) SetService(v string) { - p.Set(service, v) -} - -type AuthMechanism string - -const ( - APIKeys AuthMechanism = "api_keys" - UserAccount AuthMechanism = "user_account" - ServiceAccount AuthMechanism = "service_account" - NoAuth AuthMechanism = "no_auth" -) - -// AuthType gets the configured auth type. -func AuthType() AuthMechanism { return Default().AuthType() } -func (p *Profile) AuthType() AuthMechanism { - return AuthMechanism(p.GetString(AuthTypeField)) -} - -// SetAuthType sets the configured auth type. -func SetAuthType(v AuthMechanism) { Default().SetAuthType(v) } -func (p *Profile) SetAuthType(v AuthMechanism) { - p.Set(AuthTypeField, string(v)) -} - -// PublicAPIKey get configured public api key. -func PublicAPIKey() string { return Default().PublicAPIKey() } -func (p *Profile) PublicAPIKey() string { - return p.GetString(publicAPIKey) -} - -// SetPublicAPIKey set configured publicAPIKey. -func SetPublicAPIKey(v string) { Default().SetPublicAPIKey(v) } -func (p *Profile) SetPublicAPIKey(v string) { - p.Set(publicAPIKey, v) -} - -// PrivateAPIKey get configured private api key. -func PrivateAPIKey() string { return Default().PrivateAPIKey() } -func (p *Profile) PrivateAPIKey() string { - return p.GetString(privateAPIKey) -} - -// SetPrivateAPIKey set configured private api key. -func SetPrivateAPIKey(v string) { Default().SetPrivateAPIKey(v) } -func (p *Profile) SetPrivateAPIKey(v string) { - p.Set(privateAPIKey, v) -} - -// AccessToken get configured access token. -func AccessToken() string { return Default().AccessToken() } -func (p *Profile) AccessToken() string { - return p.GetString(AccessTokenField) -} - -// SetAccessToken set configured access token. -func SetAccessToken(v string) { Default().SetAccessToken(v) } -func (p *Profile) SetAccessToken(v string) { - p.Set(AccessTokenField, v) -} - -// RefreshToken get configured refresh token. -func RefreshToken() string { return Default().RefreshToken() } -func (p *Profile) RefreshToken() string { - return p.GetString(RefreshTokenField) -} - -// SetRefreshToken set configured refresh token. -func SetRefreshToken(v string) { Default().SetRefreshToken(v) } -func (p *Profile) SetRefreshToken(v string) { - p.Set(RefreshTokenField, v) -} - -// ClientID get configured client ID. -func ClientID() string { return Default().ClientID() } -func (p *Profile) ClientID() string { - return p.GetString(ClientIDField) -} - -// SetClientID set configured client ID. -func SetClientID(v string) { Default().SetClientID(v) } -func (p *Profile) SetClientID(v string) { - p.Set(ClientIDField, v) -} - -// ClientSecret get configured client secret. -func ClientSecret() string { return Default().ClientSecret() } -func (p *Profile) ClientSecret() string { - return p.GetString(ClientSecretField) -} - -// SetClientSecret set configured client secret. -func SetClientSecret(v string) { Default().SetClientSecret(v) } -func (p *Profile) SetClientSecret(v string) { - p.Set(ClientSecretField, v) -} - -// Token gets configured auth.Token. -func Token() (*auth.Token, error) { return Default().Token() } -func (p *Profile) Token() (*auth.Token, error) { - if p.AccessToken() == "" || p.RefreshToken() == "" { - return nil, nil - } - c, err := p.tokenClaims() - if err != nil { - return nil, err - } - var e time.Time - if c.ExpiresAt != nil { - e = c.ExpiresAt.Time - } - t := &auth.Token{ - AccessToken: p.AccessToken(), - RefreshToken: p.RefreshToken(), - TokenType: "Bearer", - Expiry: e, - } - return t, nil -} - -// AccessTokenSubject will return the encoded subject in a JWT. -// This method won't verify the token signature, it's only safe to use to get the token claims. -func AccessTokenSubject() (string, error) { return Default().AccessTokenSubject() } -func (p *Profile) AccessTokenSubject() (string, error) { - c, err := p.tokenClaims() - if err != nil { - return "", err - } - return c.Subject, err -} - -func (p *Profile) tokenClaims() (jwt.RegisteredClaims, error) { - c := jwt.RegisteredClaims{} - // ParseUnverified is ok here, only want to make sure is a JWT and to get the claims for a Subject - _, _, err := new(jwt.Parser).ParseUnverified(p.AccessToken(), &c) - return c, err -} - -// APIVersion get the default API version. -func APIVersion() string { return Default().APIVersion() } -func (p *Profile) APIVersion() string { - return p.GetString(apiVersion) -} - -// SetAPIVersion sets the default API version. -func SetAPIVersion(v string) { Default().SetAPIVersion(v) } -func (p *Profile) SetAPIVersion(v string) { - p.Set(apiVersion, v) -} - -// OpsManagerURL get configured ops manager base url. -func OpsManagerURL() string { return Default().OpsManagerURL() } -func (p *Profile) OpsManagerURL() string { - return p.GetString(OpsManagerURLField) -} - -// SetOpsManagerURL set configured ops manager base url. -func SetOpsManagerURL(v string) { Default().SetOpsManagerURL(v) } -func (p *Profile) SetOpsManagerURL(v string) { - p.Set(OpsManagerURLField, v) -} - -// AccountURL gets the configured account base url. -func AccountURL() string { return Default().AccountURL() } -func (p *Profile) AccountURL() string { - return p.GetString(AccountURLField) -} - -// ProjectID get configured project ID. -func ProjectID() string { return Default().ProjectID() } -func (p *Profile) ProjectID() string { - return p.GetString(projectID) -} - -// SetProjectID sets the global project ID. -func SetProjectID(v string) { Default().SetProjectID(v) } -func (p *Profile) SetProjectID(v string) { - p.Set(projectID, v) -} - -// OrgID get configured organization ID. -func OrgID() string { return Default().OrgID() } -func (p *Profile) OrgID() string { - return p.GetString(orgID) -} - -// SetOrgID sets the global organization ID. -func SetOrgID(v string) { Default().SetOrgID(v) } -func (p *Profile) SetOrgID(v string) { - p.Set(orgID, v) -} - -// SkipUpdateCheck get the global skip update check. -func SkipUpdateCheck() bool { return Default().SkipUpdateCheck() } -func (p *Profile) SkipUpdateCheck() bool { - return p.GetBool(skipUpdateCheck) -} - -// SetSkipUpdateCheck sets the global skip update check. -func SetSkipUpdateCheck(v bool) { Default().SetSkipUpdateCheck(v) } -func (*Profile) SetSkipUpdateCheck(v bool) { - SetGlobal(skipUpdateCheck, v) -} - -// IsTelemetryEnabledSet return true if telemetry_enabled has been set. -func IsTelemetryEnabledSet() bool { return Default().IsTelemetryEnabledSet() } -func (p *Profile) IsTelemetryEnabledSet() bool { - return p.configStore.IsSetGlobal(TelemetryEnabledProperty) -} - -// TelemetryEnabled get the configured telemetry enabled value. -func TelemetryEnabled() bool { return Default().TelemetryEnabled() } -func (p *Profile) TelemetryEnabled() bool { - return isTelemetryFeatureAllowed() && p.GetBoolWithDefault(TelemetryEnabledProperty, true) -} - -// SetTelemetryEnabled sets the telemetry enabled value. -func SetTelemetryEnabled(v bool) { Default().SetTelemetryEnabled(v) } - -func (*Profile) SetTelemetryEnabled(v bool) { - if !isTelemetryFeatureAllowed() { - return - } - SetGlobal(TelemetryEnabledProperty, v) -} - -func boolEnv(key string) bool { - value, ok := os.LookupEnv(key) - return ok && IsTrue(value) -} - -func isTelemetryFeatureAllowed() bool { - doNotTrack := boolEnv("DO_NOT_TRACK") - return !doNotTrack -} - -// Output get configured output format. -func Output() string { return Default().Output() } -func (p *Profile) Output() string { - return p.GetString(output) -} - -// SetOutput sets the global output format. -func SetOutput(v string) { Default().SetOutput(v) } -func (p *Profile) SetOutput(v string) { - p.Set(output, v) -} - -// IsAccessSet return true if Service Account or API Keys credentials have been set up. -func IsAccessSet() bool { return Default().IsAccessSet() } -func (p *Profile) IsAccessSet() bool { - isSet := p.PublicAPIKey() != "" && p.PrivateAPIKey() != "" || - p.ClientID() != "" && p.ClientSecret() != "" - - return isSet -} - -// Map returns a map describing the configuration. -func Map() map[string]string { return Default().Map() } -func (p *Profile) Map() map[string]string { - settings := p.configStore.GetProfileStringMap(p.Name()) - profileSettings := make(map[string]string, len(settings)+1) - for k, v := range settings { - if k == privateAPIKey || k == AccessTokenField || k == RefreshTokenField { - profileSettings[k] = "redacted" - } else { - profileSettings[k] = v - } - } - - return profileSettings -} - -// SortedKeys returns the properties of the Profile sorted. -func SortedKeys() []string { return Default().SortedKeys() } -func (p *Profile) SortedKeys() []string { - config := p.Map() - keys := make([]string, 0, len(config)) - for k := range config { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -// Delete deletes an existing configuration. The profiles are reloaded afterwards, as -// this edits the file directly. -func Delete() error { return Default().Delete() } -func (p *Profile) Delete() error { - return p.configStore.DeleteProfile(p.Name()) -} - -// Rename replaces the Profile to a new Profile name, overwriting any Profile that existed before. -func Rename(newProfileName string) error { return Default().Rename(newProfileName) } -func (p *Profile) Rename(newProfileName string) error { - if err := validateName(newProfileName); err != nil { - return err - } - - return p.configStore.RenameProfile(p.Name(), newProfileName) -} - -// Save the configuration to disk. -func Save() error { return Default().Save() } -func (p *Profile) Save() error { - return p.configStore.Save() -} - -// GetLocalDeploymentImage returns the configured MongoDB Docker image URL. -func GetLocalDeploymentImage() string { return Default().GetLocalDeploymentImage() } -func (p *Profile) GetLocalDeploymentImage() string { - return p.GetString(LocalDeploymentImage) -} - -// SetLocalDeploymentImage sets the MongoDB Docker image URL. -func SetLocalDeploymentImage(v string) { Default().SetLocalDeploymentImage(v) } -func (*Profile) SetLocalDeploymentImage(v string) { - SetGlobal(LocalDeploymentImage, v) -} diff --git a/internal/config/profile_test.go b/internal/config/profile_test.go deleted file mode 100644 index a4d4010788..0000000000 --- a/internal/config/profile_test.go +++ /dev/null @@ -1,440 +0,0 @@ -// Copyright 2020 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "context" - "fmt" - "os" - "path" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestCLIConfigHome(t *testing.T) { - expHome, err := os.UserConfigDir() - if err != nil { - t.Fatalf("os.UserConfigDir() unexpected error: %v", err) - } - home, err := CLIConfigHome() - if err != nil { - t.Fatalf("AtlasCLIConfigHome() unexpected error: %v", err) - } - expected := path.Join(expHome, "atlascli") - if home != expected { - t.Errorf("AtlasCLIConfigHome() = %s; want '%s'", home, expected) - } -} - -func TestConfig_IsTrue(t *testing.T) { - tests := []struct { - input string - want bool - }{ - { - input: "true", - want: true, - }, - { - input: "True", - want: true, - }, - { - input: "t", - want: true, - }, - { - input: "T", - want: true, - }, - { - input: "TRUE", - want: true, - }, - { - input: "y", - want: true, - }, - { - input: "Y", - want: true, - }, - { - input: "yes", - want: true, - }, - { - input: "Yes", - want: true, - }, - { - input: "YES", - want: true, - }, - { - input: "1", - want: true, - }, - { - input: "false", - want: false, - }, - { - input: "f", - want: false, - }, - { - input: "unknown", - want: false, - }, - { - input: "0", - want: false, - }, - { - input: "", - want: false, - }, - } - for _, tt := range tests { - t.Run("", func(t *testing.T) { - t.Parallel() - if got := IsTrue(tt.input); got != tt.want { - t.Errorf("IsTrue() get: %v, want %v", got, tt.want) - } - }) - } -} - -func Test_getConfigHostname(t *testing.T) { - type fields struct { - containerizedEnv string - atlasActionEnv string - ghActionsEnv string - } - tests := []struct { - name string - fields fields - expectedHostName string - }{ - { - name: "sets native hostname when no hostname env var is set", - fields: fields{ - containerizedEnv: "", - atlasActionEnv: "", - ghActionsEnv: "", - }, - expectedHostName: NativeHostName, - }, - { - name: "sets container hostname when containerized env var is set", - fields: fields{ - containerizedEnv: "true", - atlasActionEnv: "", - ghActionsEnv: "", - }, - expectedHostName: "-|-|" + DockerContainerHostName, - }, - { - name: "sets atlas action hostname when containerized env var is set", - fields: fields{ - containerizedEnv: "", - atlasActionEnv: "true", - ghActionsEnv: "", - }, - expectedHostName: AtlasActionHostName + "|-|-", - }, - { - name: "sets github actions hostname when action env var is set", - fields: fields{ - containerizedEnv: "", - atlasActionEnv: "", - ghActionsEnv: "true", - }, - expectedHostName: "-|" + GitHubActionsHostName + "|-", - }, - { - name: "sets actions and containerized hostnames when both env vars are set", - fields: fields{ - containerizedEnv: "true", - atlasActionEnv: "true", - ghActionsEnv: "true", - }, - expectedHostName: AtlasActionHostName + "|" + GitHubActionsHostName + "|" + DockerContainerHostName, - }, - } - for _, tt := range tests { - f := tt.fields - expectedHostName := tt.expectedHostName - t.Run(tt.name, func(t *testing.T) { - t.Setenv(AtlasActionHostNameEnv, f.atlasActionEnv) - t.Setenv(GitHubActionsHostNameEnv, f.ghActionsEnv) - t.Setenv(ContainerizedHostNameEnv, f.containerizedEnv) - actualHostName := getConfigHostnameFromEnvs() - - assert.Equal(t, expectedHostName, actualHostName) - }) - } -} - -func TestProfile_Rename(t *testing.T) { - tests := []struct { - name string - wantErr bool - }{ - { - name: "default", - wantErr: false, - }, - { - name: "default-123", - wantErr: false, - }, - { - name: "default-test", - wantErr: false, - }, - { - name: "default.123", - wantErr: true, - }, - { - name: "default.test", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - var assertion require.ErrorAssertionFunc - if tt.wantErr { - assertion = require.Error - } else { - assertion = require.NoError - } - - ctrl := gomock.NewController(t) - configStore := NewMockStore(ctrl) - if !tt.wantErr { - configStore.EXPECT().RenameProfile(DefaultProfile, tt.name).Return(nil).Times(1) - } - - p := &Profile{ - name: DefaultProfile, - configStore: configStore, - } - - assertion(t, p.Rename(tt.name), fmt.Sprintf("Rename(%v)", tt.name)) - }) - } -} - -func TestProfile_SetName(t *testing.T) { - tests := []struct { - name string - wantErr require.ErrorAssertionFunc - }{ - { - name: "default", - wantErr: require.NoError, - }, - { - name: "default-123", - wantErr: require.NoError, - }, - { - name: "default-test", - wantErr: require.NoError, - }, - { - name: "default.123", - wantErr: require.Error, - }, - { - name: "default.test", - wantErr: require.Error, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - ctrl := gomock.NewController(t) - p := &Profile{ - name: tt.name, - configStore: NewMockStore(ctrl), - } - tt.wantErr(t, p.SetName(tt.name), fmt.Sprintf("SetName(%v)", tt.name)) - }) - } -} - -func TestWithProfile(t *testing.T) { - tests := []struct { - name string - profile *Profile - ctx context.Context - }{ - { - name: "add profile to empty context", - profile: &Profile{name: "test-profile"}, - ctx: t.Context(), - }, - { - name: "add profile to context with existing values", - profile: &Profile{name: "another-profile"}, - ctx: WithProfile(t.Context(), &Profile{name: "test-profile"}), - }, - { - name: "add nil profile", - profile: nil, - ctx: t.Context(), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := WithProfile(tt.ctx, tt.profile) - - // Verify context is not nil - require.NotNil(t, result) - - // Verify the profile was stored correctly - storedProfile, ok := result.Value(profileContextKey).(*Profile) - if tt.profile == nil { - assert.Nil(t, storedProfile) - } else { - require.True(t, ok) - assert.Equal(t, tt.profile, storedProfile) - assert.Equal(t, tt.profile.name, storedProfile.name) - } - - // Verify original context values are preserved - if tt.ctx != t.Context() { - if existingValue := result.Value("existing-key"); existingValue != nil { - assert.Equal(t, "existing-value", existingValue) - } - } - }) - } -} - -func TestProfileFromContext(t *testing.T) { - ctrl := gomock.NewController(t) - mockStore := NewMockStore(ctrl) - - tests := []struct { - name string - ctx context.Context - expectedProfile *Profile - expectedOk bool - }{ - { - name: "retrieve profile from context", - ctx: WithProfile(t.Context(), &Profile{name: "test-profile", configStore: mockStore}), - expectedProfile: &Profile{name: "test-profile", configStore: mockStore}, - expectedOk: true, - }, - { - name: "no profile in context", - ctx: t.Context(), - expectedProfile: nil, - expectedOk: false, - }, - { - name: "nil context", - ctx: nil, - expectedProfile: nil, - expectedOk: false, - }, - { - name: "context with nil profile", - ctx: WithProfile(t.Context(), nil), - expectedProfile: nil, - expectedOk: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - profile, ok := ProfileFromContext(tt.ctx) - - assert.Equal(t, tt.expectedOk, ok) - - if tt.expectedProfile == nil { - assert.Nil(t, profile) - } else { - require.NotNil(t, profile) - assert.Equal(t, tt.expectedProfile.name, profile.name) - assert.Equal(t, tt.expectedProfile.configStore, profile.configStore) - } - }) - } -} - -func TestWithProfile_ProfileFromContext_RoundTrip(t *testing.T) { - ctrl := gomock.NewController(t) - mockStore := NewMockStore(ctrl) - - // Create a test profile with some data - originalProfile := NewProfile("test-profile", mockStore) - - // Store it in context - ctx := WithProfile(t.Context(), originalProfile) - - // Retrieve it back - retrievedProfile, ok := ProfileFromContext(ctx) - - // Verify round trip worked - require.True(t, ok) - require.NotNil(t, retrievedProfile) - assert.Equal(t, originalProfile.name, retrievedProfile.name) - assert.Equal(t, originalProfile.configStore, retrievedProfile.configStore) - assert.Same(t, originalProfile, retrievedProfile) // Should be the exact same object -} - -func TestWithProfile_Multiple_Profiles(t *testing.T) { - ctrl := gomock.NewController(t) - mockStore1 := NewMockStore(ctrl) - mockStore2 := NewMockStore(ctrl) - - // Create multiple profiles - profile1 := NewProfile("profile1", mockStore1) - profile2 := NewProfile("profile2", mockStore2) - - // Add first profile to context - ctx1 := WithProfile(t.Context(), profile1) - - // Verify first profile is stored - retrieved1, ok := ProfileFromContext(ctx1) - require.True(t, ok) - assert.Equal(t, "profile1", retrieved1.name) - - // Override with second profile - ctx2 := WithProfile(ctx1, profile2) - - // Verify second profile overwrites the first - retrieved2, ok := ProfileFromContext(ctx2) - require.True(t, ok) - assert.Equal(t, "profile2", retrieved2.name) - - // Verify original context still has first profile - stillRetrieved1, ok := ProfileFromContext(ctx1) - require.True(t, ok) - assert.Equal(t, "profile1", stillRetrieved1.name) -} diff --git a/internal/config/proxy_store.go b/internal/config/proxy_store.go deleted file mode 100644 index 26a9f47235..0000000000 --- a/internal/config/proxy_store.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "errors" - "slices" - - "github.com/mongodb/atlas-cli-core/config/secure" - "github.com/spf13/afero" -) - -var SecureProperties = []string{ - publicAPIKey, - privateAPIKey, - AccessTokenField, - RefreshTokenField, - ClientIDField, - ClientSecretField, -} - -type ProxyStore struct { - insecure Store - secure SecureStore -} - -func NewDefaultStore() (Store, error) { - insecure, err := NewViperStore(afero.NewOsFs(), true) - - if err != nil { - return nil, err - } - - profileNames := insecure.GetProfileNames() - secureStore := secure.NewSecureStore(profileNames, SecureProperties) - - return NewStore(insecure, secureStore), nil -} - -func NewStore(insecureStore Store, secureStore SecureStore) Store { - if !secureStore.Available() { - return insecureStore - } - - return &ProxyStore{ - insecure: insecureStore, - secure: secureStore, - } -} - -func isSecureProperty(propertyName string) bool { - return slices.Contains(SecureProperties, propertyName) -} - -// Store interface implementation for ProxyStore - -func (*ProxyStore) IsSecure() bool { - return true -} - -func (p *ProxyStore) Save() error { - errs := []error{} - - if err := p.insecure.Save(); err != nil { - errs = append(errs, err) - } - - if err := p.secure.Save(); err != nil { - errs = append(errs, err) - } - - return errors.Join(errs...) -} - -func (p *ProxyStore) GetProfileNames() []string { - return p.insecure.GetProfileNames() -} - -func (p *ProxyStore) RenameProfile(oldProfileName string, newProfileName string) error { - return p.insecure.RenameProfile(oldProfileName, newProfileName) -} - -func (p *ProxyStore) DeleteProfile(profileName string) error { - return p.insecure.DeleteProfile(profileName) -} - -func (p *ProxyStore) GetHierarchicalValue(profileName string, propertyName string) any { - if isSecureProperty(propertyName) { - return p.secure.Get(profileName, propertyName) - } - return p.insecure.GetHierarchicalValue(profileName, propertyName) -} - -func (p *ProxyStore) SetProfileValue(profileName string, propertyName string, value any) { - if isSecureProperty(propertyName) { - if v, ok := value.(string); ok { - p.secure.Set(profileName, propertyName, v) - } - return - } - p.insecure.SetProfileValue(profileName, propertyName, value) -} - -func (p *ProxyStore) GetProfileValue(profileName string, propertyName string) any { - if isSecureProperty(propertyName) { - return p.secure.Get(profileName, propertyName) - } - return p.insecure.GetProfileValue(profileName, propertyName) -} - -func (p *ProxyStore) GetProfileStringMap(profileName string) map[string]string { - return p.insecure.GetProfileStringMap(profileName) -} - -func (p *ProxyStore) SetGlobalValue(propertyName string, value any) { - if isSecureProperty(propertyName) { - if v, ok := value.(string); ok { - p.secure.Set(DefaultProfile, propertyName, v) - } - return - } - p.insecure.SetGlobalValue(propertyName, value) -} - -func (p *ProxyStore) GetGlobalValue(propertyName string) any { - if isSecureProperty(propertyName) { - return p.secure.Get(DefaultProfile, propertyName) - } - return p.insecure.GetGlobalValue(propertyName) -} - -func (p *ProxyStore) IsSetGlobal(propertyName string) bool { - return p.insecure.IsSetGlobal(propertyName) -} diff --git a/internal/config/proxy_store_test.go b/internal/config/proxy_store_test.go deleted file mode 100644 index 60c93860ff..0000000000 --- a/internal/config/proxy_store_test.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -const ( - testProfileName = "test-profile" - testValue = "test-value" -) - -func TestNewStore(t *testing.T) { - tests := []struct { - name string - secureAvailable bool - expectProxyStore bool - }{ - { - name: "secure store available - returns ProxyStore", - secureAvailable: true, - expectProxyStore: true, - }, - { - name: "secure store unavailable - returns insecure store", - secureAvailable: false, - expectProxyStore: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctrl := gomock.NewController(t) - - mockInsecure := NewMockStore(ctrl) - mockSecure := NewMockSecureStore(ctrl) - - mockSecure.EXPECT().Available().Return(tt.secureAvailable) - - store := NewStore(mockInsecure, mockSecure) - - if tt.expectProxyStore { - proxyStore, ok := store.(*ProxyStore) - require.True(t, ok, "Expected ProxyStore") - assert.Equal(t, mockInsecure, proxyStore.insecure) - assert.Equal(t, mockSecure, proxyStore.secure) - } else { - assert.Equal(t, mockInsecure, store) - } - }) - } -} - -func TestProxyStore_IsSecure(t *testing.T) { - ctrl := gomock.NewController(t) - - mockInsecure := NewMockStore(ctrl) - mockSecure := NewMockSecureStore(ctrl) - - store := &ProxyStore{ - insecure: mockInsecure, - secure: mockSecure, - } - - assert.True(t, store.IsSecure()) -} - -func TestProxyStore_PropertyRouting(t *testing.T) { - testCases := []struct { - propertyName string - isSecure bool - }{ - {publicAPIKey, true}, - {privateAPIKey, true}, - {AccessTokenField, true}, - {RefreshTokenField, true}, - {"base_url", false}, - {"project_id", false}, - {"org_id", false}, - {"output", false}, - {"service", false}, - } - - methods := []struct { - name string - testFunc func(t *testing.T, store *ProxyStore, propertyName string, isSecure bool) - }{ - { - name: "GetHierarchicalValue", - testFunc: testGetHierarchicalValue, - }, - { - name: "SetProfileValue", - testFunc: testSetProfileValue, - }, - { - name: "GetProfileValue", - testFunc: testGetProfileValue, - }, - { - name: "SetGlobalValue", - testFunc: testSetGlobalValue, - }, - { - name: "GetGlobalValue", - testFunc: testGetGlobalValue, - }, - } - - for _, method := range methods { - for _, tc := range testCases { - t.Run(method.name+"_"+tc.propertyName, func(t *testing.T) { - ctrl := gomock.NewController(t) - - mockInsecure := NewMockStore(ctrl) - mockSecure := NewMockSecureStore(ctrl) - - store := &ProxyStore{ - insecure: mockInsecure, - secure: mockSecure, - } - - method.testFunc(t, store, tc.propertyName, tc.isSecure) - }) - } - } -} - -func testGetHierarchicalValue(t *testing.T, store *ProxyStore, propertyName string, isSecure bool) { - t.Helper() - profileName := testProfileName - expectedValue := testValue - - if isSecure { - store.secure.(*MockSecureStore).EXPECT(). - Get(profileName, propertyName). - Return(expectedValue) - } else { - store.insecure.(*MockStore).EXPECT(). - GetHierarchicalValue(profileName, propertyName). - Return(expectedValue) - } - - result := store.GetHierarchicalValue(profileName, propertyName) - assert.Equal(t, expectedValue, result) -} - -func testSetProfileValue(t *testing.T, store *ProxyStore, propertyName string, isSecure bool) { - t.Helper() - profileName := testProfileName - value := testValue - - if isSecure { - store.secure.(*MockSecureStore).EXPECT(). - Set(profileName, propertyName, value) - } else { - store.insecure.(*MockStore).EXPECT(). - SetProfileValue(profileName, propertyName, value) - } - - store.SetProfileValue(profileName, propertyName, value) -} - -func testGetProfileValue(t *testing.T, store *ProxyStore, propertyName string, isSecure bool) { - t.Helper() - profileName := testProfileName - expectedValue := testValue - - if isSecure { - store.secure.(*MockSecureStore).EXPECT(). - Get(profileName, propertyName). - Return(expectedValue) - } else { - store.insecure.(*MockStore).EXPECT(). - GetProfileValue(profileName, propertyName). - Return(expectedValue) - } - - result := store.GetProfileValue(profileName, propertyName) - assert.Equal(t, expectedValue, result) -} - -func testSetGlobalValue(t *testing.T, store *ProxyStore, propertyName string, isSecure bool) { - t.Helper() - value := testValue - - if isSecure { - store.secure.(*MockSecureStore).EXPECT(). - Set(DefaultProfile, propertyName, value) - } else { - store.insecure.(*MockStore).EXPECT(). - SetGlobalValue(propertyName, value) - } - - store.SetGlobalValue(propertyName, value) -} - -func testGetGlobalValue(t *testing.T, store *ProxyStore, propertyName string, isSecure bool) { - t.Helper() - expectedValue := testValue - - if isSecure { - store.secure.(*MockSecureStore).EXPECT(). - Get(DefaultProfile, propertyName). - Return(expectedValue) - } else { - store.insecure.(*MockStore).EXPECT(). - GetGlobalValue(propertyName). - Return(expectedValue) - } - - result := store.GetGlobalValue(propertyName) - assert.Equal(t, expectedValue, result) -} - -func TestIsSecureProperty(t *testing.T) { - tests := []struct { - propertyName string - expected bool - }{ - {publicAPIKey, true}, - {privateAPIKey, true}, - {AccessTokenField, true}, - {RefreshTokenField, true}, - {"base_url", false}, - {"project_id", false}, - {"org_id", false}, - {"output", false}, - {"", false}, - {"random_property", false}, - } - - for _, tt := range tests { - t.Run(tt.propertyName, func(t *testing.T) { - result := isSecureProperty(tt.propertyName) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/internal/config/secure/go_keyring.go b/internal/config/secure/go_keyring.go deleted file mode 100644 index 855f117b7c..0000000000 --- a/internal/config/secure/go_keyring.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package secure - -import ( - "errors" - "slices" - - "github.com/zalando/go-keyring" -) - -//go:generate go tool go.uber.org/mock/mockgen -destination=./mocks.go -package=secure github.com/mongodb/atlas-cli-core/config/secure KeyringClient - -const servicePrefix = "atlascli_" - -func createServiceName(profileName string) string { - return servicePrefix + profileName -} - -// KeyringClient abstracts keyring operations for easier testing -type KeyringClient interface { - Set(service, user, password string) error - Get(service, user string) (string, error) - Delete(service, user string) error - DeleteAll(service string) error -} - -// DefaultKeyringClient implements KeyringClient using the zalando/go-keyring library -type DefaultKeyringClient struct{} - -func NewDefaultKeyringClient() *DefaultKeyringClient { - return &DefaultKeyringClient{} -} - -func (*DefaultKeyringClient) Set(service, user, password string) error { - return keyring.Set(service, user, password) -} - -func (*DefaultKeyringClient) Get(service, user string) (string, error) { - value, err := keyring.Get(service, user) - if err != nil && !errors.Is(err, keyring.ErrNotFound) { - return "", err - } - - return value, nil -} - -func (*DefaultKeyringClient) Delete(service, user string) error { - return keyring.Delete(service, user) -} - -func (*DefaultKeyringClient) DeleteAll(service string) error { - return keyring.DeleteAll(service) -} - -// Operation types for tracking changes -type operationType int - -const ( - opSet operationType = iota - opDelete - opDeleteProfile -) - -// pendingOperation represents a change that needs to be persisted -type pendingOperation struct { - opType operationType - profileName string - propertyName string - value string -} - -type KeyringStore struct { - // Available indicates if the keyring is available. - available bool - // In-memory cache: map[profileName]map[propertyName]value - cache map[string]map[string]string - // List of operations to perform when Save() is called - pendingOps []pendingOperation - // Properties that are considered secure - secureProperties []string - // KeyringClient for keyring operations - keyringClient KeyringClient -} - -func NewSecureStore(profileNames []string, secureProperties []string) *KeyringStore { - return NewSecureStoreWithClient(profileNames, secureProperties, NewDefaultKeyringClient()) -} - -func NewSecureStoreWithClient(profileNames []string, secureProperties []string, keyringClient KeyringClient) *KeyringStore { - store := &KeyringStore{ - cache: make(map[string]map[string]string), - pendingOps: make([]pendingOperation, 0), - secureProperties: secureProperties, - keyringClient: keyringClient, - } - - // Check if the keyring is available. - // We do this my marking the store as available if we can get a value from the keyring. - available := false - attemptedToRead := false - - // Load all existing secure properties for all profiles into memory -outer: - for _, profileName := range profileNames { - store.cache[profileName] = make(map[string]string) - for _, propertyName := range secureProperties { - attemptedToRead = true - - // Attempt to read the value from the keyring. - value, err := keyringClient.Get(createServiceName(profileName), propertyName) - - // If the store returns an error, break the loop. - if err != nil { - break outer - } - - store.cache[profileName][propertyName] = value - available = true - } - } - - // If we didn't attempt to read, try to read a value from the default service. - if !attemptedToRead { - _, err := keyringClient.Get(createServiceName("default"), "test") - available = err == nil - } - - // Set the available flag. - store.available = available - - return store -} - -func (k *KeyringStore) Available() bool { - return k.available -} - -func (k *KeyringStore) Save() error { - // Process all pending operations - for _, op := range k.pendingOps { - switch op.opType { - case opSet: - if err := k.keyringClient.Set(createServiceName(op.profileName), op.propertyName, op.value); err != nil { - return err - } - case opDelete: - if err := k.keyringClient.Delete(createServiceName(op.profileName), op.propertyName); err != nil { - return err - } - case opDeleteProfile: - if err := k.keyringClient.DeleteAll(createServiceName(op.profileName)); err != nil { - return err - } - } - } - - // Clear pending operations after successful save - k.pendingOps = make([]pendingOperation, 0) - return nil -} - -func (k *KeyringStore) Set(profileName string, propertyName string, value string) { - // Ignore properties that are not in SecureProperties - if !slices.Contains(k.secureProperties, propertyName) { - return - } - - // Initialize profile map if it doesn't exist - if k.cache[profileName] == nil { - k.cache[profileName] = make(map[string]string) - } - - // Update in-memory cache - k.cache[profileName][propertyName] = value - - // Add to pending operations - k.pendingOps = append(k.pendingOps, pendingOperation{ - opType: opSet, - profileName: profileName, - propertyName: propertyName, - value: value, - }) -} - -func (k *KeyringStore) Get(profileName string, propertyName string) string { - // Check if profile exists in cache - if profileCache, exists := k.cache[profileName]; exists { - if value, exists := profileCache[propertyName]; exists { - return value - } - } - return "" -} - -func (k *KeyringStore) DeleteKey(profileName string, propertyName string) { - // Remove from in-memory cache if it exists - if profileCache, exists := k.cache[profileName]; exists { - delete(profileCache, propertyName) - } - - // Add to pending operations - k.pendingOps = append(k.pendingOps, pendingOperation{ - opType: opDelete, - profileName: profileName, - propertyName: propertyName, - }) -} - -func (k *KeyringStore) DeleteProfile(profileName string) { - // Remove from in-memory cache - delete(k.cache, profileName) - - // Add to pending operations - k.pendingOps = append(k.pendingOps, pendingOperation{ - opType: opDeleteProfile, - profileName: profileName, - }) -} diff --git a/internal/config/secure/go_keyring_test.go b/internal/config/secure/go_keyring_test.go deleted file mode 100644 index d6bfaf2f17..0000000000 --- a/internal/config/secure/go_keyring_test.go +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package secure - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestNewSecureStore(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockKeyring := NewMockKeyringClient(ctrl) - profileNames := []string{"profile1", "profile2", "profile3"} - secureProperties := []string{"public_api_key", "private_api_key", "access_token", "refresh_token"} - - // Setup expectations for loading existing values - mockKeyring.EXPECT().Get("atlascli_profile1", "public_api_key").Return("existing_public_1", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "private_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "access_token").Return("existing_access_1", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "refresh_token").Return("", nil) - - mockKeyring.EXPECT().Get("atlascli_profile2", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile2", "private_api_key").Return("existing_private_2", nil) - mockKeyring.EXPECT().Get("atlascli_profile2", "access_token").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile2", "refresh_token").Return("", nil) - - mockKeyring.EXPECT().Get("atlascli_profile3", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile3", "private_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile3", "access_token").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile3", "refresh_token").Return("", nil) - - store := NewSecureStoreWithClient(profileNames, secureProperties, mockKeyring) - - // Verify cache structure is initialized - assert.NotNil(t, store.cache) - assert.NotNil(t, store.cache["profile1"]) - assert.NotNil(t, store.cache["profile2"]) - assert.NotNil(t, store.cache["profile3"]) - - // Verify existing values were loaded correctly - assert.Equal(t, "existing_public_1", store.cache["profile1"]["public_api_key"]) - assert.Equal(t, "existing_access_1", store.cache["profile1"]["access_token"]) - assert.Equal(t, "existing_private_2", store.cache["profile2"]["private_api_key"]) - - // Verify secure properties are stored - assert.Equal(t, secureProperties, store.secureProperties) - - // Verify pending operations is empty - assert.Empty(t, store.pendingOps) - - // Verify keyring client is set - assert.Equal(t, mockKeyring, store.keyringClient) -} - -func TestKeyringStore_Set(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockKeyring := NewMockKeyringClient(ctrl) - profileNames := []string{"profile1"} - secureProperties := []string{"public_api_key", "private_api_key"} - - // Setup expectations for loading (no existing values) - mockKeyring.EXPECT().Get("atlascli_profile1", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "private_api_key").Return("", nil) - - store := NewSecureStoreWithClient(profileNames, secureProperties, mockKeyring) - - t.Run("Set secure property", func(t *testing.T) { - store.Set("profile1", "public_api_key", "test_public_key") - - // Verify value is in cache - assert.Equal(t, "test_public_key", store.cache["profile1"]["public_api_key"]) - - // Verify pending operation is added - assert.Len(t, store.pendingOps, 1) - assert.Equal(t, opSet, store.pendingOps[0].opType) - assert.Equal(t, "profile1", store.pendingOps[0].profileName) - assert.Equal(t, "public_api_key", store.pendingOps[0].propertyName) - assert.Equal(t, "test_public_key", store.pendingOps[0].value) - }) - - t.Run("Set non-secure property", func(t *testing.T) { - store.Set("profile1", "non_secure_prop", "value") - - // Verify value is NOT in cache - assert.Empty(t, store.cache["profile1"]["non_secure_prop"]) - - // Verify no new pending operation is added - assert.Len(t, store.pendingOps, 1) // Still only the one from previous test - }) - - t.Run("Set for new profile", func(t *testing.T) { - store.Set("new_profile", "private_api_key", "new_private_key") - - // Verify profile is created and value is set - assert.Equal(t, "new_private_key", store.cache["new_profile"]["private_api_key"]) - - // Verify pending operation is added - assert.Len(t, store.pendingOps, 2) - }) -} - -func TestKeyringStore_Get(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockKeyring := NewMockKeyringClient(ctrl) - profileNames := []string{"profile1"} - secureProperties := []string{"public_api_key", "private_api_key"} - - // Setup expectations for loading (no existing values) - mockKeyring.EXPECT().Get("atlascli_profile1", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "private_api_key").Return("", nil) - - store := NewSecureStoreWithClient(profileNames, secureProperties, mockKeyring) - - // Set a value in cache - store.cache["profile1"]["public_api_key"] = "cached_value" - - t.Run("Get existing value", func(t *testing.T) { - value := store.Get("profile1", "public_api_key") - assert.Equal(t, "cached_value", value) - }) - - t.Run("Get non-existing property", func(t *testing.T) { - value := store.Get("profile1", "non_existing") - assert.Empty(t, value) - }) - - t.Run("Get from non-existing profile", func(t *testing.T) { - value := store.Get("non_existing_profile", "public_api_key") - assert.Empty(t, value) - }) -} - -func TestKeyringStore_DeleteKey(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockKeyring := NewMockKeyringClient(ctrl) - profileNames := []string{"profile1"} - secureProperties := []string{"public_api_key", "private_api_key"} - - // Setup expectations for loading (no existing values) - mockKeyring.EXPECT().Get("atlascli_profile1", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "private_api_key").Return("", nil) - - store := NewSecureStoreWithClient(profileNames, secureProperties, mockKeyring) - - // Set some values in cache - store.cache["profile1"]["public_api_key"] = "profile1_value1" - store.cache["profile1"]["private_api_key"] = "profile1_value2" - - t.Run("Delete existing key", func(t *testing.T) { - store.DeleteKey("profile1", "public_api_key") - - // Verify value is removed from cache - _, exists := store.cache["profile1"]["public_api_key"] - assert.False(t, exists) - - // Verify other values remain - assert.Equal(t, "profile1_value2", store.cache["profile1"]["private_api_key"]) - - // Verify pending operation is added - assert.Len(t, store.pendingOps, 1) - assert.Equal(t, opDelete, store.pendingOps[0].opType) - assert.Equal(t, "profile1", store.pendingOps[0].profileName) - assert.Equal(t, "public_api_key", store.pendingOps[0].propertyName) - }) - - t.Run("Delete from non-existing profile", func(t *testing.T) { - store.DeleteKey("non_existing", "public_api_key") - - // Verify pending operation is still added - assert.Len(t, store.pendingOps, 2) - }) -} - -func TestKeyringStore_DeleteProfile(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockKeyring := NewMockKeyringClient(ctrl) - profileNames := []string{"profile1", "profile2"} - secureProperties := []string{"public_api_key", "private_api_key"} - - // Setup expectations for loading (no existing values) - mockKeyring.EXPECT().Get("atlascli_profile1", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "private_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile2", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile2", "private_api_key").Return("", nil) - - store := NewSecureStoreWithClient(profileNames, secureProperties, mockKeyring) - - // Set some values in cache - store.cache["profile1"]["public_api_key"] = "value1" - store.cache["profile2"]["private_api_key"] = "value2" - - t.Run("Delete existing profile", func(t *testing.T) { - store.DeleteProfile("profile1") - - // Verify profile is removed from cache - _, exists := store.cache["profile1"] - assert.False(t, exists) - - // Verify other profiles remain - assert.Equal(t, "value2", store.cache["profile2"]["private_api_key"]) - - // Verify pending operation is added - assert.Len(t, store.pendingOps, 1) - assert.Equal(t, opDeleteProfile, store.pendingOps[0].opType) - assert.Equal(t, "profile1", store.pendingOps[0].profileName) - }) -} - -func TestKeyringStore_Save(t *testing.T) { - ctrl := gomock.NewController(t) - - mockKeyring := NewMockKeyringClient(ctrl) - profileNames := []string{"profile1"} - secureProperties := []string{"public_api_key", "private_api_key"} - - // Setup expectations for loading (no existing values) - mockKeyring.EXPECT().Get("atlascli_profile1", "public_api_key").Return("", nil) - mockKeyring.EXPECT().Get("atlascli_profile1", "private_api_key").Return("", nil) - - store := NewSecureStoreWithClient(profileNames, secureProperties, mockKeyring) - - t.Run("Save successful operations", func(t *testing.T) { - // Add some pending operations - store.Set("profile1", "public_api_key", "new_public_key") - store.Set("profile1", "private_api_key", "new_private_key") - store.DeleteKey("profile1", "old_key") - - assert.Len(t, store.pendingOps, 3) - - // Setup expectations for Save operation - mockKeyring.EXPECT().Set("atlascli_profile1", "public_api_key", "new_public_key").Return(nil) - mockKeyring.EXPECT().Set("atlascli_profile1", "private_api_key", "new_private_key").Return(nil) - mockKeyring.EXPECT().Delete("atlascli_profile1", "old_key").Return(nil) - - err := store.Save() - require.NoError(t, err) - - // Verify pending operations are cleared - assert.Empty(t, store.pendingOps) - }) - - t.Run("Save with error", func(t *testing.T) { - // Add a pending operation - store.Set("profile1", "public_api_key", "failing_key") - - // Mock an error - mockKeyring.EXPECT().Set("atlascli_profile1", "public_api_key", "failing_key").Return(errors.New("keyring error")) - - err := store.Save() - require.Error(t, err) - assert.Contains(t, err.Error(), "keyring error") - - // Verify pending operations are NOT cleared on error - assert.Len(t, store.pendingOps, 1) - }) - - t.Run("Save delete profile operation", func(t *testing.T) { - // Clear any previous operations - store.pendingOps = []pendingOperation{} - - store.DeleteProfile("test_profile") - - // Mock successful delete all - mockKeyring.EXPECT().DeleteAll("atlascli_test_profile").Return(nil) - - err := store.Save() - require.NoError(t, err) - - // Verify pending operations are cleared - assert.Empty(t, store.pendingOps) - }) -} - -func TestKeyringStore_Available(t *testing.T) { - t.Parallel() - - t.Run("Available when keyring works", func(t *testing.T) { - ctrl := gomock.NewController(t) - mockKeyring := NewMockKeyringClient(ctrl) - // Mock successful get call - mockKeyring.EXPECT().Get("atlascli_default", "demo_secret").Return("my_demo_secret", nil) - - store := NewSecureStoreWithClient([]string{"default"}, []string{"demo_secret"}, mockKeyring) - assert.True(t, store.Available()) - }) - - t.Run("Available when keyring works, but no profiles", func(t *testing.T) { - ctrl := gomock.NewController(t) - mockKeyring := NewMockKeyringClient(ctrl) - // Mock successful get call - mockKeyring.EXPECT().Get("atlascli_default", "test").Return("test", nil) - - store := NewSecureStoreWithClient([]string{}, []string{"demo_secret"}, mockKeyring) - assert.True(t, store.Available()) - }) - - t.Run("Not available when keyring fails", func(t *testing.T) { - ctrl := gomock.NewController(t) - mockKeyring := NewMockKeyringClient(ctrl) - // Mock failed get call - mockKeyring.EXPECT().Get("atlascli_default", "test").Return("", errors.New("keyring error")) - - store := NewSecureStoreWithClient([]string{}, []string{}, mockKeyring) - assert.False(t, store.Available()) - }) -} diff --git a/internal/config/secure/mocks.go b/internal/config/secure/mocks.go deleted file mode 100644 index c805d13457..0000000000 --- a/internal/config/secure/mocks.go +++ /dev/null @@ -1,97 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mongodb/atlas-cli-core/config/secure (interfaces: KeyringClient) -// -// Generated by this command: -// -// mockgen -destination=./mocks.go -package=secure github.com/mongodb/atlas-cli-core/config/secure KeyringClient -// - -// Package secure is a generated GoMock package. -package secure - -import ( - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockKeyringClient is a mock of KeyringClient interface. -type MockKeyringClient struct { - ctrl *gomock.Controller - recorder *MockKeyringClientMockRecorder - isgomock struct{} -} - -// MockKeyringClientMockRecorder is the mock recorder for MockKeyringClient. -type MockKeyringClientMockRecorder struct { - mock *MockKeyringClient -} - -// NewMockKeyringClient creates a new mock instance. -func NewMockKeyringClient(ctrl *gomock.Controller) *MockKeyringClient { - mock := &MockKeyringClient{ctrl: ctrl} - mock.recorder = &MockKeyringClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockKeyringClient) EXPECT() *MockKeyringClientMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockKeyringClient) Delete(service, user string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", service, user) - ret0, _ := ret[0].(error) - return ret0 -} - -// Delete indicates an expected call of Delete. -func (mr *MockKeyringClientMockRecorder) Delete(service, user any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockKeyringClient)(nil).Delete), service, user) -} - -// DeleteAll mocks base method. -func (m *MockKeyringClient) DeleteAll(service string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAll", service) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteAll indicates an expected call of DeleteAll. -func (mr *MockKeyringClientMockRecorder) DeleteAll(service any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAll", reflect.TypeOf((*MockKeyringClient)(nil).DeleteAll), service) -} - -// Get mocks base method. -func (m *MockKeyringClient) Get(service, user string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", service, user) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockKeyringClientMockRecorder) Get(service, user any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKeyringClient)(nil).Get), service, user) -} - -// Set mocks base method. -func (m *MockKeyringClient) Set(service, user, password string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Set", service, user, password) - ret0, _ := ret[0].(error) - return ret0 -} - -// Set indicates an expected call of Set. -func (mr *MockKeyringClientMockRecorder) Set(service, user, password any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockKeyringClient)(nil).Set), service, user, password) -} diff --git a/internal/config/store.go b/internal/config/store.go deleted file mode 100644 index aafd0aa425..0000000000 --- a/internal/config/store.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "slices" - "sort" - - "github.com/spf13/viper" -) - -//go:generate go tool go.uber.org/mock/mockgen -destination=./mocks.go -package=config github.com/mongodb/atlas-cli-core/config Store,SecureStore - -type Store interface { - IsSecure() bool - Save() error - - GetProfileNames() []string - RenameProfile(oldProfileName string, newProfileName string) error - DeleteProfile(profileName string) error - - GetHierarchicalValue(profileName string, propertyName string) any - - SetProfileValue(profileName string, propertyName string, value any) - GetProfileValue(profileName string, propertyName string) any - GetProfileStringMap(profileName string) map[string]string - - SetGlobalValue(propertyName string, value any) - GetGlobalValue(propertyName string) any - IsSetGlobal(propertyName string) bool -} - -type SecureStore interface { - Available() bool - Save() error - - Set(profileName string, propertyName string, value string) - Get(profileName string, propertyName string) string - DeleteKey(profileName string, propertyName string) - DeleteProfile(profileName string) -} - -// Temporary InMemoryStore to mimick legacy behavior -// Will be removed when we get rid of static references in the profile -type InMemoryStore struct { - v *viper.Viper -} - -func NewInMemoryStore() *InMemoryStore { - return &InMemoryStore{ - v: viper.New(), - } -} - -func (*InMemoryStore) IsSecure() bool { - return true -} - -func (*InMemoryStore) Save() error { - return nil -} - -func (s *InMemoryStore) GetProfileNames() []string { - allKeys := s.v.AllKeys() - - profileNames := make([]string, 0, len(allKeys)) - for _, key := range allKeys { - if !slices.Contains(GlobalProperties(), key) { - profileNames = append(profileNames, key) - } - } - // keys in maps are non-deterministic, trying to give users a consistent output - sort.Strings(profileNames) - return profileNames -} - -func (*InMemoryStore) RenameProfile(_, _ string) error { - panic("not implemented") -} - -func (*InMemoryStore) DeleteProfile(_ string) error { - panic("not implemented") -} - -func (s *InMemoryStore) GetHierarchicalValue(profileName string, propertyName string) any { - if s.v.IsSet(propertyName) && s.v.Get(propertyName) != "" { - return s.v.Get(propertyName) - } - settings := s.v.GetStringMap(profileName) - return settings[propertyName] -} - -func (s *InMemoryStore) SetProfileValue(profileName string, propertyName string, value any) { - settings := s.v.GetStringMap(profileName) - settings[propertyName] = value - s.v.Set(profileName, settings) -} - -func (s *InMemoryStore) GetProfileValue(profileName string, propertyName string) any { - settings := s.v.GetStringMap(profileName) - return settings[propertyName] -} - -func (s *InMemoryStore) GetProfileStringMap(profileName string) map[string]string { - return s.v.GetStringMapString(profileName) -} - -func (s *InMemoryStore) SetGlobalValue(propertyName string, value any) { - s.v.Set(propertyName, value) -} - -func (s *InMemoryStore) GetGlobalValue(propertyName string) any { - return s.v.Get(propertyName) -} - -func (s *InMemoryStore) IsSetGlobal(propertyName string) bool { - return s.v.IsSet(propertyName) -} diff --git a/internal/config/viper_store.go b/internal/config/viper_store.go deleted file mode 100644 index 3ff1a09261..0000000000 --- a/internal/config/viper_store.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2025 MongoDB Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "errors" - "os" - "path/filepath" - "slices" - "sort" - "strings" - - "github.com/pelletier/go-toml" - "github.com/spf13/afero" - "github.com/spf13/viper" -) - -// ViperConfigStore implements the config.Store interface -type ViperConfigStore struct { - viper viper.Viper - configDir string - fs afero.Fs -} - -// ViperConfigStore specific methods -func NewViperStore(fs afero.Fs, loadEnvVars bool) (*ViperConfigStore, error) { - configDir, err := CLIConfigHome() - if err != nil { - return nil, err - } - - v := viper.New() - - v.SetConfigName("config") - v.SetConfigType(configType) - v.SetConfigPermissions(configPerm) - v.AddConfigPath(configDir) - v.SetFs(fs) - - v.SetEnvPrefix(AtlasCLIEnvPrefix) - if loadEnvVars { - v.AutomaticEnv() - - if hasMongoCLIEnvVars() { - v.SetEnvKeyReplacer(strings.NewReplacer(AtlasCLIEnvPrefix, MongoCLIEnvPrefix)) - } - } - - // aliases only work for a config file, this won't work for env variables - v.RegisterAlias(baseURL, OpsManagerURLField) - - // If a config file is found, read it in. - if err := v.ReadInConfig(); err != nil { - // ignore if it doesn't exists - var e viper.ConfigFileNotFoundError - if !errors.As(err, &e) { - return nil, err - } - } - return &ViperConfigStore{ - viper: *v, - configDir: configDir, - fs: fs, - }, nil -} - -func hasMongoCLIEnvVars() bool { - envVars := os.Environ() - for _, v := range envVars { - if strings.HasPrefix(v, MongoCLIEnvPrefix) { - return true - } - } - - return false -} - -func ViperConfigStoreFilename(configDir string) string { - return filepath.Join(configDir, "config.toml") -} - -func (s *ViperConfigStore) Filename() string { - return ViperConfigStoreFilename(s.configDir) -} - -// ConfigStore implementation - -func (*ViperConfigStore) IsSecure() bool { - return false -} - -func (s *ViperConfigStore) Save() error { - exists, err := afero.DirExists(s.fs, s.configDir) - if err != nil { - return err - } - if !exists { - if err := s.fs.MkdirAll(s.configDir, defaultPermissions); err != nil { - return err - } - } - - return s.viper.WriteConfigAs(s.Filename()) -} - -func (s *ViperConfigStore) GetProfileNames() []string { - allKeys := s.viper.AllSettings() - - profileNames := make([]string, 0, len(allKeys)) - for key := range allKeys { - if !slices.Contains(AllProperties(), key) { - profileNames = append(profileNames, key) - } - } - // keys in maps are non-deterministic, trying to give users a consistent output - sort.Strings(profileNames) - return profileNames -} - -func (s *ViperConfigStore) RenameProfile(oldProfileName string, newProfileName string) error { - if err := validateName(newProfileName); err != nil { - return err - } - - // Configuration needs to be deleted from toml, as viper doesn't support this yet. - // FIXME :: change when https://github.com/spf13/viper/pull/519 is merged. - configurationAfterDelete := s.viper.AllSettings() - - t, err := toml.TreeFromMap(configurationAfterDelete) - if err != nil { - return err - } - - t.Set(newProfileName, t.Get(oldProfileName)) - - err = t.Delete(oldProfileName) - if err != nil { - return err - } - - tomlString := t.String() - - f, err := s.fs.OpenFile(s.Filename(), fileFlags, configPerm) - if err != nil { - return err - } - defer f.Close() - - if _, err := f.WriteString(tomlString); err != nil { - return err - } - - return nil -} - -func (s *ViperConfigStore) DeleteProfile(profileName string) error { - // Configuration needs to be deleted from toml, as viper doesn't support this yet. - // FIXME :: change when https://github.com/spf13/viper/pull/519 is merged. - settings := s.viper.AllSettings() - - t, err := toml.TreeFromMap(settings) - if err != nil { - return err - } - - // Delete from the toml manually - err = t.Delete(profileName) - if err != nil { - return err - } - - tomlString := t.String() - - f, err := s.fs.OpenFile(s.Filename(), fileFlags, configPerm) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(tomlString) - return err -} - -func (s *ViperConfigStore) GetHierarchicalValue(profileName string, propertyName string) any { - if s.viper.IsSet(propertyName) && s.viper.Get(propertyName) != "" { - return s.viper.Get(propertyName) - } - settings := s.viper.GetStringMap(profileName) - return settings[propertyName] -} - -func (s *ViperConfigStore) SetProfileValue(profileName string, propertyName string, value any) { - // HACK: viper doesn't allow deleting values: https://github.com/spf13/viper/issues/632 - // Viper was never intended to be used as a key-value store with a save functionality. - // Setting the value to `nil` or `""` will not delete the value from the config file. - // Setting the value to `struct{}` will delete the value from the config file. - if value == nil { - value = struct{}{} - } - - settings := s.viper.GetStringMap(profileName) - settings[propertyName] = value - s.viper.Set(profileName, settings) -} - -func (s *ViperConfigStore) GetProfileValue(profileName string, propertyName string) any { - settings := s.viper.GetStringMap(profileName) - return settings[propertyName] -} - -func (s *ViperConfigStore) GetProfileStringMap(profileName string) map[string]string { - return s.viper.GetStringMapString(profileName) -} - -func (s *ViperConfigStore) SetGlobalValue(propertyName string, value any) { - s.viper.Set(propertyName, value) -} - -func (s *ViperConfigStore) GetGlobalValue(propertyName string) any { - return s.viper.Get(propertyName) -} - -func (s *ViperConfigStore) IsSetGlobal(propertyName string) bool { - return s.viper.IsSet(propertyName) -} From 146317e2c8754f3b2c9d0c43488f5613433f8846 Mon Sep 17 00:00:00 2001 From: Melanija Cvetic Date: Tue, 2 Sep 2025 15:04:40 +0100 Subject: [PATCH 2/2] tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 48d5a83dd1..be7b077568 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/evergreen-ci/shrub v0.0.0-20250506131348-39cf0eb2b3dc github.com/getkin/kin-openapi v0.133.0 github.com/go-test/deep v1.1.1 - github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/go-github/v61 v61.0.0 github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 github.com/klauspost/compress v1.18.0 @@ -63,6 +62,7 @@ require ( github.com/cli/safeexec v1.0.0 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/addlicense v1.1.1 // indirect github.com/google/go-licenses/v2 v2.0.0-alpha.1 // indirect