diff --git a/cmd/argocd-util/main.go b/cmd/argocd-util/main.go index 356d65407d..0f4e68a878 100644 --- a/cmd/argocd-util/main.go +++ b/cmd/argocd-util/main.go @@ -59,7 +59,6 @@ func NewCommand() *cobra.Command { command.AddCommand(NewGenDexConfigCommand()) command.AddCommand(NewImportCommand()) command.AddCommand(NewExportCommand()) - command.AddCommand(NewSettingsCommand()) command.AddCommand(NewClusterConfig()) command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error") @@ -348,42 +347,6 @@ func NewExportCommand() *cobra.Command { return &command } -// NewSettingsCommand returns a new instance of `argocd-util settings` command -func NewSettingsCommand() *cobra.Command { - var ( - clientConfig clientcmd.ClientConfig - updateSuperuser bool - superuserPassword string - updateSignature bool - ) - var command = &cobra.Command{ - Use: "settings", - Short: "Creates or updates Argo CD settings", - Long: "Creates or updates Argo CD settings", - Run: func(c *cobra.Command, args []string) { - conf, err := clientConfig.ClientConfig() - errors.CheckError(err) - namespace, wasSpecified, err := clientConfig.Namespace() - errors.CheckError(err) - if !(wasSpecified) { - namespace = "argocd" - } - - kubeclientset, err := kubernetes.NewForConfig(conf) - errors.CheckError(err) - settingsMgr := settings.NewSettingsManager(context.Background(), kubeclientset, namespace) - - _, err = settings.UpdateSettings(superuserPassword, settingsMgr, updateSignature, updateSuperuser, namespace) - errors.CheckError(err) - }, - } - command.Flags().BoolVar(&updateSuperuser, "update-superuser", false, "force updating the superuser password") - command.Flags().StringVar(&superuserPassword, "superuser-password", "", "password for super user") - command.Flags().BoolVar(&updateSignature, "update-signature", false, "force updating the server-side token signing signature") - clientConfig = cli.AddKubectlFlagsToCmd(command) - return command -} - // NewClusterConfig returns a new instance of `argocd-util cluster-kubeconfig` command func NewClusterConfig() *cobra.Command { var ( diff --git a/server/account/account.go b/server/account/account.go index 4c9806ae6b..c23735c90c 100644 --- a/server/account/account.go +++ b/server/account/account.go @@ -4,6 +4,7 @@ import ( "time" jwt "github.com/dgrijalva/jwt-go" + log "github.com/sirupsen/logrus" "golang.org/x/net/context" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -59,7 +60,7 @@ func (s *Server) UpdatePassword(ctx context.Context, q *UpdatePasswordRequest) ( if err != nil { return nil, err } - + log.Infof("user '%s' updated password", username) return &UpdatePasswordResponse{}, nil } diff --git a/server/account/account_test.go b/server/account/account_test.go new file mode 100644 index 0000000000..230704d7b3 --- /dev/null +++ b/server/account/account_test.go @@ -0,0 +1,84 @@ +package account + +import ( + "context" + "testing" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + + "github.com/argoproj/argo-cd/errors" + "github.com/argoproj/argo-cd/server/session" + "github.com/argoproj/argo-cd/util/password" + sessionutil "github.com/argoproj/argo-cd/util/session" + "github.com/argoproj/argo-cd/util/settings" +) + +const ( + testNamespace = "default" +) + +// return an AccountServer which returns fake data +func newTestAccountServer(ctx context.Context, objects ...runtime.Object) (*fake.Clientset, *Server, *session.Server) { + bcrypt, err := password.HashPassword("oldpassword") + errors.CheckError(err) + kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-cm", + Namespace: testNamespace, + }, + }, &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-secret", + Namespace: testNamespace, + }, + Data: map[string][]byte{ + "admin.password": []byte(bcrypt), + "server.secretkey": []byte("test"), + }, + }) + settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace) + sessionMgr := sessionutil.NewSessionManager(settingsMgr) + return kubeclientset, NewServer(sessionMgr, settingsMgr), session.NewServer(sessionMgr) +} + +func TestUpdatePassword(t *testing.T) { + ctx := context.Background() + _, accountServer, sessionServer := newTestAccountServer(ctx) + ctx = context.WithValue(ctx, "claims", &jwt.StandardClaims{Subject: "admin"}) + var err error + + // ensure password is not allowed to be updated if given bad password + _, err = accountServer.UpdatePassword(ctx, &UpdatePasswordRequest{CurrentPassword: "badpassword", NewPassword: "newpassword"}) + assert.Error(t, err) + assert.NoError(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "oldpassword")) + assert.Error(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "newpassword")) + // verify old password works + _, err = sessionServer.Create(ctx, &session.SessionCreateRequest{Username: "admin", Password: "oldpassword"}) + assert.NoError(t, err) + // verify new password doesn't + _, err = sessionServer.Create(ctx, &session.SessionCreateRequest{Username: "admin", Password: "newpassword"}) + assert.Error(t, err) + + // ensure password can be updated with valid password and immediately be used + settings, err := accountServer.settingsMgr.GetSettings() + assert.NoError(t, err) + prevHash := settings.AdminPasswordHash + _, err = accountServer.UpdatePassword(ctx, &UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword"}) + assert.NoError(t, err) + settings, err = accountServer.settingsMgr.GetSettings() + assert.NoError(t, err) + assert.NotEqual(t, prevHash, settings.AdminPasswordHash) + assert.NoError(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "newpassword")) + assert.Error(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "oldpassword")) + // verify old password is invalid + _, err = sessionServer.Create(ctx, &session.SessionCreateRequest{Username: "admin", Password: "oldpassword"}) + assert.Error(t, err) + // verify new password works + _, err = sessionServer.Create(ctx, &session.SessionCreateRequest{Username: "admin", Password: "newpassword"}) + assert.NoError(t, err) +} diff --git a/server/project/project_test.go b/server/project/project_test.go index 1fea43e406..3b7776cf6f 100644 --- a/server/project/project_test.go +++ b/server/project/project_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/grpc" "google.golang.org/grpc/codes" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" @@ -24,15 +25,30 @@ import ( "github.com/argoproj/argo-cd/util/settings" ) +const testNamespace = "default" + func TestProjectServer(t *testing.T) { - enforcer := rbac.NewEnforcer(fake.NewSimpleClientset(), "default", common.ArgoCDRBACConfigMapName, nil) + kubeclientset := fake.NewSimpleClientset(&corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{Namespace: testNamespace, Name: "argocd-cm"}, + }, &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "argocd-secret", + Namespace: testNamespace, + }, + Data: map[string][]byte{ + "admin.password": []byte("test"), + "server.secretkey": []byte("test"), + }, + }) + settingsMgr := settings.NewSettingsManager(context.Background(), kubeclientset, testNamespace) + enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil) enforcer.SetBuiltinPolicy(test.BuiltinPolicy) enforcer.SetDefaultRole("role:admin") enforcer.SetClaimsEnforcerFunc(func(claims jwt.Claims, rvals ...interface{}) bool { return true }) existingProj := v1alpha1.AppProject{ - ObjectMeta: v1.ObjectMeta{Name: "test", Namespace: "default"}, + ObjectMeta: v1.ObjectMeta{Name: "test", Namespace: testNamespace}, Spec: v1alpha1.AppProjectSpec{ Destinations: []v1alpha1.ApplicationDestination{ {Namespace: "ns1", Server: "https://server1"}, @@ -144,7 +160,7 @@ func TestProjectServer(t *testing.T) { }) t.Run("TestCreateTokenSuccesfully", func(t *testing.T) { - sessionMgr := session.NewSessionManager(&settings.ArgoCDSettings{}) + sessionMgr := session.NewSessionManager(settingsMgr) projectWithRole := existingProj.DeepCopy() tokenName := "testToken" projectWithRole.Spec.Roles = []v1alpha1.ProjectRole{{Name: tokenName}} @@ -163,7 +179,7 @@ func TestProjectServer(t *testing.T) { }) t.Run("TestDeleteTokenSuccesfully", func(t *testing.T) { - sessionMgr := session.NewSessionManager(&settings.ArgoCDSettings{}) + sessionMgr := session.NewSessionManager(settingsMgr) projWithToken := existingProj.DeepCopy() tokenName := "testToken" issuedAt := int64(1) @@ -182,7 +198,7 @@ func TestProjectServer(t *testing.T) { }) t.Run("TestCreateTwoTokensInRoleSuccess", func(t *testing.T) { - sessionMgr := session.NewSessionManager(&settings.ArgoCDSettings{}) + sessionMgr := session.NewSessionManager(settingsMgr) projWithToken := existingProj.DeepCopy() tokenName := "testToken" token := v1alpha1.ProjectRole{Name: tokenName, JWTTokens: []v1alpha1.JWTToken{{IssuedAt: 1}}} diff --git a/server/server.go b/server/server.go index 8b8b61985e..d55a2c84d7 100644 --- a/server/server.go +++ b/server/server.go @@ -17,9 +17,9 @@ import ( "github.com/gobuffalo/packr" golang_proto "github.com/golang/protobuf/proto" - "github.com/grpc-ecosystem/go-grpc-middleware" - "github.com/grpc-ecosystem/go-grpc-middleware/auth" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" "github.com/grpc-ecosystem/grpc-gateway/runtime" log "github.com/sirupsen/logrus" "github.com/soheilhy/cmux" @@ -36,7 +36,7 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" - "github.com/argoproj/argo-cd" + argocd "github.com/argoproj/argo-cd" "github.com/argoproj/argo-cd/common" "github.com/argoproj/argo-cd/controller" "github.com/argoproj/argo-cd/errors" @@ -164,34 +164,14 @@ func initializeDefaultProject(opts ArgoCDServerOpts) error { return err } -// initializeSettings sets default secret settings (password set to hostname) -func initializeSettings(settingsMgr *settings_util.SettingsManager, opts ArgoCDServerOpts) (*settings_util.ArgoCDSettings, error) { - - defaultPassword, err := os.Hostname() - errors.CheckError(err) - - cdSettings, err := settings_util.UpdateSettings(defaultPassword, settingsMgr, false, false, opts.Namespace) - if err != nil { - // assume settings are initialized by another instance of api server - if apierrors.IsConflict(err) { - return settingsMgr.GetSettings() - } else { - log.Fatal(err) - } - } - - return cdSettings, nil -} - // NewServer returns a new instance of the Argo CD API server func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer { settingsMgr := settings_util.NewSettingsManager(ctx, opts.KubeClientset, opts.Namespace) - - settings, err := initializeSettings(settingsMgr, opts) + settings, err := settingsMgr.InitializeSettings() errors.CheckError(err) err = initializeDefaultProject(opts) errors.CheckError(err) - sessionMgr := util_session.NewSessionManager(settings) + sessionMgr := util_session.NewSessionManager(settingsMgr) factory := appinformer.NewFilteredSharedInformerFactory(opts.AppClientset, 0, opts.Namespace, func(options *metav1.ListOptions) {}) appInformer := factory.Argoproj().V1alpha1().Applications().Informer() diff --git a/server/server_test.go b/server/server_test.go index ff154f61fa..a7f41e9200 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -74,7 +74,10 @@ func fakeSecret(policy ...string) *apiv1.Secret { Name: common.ArgoCDSecretName, Namespace: fakeNamespace, }, - Data: make(map[string][]byte), + Data: map[string][]byte{ + "admin.password": []byte("test"), + "server.secretkey": []byte("test"), + }, } return &secret } diff --git a/util/session/sessionmanager.go b/util/session/sessionmanager.go index fb54021a50..f5cf99e187 100644 --- a/util/session/sessionmanager.go +++ b/util/session/sessionmanager.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/coreos/go-oidc" + oidc "github.com/coreos/go-oidc" jwt "github.com/dgrijalva/jwt-go" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" @@ -25,9 +25,9 @@ import ( // SessionManager generates and validates JWT tokens for login sessions. type SessionManager struct { - settings *settings.ArgoCDSettings - client *http.Client - provider *oidc.Provider + settingsMgr *settings.SettingsManager + client *http.Client + provider *oidc.Provider } const ( @@ -41,9 +41,13 @@ const ( ) // NewSessionManager creates a new session manager from Argo CD settings -func NewSessionManager(settings *settings.ArgoCDSettings) *SessionManager { +func NewSessionManager(settingsMgr *settings.SettingsManager) *SessionManager { s := SessionManager{ - settings: settings, + settingsMgr: settingsMgr, + } + settings, err := settingsMgr.GetSettings() + if err != nil { + panic(err) } tlsConfig := settings.TLSConfig() if tlsConfig != nil { @@ -90,7 +94,11 @@ func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int64) (st func (mgr *SessionManager) signClaims(claims jwt.Claims) (string, error) { log.Infof("Issuing claims: %v", claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(mgr.settings.ServerSignature) + settings, err := mgr.settingsMgr.GetSettings() + if err != nil { + return "", err + } + return token.SignedString(settings.ServerSignature) } // Parse tries to parse the provided string and returns the token claims for local superuser login. @@ -100,19 +108,23 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, error) { // head of the token to identify which key to use, but the parsed token (head and claims) is provided // to the callback, providing flexibility. var claims jwt.MapClaims + settings, err := mgr.settingsMgr.GetSettings() + if err != nil { + return nil, err + } token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) { // Don't forget to validate the alg is what you expect: if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } - return mgr.settings.ServerSignature, nil + return settings.ServerSignature, nil }) if err != nil { return nil, err } issuedAt := time.Unix(int64(claims["iat"].(float64)), 0) - if issuedAt.Before(mgr.settings.AdminPasswordMtime) { + if issuedAt.Before(settings.AdminPasswordMtime) { return nil, fmt.Errorf("Password for superuser has changed since token issued") } return token.Claims, nil @@ -126,7 +138,11 @@ func (mgr *SessionManager) VerifyUsernamePassword(username, password string) err if password == "" { return status.Errorf(codes.Unauthenticated, blankPasswordError) } - valid, _ := passwordutil.VerifyPassword(password, mgr.settings.AdminPasswordHash) + settings, err := mgr.settingsMgr.GetSettings() + if err != nil { + return err + } + valid, _ := passwordutil.VerifyPassword(password, settings.AdminPasswordHash) if !valid { return status.Errorf(codes.Unauthenticated, invalidLoginError) } @@ -219,10 +235,14 @@ func (mgr *SessionManager) oidcProvider() (*oidc.Provider, error) { // initializeOIDCProvider re-initializes the OIDC provider, querying the well known oidc // configuration path (http://example-argocd.com/api/dex/.well-known/openid-configuration) func (mgr *SessionManager) initializeOIDCProvider() (*oidc.Provider, error) { - if !mgr.settings.IsSSOConfigured() { + settings, err := mgr.settingsMgr.GetSettings() + if err != nil { + return nil, err + } + if !settings.IsSSOConfigured() { return nil, fmt.Errorf("SSO is not configured") } - provider, err := oidcutil.NewOIDCProvider(mgr.settings.IssuerURL(), mgr.client) + provider, err := oidcutil.NewOIDCProvider(settings.IssuerURL(), mgr.client) if err != nil { return nil, err } diff --git a/util/session/sessionmanager_test.go b/util/session/sessionmanager_test.go index 0e050d0430..4f3d64cb0b 100644 --- a/util/session/sessionmanager_test.go +++ b/util/session/sessionmanager_test.go @@ -1,10 +1,18 @@ -package session +package session_test import ( + "context" "testing" - "github.com/argoproj/argo-cd/util/settings" jwt "github.com/dgrijalva/jwt-go" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/argoproj/argo-cd/errors" + "github.com/argoproj/argo-cd/util/password" + sessionutil "github.com/argoproj/argo-cd/util/session" + "github.com/argoproj/argo-cd/util/settings" ) func TestSessionManager(t *testing.T) { @@ -12,10 +20,27 @@ func TestSessionManager(t *testing.T) { defaultSecretKey = "Hello, world!" defaultSubject = "argo" ) - set := settings.ArgoCDSettings{ - ServerSignature: []byte(defaultSecretKey), - } - mgr := NewSessionManager(&set) + + bcrypt, err := password.HashPassword("password") + errors.CheckError(err) + kubeclientset := fake.NewSimpleClientset(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-cm", + Namespace: "argocd", + }, + }, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-secret", + Namespace: "argocd", + }, + Data: map[string][]byte{ + "admin.password": []byte(bcrypt), + "server.secretkey": []byte(defaultSecretKey), + }, + }) + + settingsMgr := settings.NewSettingsManager(context.Background(), kubeclientset, "argocd") + mgr := sessionutil.NewSessionManager(settingsMgr) token, err := mgr.Create(defaultSubject, 0) if err != nil { diff --git a/util/settings/settings.go b/util/settings/settings.go index d7d5158fb2..87368f656d 100644 --- a/util/settings/settings.go +++ b/util/settings/settings.go @@ -7,28 +7,27 @@ import ( "crypto/x509" "encoding/base64" "fmt" + "os" "strings" "sync" "time" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/client-go/informers/core/v1" - "github.com/ghodss/yaml" log "github.com/sirupsen/logrus" - apiv1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" + v1 "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" v1listers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" "github.com/argoproj/argo-cd/common" "github.com/argoproj/argo-cd/util" - "github.com/argoproj/argo-cd/util/cli" "github.com/argoproj/argo-cd/util/password" tlsutil "github.com/argoproj/argo-cd/util/tls" ) @@ -144,7 +143,7 @@ func (e *incompleteSettingsError) Error() string { } func (mgr *SettingsManager) GetSecretsLister() (v1listers.SecretLister, error) { - err := mgr.ensureInitialized(false) + err := mgr.ensureSynced(false) if err != nil { return nil, err } @@ -153,34 +152,35 @@ func (mgr *SettingsManager) GetSecretsLister() (v1listers.SecretLister, error) { // GetSettings retrieves settings from the ArgoCDConfigMap and secret. func (mgr *SettingsManager) GetSettings() (*ArgoCDSettings, error) { - err := mgr.ensureInitialized(false) + err := mgr.ensureSynced(false) if err != nil { return nil, err } - - var settings ArgoCDSettings argoCDCM, err := mgr.configmaps.ConfigMaps(mgr.namespace).Get(common.ArgoCDConfigMapName) if err != nil { return nil, err } - err = mgr.updateSettingsFromConfigMap(&settings, argoCDCM) - if err != nil { - return nil, err - } argoCDSecret, err := mgr.secrets.Secrets(mgr.namespace).Get(common.ArgoCDSecretName) if err != nil { - return &settings, err + return nil, err } - err = updateSettingsFromSecret(&settings, argoCDSecret) - if err != nil { - return &settings, err + var settings ArgoCDSettings + var errs []error + if err := updateSettingsFromConfigMap(&settings, argoCDCM); err != nil { + errs = append(errs, err) + } + if err := updateSettingsFromSecret(&settings, argoCDSecret); err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { + return &settings, errs[0] } return &settings, nil } // MigrateLegacyRepoSettings migrates legacy (v0.10 and below) repo secrets into the v0.11 configmap func (mgr *SettingsManager) MigrateLegacyRepoSettings(settings *ArgoCDSettings) error { - err := mgr.ensureInitialized(false) + err := mgr.ensureSynced(false) if err != nil { return err } @@ -247,7 +247,7 @@ func (mgr *SettingsManager) initialize(ctx context.Context) error { if !cache.WaitForCacheSync(ctx.Done(), cmInformer.HasSynced, secretsInformer.HasSynced) { return fmt.Errorf("Timed out waiting for settings cache to sync") } - log.Info("Configmap/secret informer synched") + log.Info("Configmap/secret informer synced") tryNotify := func() { newSettings, err := mgr.GetSettings() @@ -282,7 +282,7 @@ func (mgr *SettingsManager) initialize(ctx context.Context) error { return nil } -func (mgr *SettingsManager) ensureInitialized(forceResync bool) error { +func (mgr *SettingsManager) ensureSynced(forceResync bool) error { if !forceResync && mgr.secrets != nil && mgr.configmaps != nil { return nil } @@ -300,49 +300,59 @@ func (mgr *SettingsManager) ensureInitialized(forceResync bool) error { return mgr.initialize(ctx) } -func (mgr *SettingsManager) updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.ConfigMap) error { +func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.ConfigMap) error { settings.DexConfig = argoCDCM.Data[settingDexConfigKey] settings.OIDCConfigRAW = argoCDCM.Data[settingsOIDCConfigKey] settings.URL = argoCDCM.Data[settingURLKey] + settings.AppInstanceLabelKey = argoCDCM.Data[settingsApplicationInstanceLabelKey] repositoriesStr := argoCDCM.Data[repositoriesKey] + var errors []error if repositoriesStr != "" { - settings.Repositories = make([]RepoCredentials, 0) - err := yaml.Unmarshal([]byte(repositoriesStr), &settings.Repositories) + repositories := make([]RepoCredentials, 0) + err := yaml.Unmarshal([]byte(repositoriesStr), &repositories) if err != nil { - return err + errors = append(errors, err) + } else { + settings.Repositories = repositories } } helmRepositoriesStr := argoCDCM.Data[helmRepositoriesKey] if helmRepositoriesStr != "" { - settings.HelmRepositories = make([]HelmRepoCredentials, 0) - err := yaml.Unmarshal([]byte(helmRepositoriesStr), &settings.HelmRepositories) + helmRepositories := make([]HelmRepoCredentials, 0) + err := yaml.Unmarshal([]byte(helmRepositoriesStr), &helmRepositories) if err != nil { - return err + errors = append(errors, err) + } else { + settings.HelmRepositories = helmRepositories } } - settings.AppInstanceLabelKey = argoCDCM.Data[settingsApplicationInstanceLabelKey] + if len(errors) > 0 { + return errors[0] + } return nil } // updateSettingsFromSecret transfers settings from a Kubernetes secret into an ArgoCDSettings struct. func updateSettingsFromSecret(settings *ArgoCDSettings, argoCDSecret *apiv1.Secret) error { + var errs []error adminPasswordHash, ok := argoCDSecret.Data[settingAdminPasswordHashKey] - if !ok { - return &incompleteSettingsError{message: "admin.password is missing"} + if ok { + settings.AdminPasswordHash = string(adminPasswordHash) + } else { + errs = append(errs, &incompleteSettingsError{message: "admin.password is missing"}) } - settings.AdminPasswordHash = string(adminPasswordHash) - settings.AdminPasswordMtime = time.Now().UTC() - if adminPasswordMtimeBytes, ok := argoCDSecret.Data[settingAdminPasswordMtimeKey]; ok { + adminPasswordMtimeBytes, ok := argoCDSecret.Data[settingAdminPasswordMtimeKey] + if ok { if adminPasswordMtime, err := time.Parse(time.RFC3339, string(adminPasswordMtimeBytes)); err == nil { settings.AdminPasswordMtime = adminPasswordMtime } } - secretKey, ok := argoCDSecret.Data[settingServerSignatureKey] - if !ok { - return &incompleteSettingsError{message: "server.secretkey is missing"} + if ok { + settings.ServerSignature = secretKey + } else { + errs = append(errs, &incompleteSettingsError{message: "server.secretkey is missing"}) } - settings.ServerSignature = secretKey if githubWebhookSecret := argoCDSecret.Data[settingsWebhookGitHubSecretKey]; len(githubWebhookSecret) > 0 { settings.WebhookGitHubSecret = string(githubWebhookSecret) } @@ -358,21 +368,25 @@ func updateSettingsFromSecret(settings *ArgoCDSettings, argoCDSecret *apiv1.Secr if certOk && keyOk { cert, err := tls.X509KeyPair(serverCert, serverKey) if err != nil { - return &incompleteSettingsError{message: fmt.Sprintf("invalid x509 key pair %s/%s in secret: %s", settingServerCertificate, settingServerPrivateKey, err)} + errs = append(errs, &incompleteSettingsError{message: fmt.Sprintf("invalid x509 key pair %s/%s in secret: %s", settingServerCertificate, settingServerPrivateKey, err)}) + } else { + settings.Certificate = &cert } - settings.Certificate = &cert } secretValues := make(map[string]string, len(argoCDSecret.Data)) for k, v := range argoCDSecret.Data { secretValues[k] = string(v) } settings.Secrets = secretValues + if len(errs) > 0 { + return errs[0] + } return nil } // SaveSettings serializes ArgoCDSettings and upserts it into K8s secret/configmap func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error { - err := mgr.ensureInitialized(false) + err := mgr.ensureSynced(false) if err != nil { return err } @@ -447,24 +461,26 @@ func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error { } createSecret = true } + if argoCDSecret.Data == nil { + argoCDSecret.Data = make(map[string][]byte) + } - argoCDSecret.StringData = make(map[string]string) - argoCDSecret.StringData[settingServerSignatureKey] = string(settings.ServerSignature) - argoCDSecret.StringData[settingAdminPasswordHashKey] = settings.AdminPasswordHash - argoCDSecret.StringData[settingAdminPasswordMtimeKey] = settings.AdminPasswordMtime.Format(time.RFC3339) + argoCDSecret.Data[settingServerSignatureKey] = settings.ServerSignature + argoCDSecret.Data[settingAdminPasswordHashKey] = []byte(settings.AdminPasswordHash) + argoCDSecret.Data[settingAdminPasswordMtimeKey] = []byte(settings.AdminPasswordMtime.Format(time.RFC3339)) if settings.WebhookGitHubSecret != "" { - argoCDSecret.StringData[settingsWebhookGitHubSecretKey] = settings.WebhookGitHubSecret + argoCDSecret.Data[settingsWebhookGitHubSecretKey] = []byte(settings.WebhookGitHubSecret) } if settings.WebhookGitLabSecret != "" { - argoCDSecret.StringData[settingsWebhookGitLabSecretKey] = settings.WebhookGitLabSecret + argoCDSecret.Data[settingsWebhookGitLabSecretKey] = []byte(settings.WebhookGitLabSecret) } if settings.WebhookBitbucketUUID != "" { - argoCDSecret.StringData[settingsWebhookBitbucketUUIDKey] = settings.WebhookBitbucketUUID + argoCDSecret.Data[settingsWebhookBitbucketUUIDKey] = []byte(settings.WebhookBitbucketUUID) } if settings.Certificate != nil { - cert, key := tlsutil.EncodeX509KeyPairString(*settings.Certificate) - argoCDSecret.StringData[settingServerCertificate] = cert - argoCDSecret.StringData[settingServerPrivateKey] = key + cert, key := tlsutil.EncodeX509KeyPair(*settings.Certificate) + argoCDSecret.Data[settingServerCertificate] = cert + argoCDSecret.Data[settingServerPrivateKey] = key } else { delete(argoCDSecret.Data, settingServerCertificate) delete(argoCDSecret.Data, settingServerPrivateKey) @@ -494,7 +510,7 @@ func NewSettingsManager(ctx context.Context, clientset kubernetes.Interface, nam } func (mgr *SettingsManager) ResyncInformers() error { - return mgr.ensureInitialized(true) + return mgr.ensureSynced(true) } // IsSSOConfigured returns whether or not single-sign-on is configured @@ -636,38 +652,40 @@ func isIncompleteSettingsError(err error) bool { return ok } -// UpdateSettings is used to update the admin password, signature, certificate etc -func UpdateSettings(defaultPassword string, settingsMgr *SettingsManager, updateSignature bool, updateSuperuser bool, Namespace string) (*ArgoCDSettings, error) { - - cdSettings, err := settingsMgr.GetSettings() - if err != nil && !apierr.IsNotFound(err) && !isIncompleteSettingsError(err) { +// InitializeSettings is used to initialize empty admin password, signature, certificate etc if missing +func (mgr *SettingsManager) InitializeSettings() (*ArgoCDSettings, error) { + cdSettings, err := mgr.GetSettings() + if err != nil && !isIncompleteSettingsError(err) { return nil, err } if cdSettings == nil { cdSettings = &ArgoCDSettings{} } - if cdSettings.ServerSignature == nil || updateSignature { + if cdSettings.ServerSignature == nil { // set JWT signature signature, err := util.MakeSignature(32) if err != nil { return nil, err } cdSettings.ServerSignature = signature + log.Info("Initialized server signature") } - if cdSettings.AdminPasswordHash == "" || updateSuperuser { - passwordRaw := defaultPassword - if passwordRaw == "" { - passwordRaw, err = cli.ReadAndConfirmPassword() - if err != nil { - return nil, err - } + if cdSettings.AdminPasswordHash == "" { + defaultPassword, err := os.Hostname() + if err != nil { + return nil, err } - hashedPassword, err := password.HashPassword(passwordRaw) + hashedPassword, err := password.HashPassword(defaultPassword) if err != nil { return nil, err } cdSettings.AdminPasswordHash = hashedPassword cdSettings.AdminPasswordMtime = time.Now().UTC() + log.Info("Initialized admin password") + } + if cdSettings.AdminPasswordMtime.IsZero() { + cdSettings.AdminPasswordMtime = time.Now().UTC() + log.Info("Initialized admin mtime") } if cdSettings.Certificate == nil { @@ -675,9 +693,9 @@ func UpdateSettings(defaultPassword string, settingsMgr *SettingsManager, update hosts := []string{ "localhost", "argocd-server", - fmt.Sprintf("argocd-server.%s", Namespace), - fmt.Sprintf("argocd-server.%s.svc", Namespace), - fmt.Sprintf("argocd-server.%s.svc.cluster.local", Namespace), + fmt.Sprintf("argocd-server.%s", mgr.namespace), + fmt.Sprintf("argocd-server.%s.svc", mgr.namespace), + fmt.Sprintf("argocd-server.%s.svc.cluster.local", mgr.namespace), } certOpts := tlsutil.CertOptions{ Hosts: hosts, @@ -689,16 +707,23 @@ func UpdateSettings(defaultPassword string, settingsMgr *SettingsManager, update return nil, err } cdSettings.Certificate = cert + log.Info("Initialized TLS certificate") } if len(cdSettings.Repositories) == 0 { - err = settingsMgr.MigrateLegacyRepoSettings(cdSettings) + err = mgr.MigrateLegacyRepoSettings(cdSettings) if err != nil { return nil, err } } - return cdSettings, settingsMgr.SaveSettings(cdSettings) + err = mgr.SaveSettings(cdSettings) + if apierrors.IsConflict(err) { + // assume settings are initialized by another instance of api server + log.Warnf("conflict when initializing settings. assuming updated by another replica") + return mgr.GetSettings() + } + return cdSettings, nil } // ReplaceStringSecret checks if given string is a secret key reference ( starts with $ ) and returns corresponding value from provided map