Show CLI progress for sync and rollback (#393)

* Add ExpiresAt seconds

Per NumericDate having resolution of seconds at https://tools.ietf.org/html/rfc7519#page-6

* Rename expires for clarity; update comments

* Don't use different possible values for now

* Use intermediate variable for expires value

* Add pseudocode comments to session manager

* Update password storage

* Factor out LocalUsers

* Fix compile errors

* Add claim checks

* Support expiry on ReissueClaims tokens

* Set location to UTC for tokens

* Add logging for username

* Fix issuedAt type assertion

* Set mtime to UTC location

* Set second param on mgr.Create

* Update output for sync

* Major refactor

* Reduce verbosity

* Reduce duplicated code some more, thanks @jessesuen

* Move printout

* Move printout to success, not failure

* Revert "Move printout to success, not failure"

This reverts commit 3a6863d8f497c02bd381cf9ed6ff4a642c8bdcb5.

* Print final status on success _or_ failure

* Adjust printouts with frankenparameters

* Major refactor of data pipelining, thanks @jessesuen

* Refactor app state change printouts

* Fix number of Sprintf args

* Use previous format for keys, rather than hash

* Rename res => hook for clarity

* Don't print app resources initially, thanks @jessesuen

* Refactor Fprintf call to Fprintln

* Rename waitUntilOperationCompleted, thanks @jessesuen

* Refactor to merge data on update

* Default to updated for new resource states

* Use map for fields that actually change

* Don't let flapping lead to duplicate printouts

* Simplify caching mechanism
This commit is contained in:
Andrew Merenbach 2018-07-25 09:01:50 -07:00 committed by GitHub
parent 6a7df88cf4
commit 7b6b945cbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 221 additions and 141 deletions

View file

@ -637,9 +637,10 @@ func formatConditionsSummary(app argoappv1.Application) string {
// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
syncOnly bool
healthOnly bool
timeout uint
watchSync bool
watchHealth bool
watchOperations bool
timeout uint
)
var command = &cobra.Command{
Use: "wait APPNAME",
@ -649,49 +650,22 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
c.HelpFunc()(c, args)
os.Exit(1)
}
if syncOnly && healthOnly {
log.Fatalln("Please specify at most one of --sync-only or --health-only.")
if !watchSync && !watchHealth && !watchOperations {
watchSync = true
watchHealth = true
watchOperations = true
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer util.Close(conn)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if timeout != 0 {
time.AfterFunc(time.Duration(timeout)*time.Second, func() {
cancel()
})
}
// print the initial components to format the tabwriter columns
app, err := appIf.Get(ctx, &application.ApplicationQuery{Name: &appName})
_, err := waitOnApplicationStatus(appIf, appName, timeout, watchSync, watchHealth, watchOperations)
errors.CheckError(err)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
printAppResources(w, app, false)
_ = w.Flush()
prevCompRes := &app.Status.ComparisonResult
appEventCh := watchApp(ctx, appIf, appName)
for appEvent := range appEventCh {
app := appEvent.Application
printAppStateChange(w, prevCompRes, &app)
_ = w.Flush()
prevCompRes = &app.Status.ComparisonResult
synced := app.Status.ComparisonResult.Status == argoappv1.ComparisonStatusSynced
healthy := app.Status.Health.Status == argoappv1.HealthStatusHealthy
if len(app.Status.GetErrorConditions()) == 0 && ((synced && healthy) || (synced && syncOnly) || (healthy && healthOnly)) {
log.Printf("App %q matches desired state", appName)
return
}
}
log.Fatalf("Timed out (%ds) waiting for app %q match desired state", timeout, appName)
},
}
command.Flags().BoolVar(&syncOnly, "sync-only", false, "Wait only for sync")
command.Flags().BoolVar(&healthOnly, "health-only", false, "Wait only for health")
command.Flags().BoolVar(&watchSync, "sync", false, "Wait for sync")
command.Flags().BoolVar(&watchHealth, "health", false, "Wait for health")
command.Flags().BoolVar(&watchOperations, "operation", false, "Wait for pending operations")
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
return command
}
@ -803,38 +777,6 @@ func printAppResources(w io.Writer, app *argoappv1.Application, showOperation bo
}
}
// printAppStateChange prints a component state change if it was different from the last time we saw it
func printAppStateChange(w io.Writer, prevComp *argoappv1.ComparisonResult, app *argoappv1.Application) {
getPrevResState := func(kind, name string) (argoappv1.ComparisonStatus, argoappv1.HealthStatusCode) {
for _, res := range prevComp.Resources {
obj, err := argoappv1.UnmarshalToUnstructured(res.TargetState)
errors.CheckError(err)
if obj == nil {
obj, err = argoappv1.UnmarshalToUnstructured(res.LiveState)
errors.CheckError(err)
}
if obj.GetKind() == kind && obj.GetName() == name {
return res.Status, res.Health.Status
}
}
return "", ""
}
if len(app.Status.ComparisonResult.Resources) > 0 {
for _, res := range app.Status.ComparisonResult.Resources {
obj, err := argoappv1.UnmarshalToUnstructured(res.TargetState)
errors.CheckError(err)
if obj == nil {
obj, err = argoappv1.UnmarshalToUnstructured(res.LiveState)
errors.CheckError(err)
}
prevSync, prevHealth := getPrevResState(obj.GetKind(), obj.GetName())
if prevSync != res.Status || prevHealth != res.Health.Status {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", obj.GetKind(), obj.GetName(), res.Status, res.Health.Status)
}
}
}
}
// NewApplicationSyncCommand returns a new instance of an `argocd app sync` command
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
@ -875,23 +817,10 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
ctx := context.Background()
_, err := appIf.Sync(ctx, &syncReq)
errors.CheckError(err)
app, err := waitUntilOperationCompleted(appIf, appName, timeout)
app, err := waitOnApplicationStatus(appIf, appName, timeout, false, false, true)
errors.CheckError(err)
// get refreshed app before printing to show accurate sync/health status
app, err = appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, Refresh: true})
errors.CheckError(err)
fmt.Printf(printOpFmtStr, "Application:", appName)
printOperationResult(app.Status.OperationState)
if len(app.Status.ComparisonResult.Resources) > 0 {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
printAppResources(w, app, true)
_ = w.Flush()
}
pruningRequired := 0
for _, resDetails := range app.Status.OperationState.SyncResult.Resources {
if resDetails.Status == argoappv1.ResourceDetailsPruningRequired {
@ -916,22 +845,164 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
return command
}
func waitUntilOperationCompleted(appClient application.ApplicationServiceClient, appName string, timeout uint) (*argoappv1.Application, error) {
// ResourceState tracks the state of a resource when waiting on an application status.
type resourceState struct {
Kind string
Name string
PrevState string
Fields map[string]string
Updated bool
}
func newResourceState(kind, name, status, healthStatus, resType, message string) *resourceState {
return &resourceState{
Kind: kind,
Name: name,
Fields: map[string]string{
"status": status,
"healthStatus": healthStatus,
"type": resType,
"message": message,
},
}
}
// Key returns a unique-ish key for the resource.
func (rs *resourceState) Key() string {
return fmt.Sprintf("%s/%s", rs.Kind, rs.Name)
}
// Merge merges the new state into the previous state, returning whether the
// new state contains any additional keys or different values from the old state.
func (rs *resourceState) Merge() bool {
if out := rs.String(); out != rs.PrevState {
rs.PrevState = out
return true
}
return false
}
func (rs *resourceState) String() string {
return fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s", rs.Kind, rs.Name, rs.Fields["status"], rs.Fields["healthStatus"], rs.Fields["type"], rs.Fields["message"])
}
// Update a resourceState with any different contents from another resourceState.
// Blank fields in the receiver state will be updated to non-blank.
// Non-blank fields in the receiver state will never be updated to blank.
func (rs *resourceState) Update(newState *resourceState) {
for k, v := range newState.Fields {
if v != "" {
rs.Fields[k] = v
}
}
}
func waitOnApplicationStatus(appClient application.ApplicationServiceClient, appName string, timeout uint, watchSync, watchHealth, watchOperations bool) (*argoappv1.Application, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
printFinalStatus := func() {
// get refreshed app before printing to show accurate sync/health status
app, err := appClient.Get(ctx, &application.ApplicationQuery{Name: &appName, Refresh: true})
errors.CheckError(err)
fmt.Printf(printOpFmtStr, "Application:", appName)
printOperationResult(app.Status.OperationState)
if len(app.Status.ComparisonResult.Resources) > 0 {
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
printAppResources(w, app, true)
_ = w.Flush()
}
}
if timeout != 0 {
time.AfterFunc(time.Duration(timeout)*time.Second, func() {
cancel()
printFinalStatus()
})
}
// print the initial components to format the tabwriter columns
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "KIND\tNAME\tSTATUS\tHEALTH\tHOOK\tOPERATIONMSG")
_ = w.Flush()
prevStates := make(map[string]*resourceState)
conditionallyPrintOutput := func(w io.Writer, newState *resourceState) {
stateKey := newState.Key()
if prevState, found := prevStates[stateKey]; found {
prevState.Update(newState)
} else {
prevStates[stateKey] = newState
}
}
printCompResults := func(compResult *argoappv1.ComparisonResult) {
if compResult != nil {
for _, res := range compResult.Resources {
obj, err := argoappv1.UnmarshalToUnstructured(res.TargetState)
errors.CheckError(err)
if obj == nil {
obj, err = argoappv1.UnmarshalToUnstructured(res.LiveState)
errors.CheckError(err)
}
newState := newResourceState(obj.GetKind(), obj.GetName(), string(res.Status), res.Health.Status, "", "")
conditionallyPrintOutput(w, newState)
}
}
}
printOpResults := func(opResult *argoappv1.SyncOperationResult) {
if opResult != nil {
if opResult.Hooks != nil {
for _, hook := range opResult.Hooks {
newState := newResourceState(hook.Kind, hook.Name, string(hook.Status), "", string(hook.Type), hook.Message)
conditionallyPrintOutput(w, newState)
}
}
if opResult.Resources != nil {
for _, res := range opResult.Resources {
newState := newResourceState(res.Kind, res.Name, string(res.Status), "", "", res.Message)
conditionallyPrintOutput(w, newState)
}
}
}
}
appEventCh := watchApp(ctx, appClient, appName)
for appEvent := range appEventCh {
if appEvent.Application.Status.OperationState != nil && appEvent.Application.Status.OperationState.Phase.Completed() {
return &appEvent.Application, nil
app := appEvent.Application
printCompResults(&app.Status.ComparisonResult)
if opState := app.Status.OperationState; opState != nil {
printOpResults(opState.SyncResult)
printOpResults(opState.RollbackResult)
}
for _, v := range prevStates {
if v.Merge() {
fmt.Fprintln(w, v)
}
}
_ = w.Flush()
// consider skipped checks successful
synced := !watchSync || app.Status.ComparisonResult.Status == argoappv1.ComparisonStatusSynced
healthy := !watchHealth || app.Status.Health.Status == argoappv1.HealthStatusHealthy
operational := !watchOperations || appEvent.Application.Operation == nil
if len(app.Status.GetErrorConditions()) == 0 && synced && healthy && operational {
log.Printf("App %q matches desired state", appName)
printFinalStatus()
return &app, nil
}
}
return nil, fmt.Errorf("Timed out (%ds) waiting for app %q match desired state", timeout, appName)
}
@ -1080,18 +1151,8 @@ func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobr
})
errors.CheckError(err)
app, err = waitUntilOperationCompleted(appIf, appName, timeout)
_, err = waitOnApplicationStatus(appIf, appName, timeout, false, false, true)
errors.CheckError(err)
// get refreshed app before printing to show accurate sync/health status
app, err = appIf.Get(ctx, &application.ApplicationQuery{Name: &appName, Refresh: true})
errors.CheckError(err)
fmt.Printf(printOpFmtStr, "Application:", appName)
printOperationResult(app.Status.OperationState)
if !app.Status.OperationState.Phase.Successful() {
os.Exit(1)
}
},
}
command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")

View file

@ -1,11 +1,14 @@
package account
import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/argoproj/argo-cd/common"
jwtutil "github.com/argoproj/argo-cd/util/jwt"
"github.com/argoproj/argo-cd/util/password"
"github.com/argoproj/argo-cd/util/session"
@ -27,16 +30,17 @@ func NewServer(sessionMgr *session.SessionManager, settingsMgr *settings.Setting
}
//UpdatePassword is used to Update a User's Passwords
// UpdatePassword updates the password of the local admin superuser.
func (s *Server) UpdatePassword(ctx context.Context, q *UpdatePasswordRequest) (*UpdatePasswordResponse, error) {
username := getAuthenticatedUser(ctx)
if username != common.ArgoCDAdminUsername {
return nil, status.Errorf(codes.InvalidArgument, "password can only be changed for local users, not user %q", username)
}
cdSettings, err := s.settingsMgr.GetSettings()
if err != nil {
return nil, err
}
if _, ok := cdSettings.LocalUsers[username]; !ok {
return nil, status.Errorf(codes.InvalidArgument, "password can only be changed for local users")
}
err = s.sessionMgr.VerifyUsernamePassword(username, q.CurrentPassword)
if err != nil {
@ -48,7 +52,8 @@ func (s *Server) UpdatePassword(ctx context.Context, q *UpdatePasswordRequest) (
return nil, err
}
cdSettings.LocalUsers[username] = hashedPassword
cdSettings.AdminPasswordHash = hashedPassword
cdSettings.AdminPasswordMtime = time.Now().UTC()
err = s.settingsMgr.SaveSettings(cdSettings)
if err != nil {

View file

@ -36,7 +36,7 @@ func (s *Server) Create(ctx context.Context, q *SessionCreateRequest) (*SessionR
if err != nil {
return nil, err
}
tokenString, err = s.mgr.Create(q.Username)
tokenString, err = s.mgr.Create(q.Username, 0)
if err != nil {
return nil, err
}
@ -50,7 +50,7 @@ func (s *Server) Create(ctx context.Context, q *SessionCreateRequest) (*SessionR
if err != nil {
return nil, err
}
tokenString, err = s.mgr.ReissueClaims(claims)
tokenString, err = s.mgr.ReissueClaims(claims, 0)
if err != nil {
return nil, fmt.Errorf("Failed to resign claims: %v", err)
}

View file

@ -38,6 +38,7 @@ const (
// invalidLoginError, for security purposes, doesn't say whether the username or password was invalid. This does not mitigate the potential for timing attacks to determine which is which.
invalidLoginError = "Invalid username or password"
blankPasswordError = "Blank passwords are not allowed"
badUserError = "Bad local superuser username"
)
// NewSessionManager creates a new session manager from ArgoCD settings
@ -68,17 +69,21 @@ func NewSessionManager(settings *settings.ArgoCDSettings) *SessionManager {
}
// Create creates a new token for a given subject (user) and returns it as a string.
func (mgr *SessionManager) Create(subject string) (string, error) {
// Passing a value of `0` for secondsBeforeExpiry creates a token that never expires.
func (mgr *SessionManager) Create(subject string, secondsBeforeExpiry int) (string, error) {
// Create a new token object, specifying signing method and the claims
// you would like it to contain.
now := time.Now().Unix()
now := time.Now().UTC()
claims := jwt.StandardClaims{
//ExpiresAt: time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
IssuedAt: now,
IssuedAt: now.Unix(),
Issuer: SessionManagerClaimsIssuer,
NotBefore: now,
NotBefore: now.Unix(),
Subject: subject,
}
if secondsBeforeExpiry > 0 {
expires := now.Add(time.Duration(secondsBeforeExpiry) * time.Second)
claims.ExpiresAt = expires.Unix()
}
return mgr.signClaims(claims)
}
@ -89,20 +94,24 @@ func (mgr *SessionManager) signClaims(claims jwt.Claims) (string, error) {
}
// ReissueClaims re-issues and re-signs a new token signed by us, while preserving most of the claim values
func (mgr *SessionManager) ReissueClaims(claims jwt.MapClaims) (string, error) {
now := time.Now().Unix()
func (mgr *SessionManager) ReissueClaims(claims jwt.MapClaims, secondsBeforeExpiry int) (string, error) {
now := time.Now().UTC()
newClaims := make(jwt.MapClaims)
for k, v := range claims {
newClaims[k] = v
}
newClaims["iss"] = SessionManagerClaimsIssuer
newClaims["iat"] = now
newClaims["nbf"] = now
newClaims["iat"] = now.Unix()
newClaims["nbf"] = now.Unix()
delete(newClaims, "exp")
if secondsBeforeExpiry > 0 {
expires := now.Add(time.Duration(secondsBeforeExpiry) * time.Second)
claims["exp"] = expires.Unix()
}
return mgr.signClaims(newClaims)
}
// Parse tries to parse the provided string and returns the token claims.
// Parse tries to parse the provided string and returns the token claims for local superuser login.
func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, error) {
// Parse takes the token string and a function for looking up the key. The latter is especially
// useful if you use multiple keys for your application. The standard is to use 'kid' in the
@ -119,23 +128,23 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, error) {
if err != nil {
return nil, err
}
issuedAt := time.Unix(int64(claims["iat"].(float64)), 0)
if issuedAt.Before(mgr.settings.AdminPasswordMtime) {
return nil, fmt.Errorf("Password for superuser has changed since token issued")
}
return token.Claims, nil
}
// VerifyUsernamePassword verifies if a username/password combo is correct
func (mgr *SessionManager) VerifyUsernamePassword(username, password string) error {
if username != common.ArgoCDAdminUsername {
return status.Errorf(codes.Unauthenticated, badUserError)
}
if password == "" {
return status.Errorf(codes.Unauthenticated, blankPasswordError)
}
passwordHash, ok := mgr.settings.LocalUsers[username]
if !ok {
// Username was not found in local user store.
// Ensure we still send password to hashing algorithm for comparison.
// This mitigates potential for timing attacks that benefit from short-circuiting,
// provided the hashing library/algorithm in use doesn't itself short-circuit.
passwordHash = ""
}
valid, _ := passwordutil.VerifyPassword(password, passwordHash)
valid, _ := passwordutil.VerifyPassword(password, mgr.settings.AdminPasswordHash)
if !valid {
return status.Errorf(codes.Unauthenticated, invalidLoginError)
}

View file

@ -17,7 +17,7 @@ func TestSessionManager(t *testing.T) {
}
mgr := NewSessionManager(&set)
token, err := mgr.Create(defaultSubject)
token, err := mgr.Create(defaultSubject, 0)
if err != nil {
t.Errorf("Could not create token: %v", err)
}

View file

@ -33,10 +33,11 @@ type ArgoCDSettings struct {
// URL is the externally facing URL users will visit to reach ArgoCD.
// The value here is used when configuring SSO. Omitting this value will disable SSO.
URL string `json:"url,omitempty"`
// Admin superuser password storage
AdminPasswordHash string `json:"adminPasswordHash,omitempty"`
AdminPasswordMtime time.Time `json:"adminPasswordMtime,omitempty"`
// DexConfig is contains portions of a dex config yaml
DexConfig string `json:"dexConfig,omitempty"`
// LocalUsers holds users local to (stored on) the server. This is to be distinguished from any potential alternative future login providers (LDAP, SAML, etc.) that might ever be added.
LocalUsers map[string]string `json:"localUsers,omitempty"`
// ServerSignature holds the key used to generate JWT tokens.
ServerSignature []byte `json:"serverSignature,omitempty"`
// Certificate holds the certificate/private key for the ArgoCD API server.
@ -53,8 +54,10 @@ type ArgoCDSettings struct {
}
const (
// settingAdminPasswordKey designates the key for a root password inside a Kubernetes secret.
settingAdminPasswordKey = "admin.password"
// settingAdminPasswordHashKey designates the key for a root password hash inside a Kubernetes secret.
settingAdminPasswordHashKey = "admin.password"
// settingAdminPasswordMtimeKey designates the key for a root password mtime inside a Kubernetes secret.
settingAdminPasswordMtimeKey = "admin.passwordMtime"
// settingServerSignatureKey designates the key for a server secret key inside a Kubernetes secret.
settingServerSignatureKey = "server.secretkey"
// settingServerCertificate designates the key for the public cert used in TLS
@ -107,13 +110,18 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi
settings.URL = argoCDCM.Data[settingURLKey]
}
// UpdateSettingsFromSecret transfers settings from a Kubernetes secret into an ArgoCDSettings struct.
func updateSettingsFromSecret(settings *ArgoCDSettings, argoCDSecret *apiv1.Secret) error {
adminPasswordHash, ok := argoCDSecret.Data[settingAdminPasswordKey]
adminPasswordHash, ok := argoCDSecret.Data[settingAdminPasswordHashKey]
if !ok {
return fmt.Errorf("admin user not found")
}
settings.LocalUsers = map[string]string{
common.ArgoCDAdminUsername: string(adminPasswordHash),
settings.AdminPasswordHash = string(adminPasswordHash)
settings.AdminPasswordMtime = time.Now().UTC()
if adminPasswordMtimeBytes, ok := argoCDSecret.Data[settingAdminPasswordMtimeKey]; ok {
if adminPasswordMtime, err := time.Parse(time.RFC3339, string(adminPasswordMtimeBytes)); err == nil {
settings.AdminPasswordMtime = adminPasswordMtime
}
}
secretKey, ok := argoCDSecret.Data[settingServerSignatureKey]
if !ok {
@ -194,7 +202,8 @@ func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error {
}
argoCDSecret.StringData = make(map[string]string)
argoCDSecret.StringData[settingServerSignatureKey] = string(settings.ServerSignature)
argoCDSecret.StringData[settingAdminPasswordKey] = settings.LocalUsers[common.ArgoCDAdminUsername]
argoCDSecret.StringData[settingAdminPasswordHashKey] = settings.AdminPasswordHash
argoCDSecret.StringData[settingAdminPasswordMtimeKey] = settings.AdminPasswordMtime.Format(time.RFC3339)
if settings.WebhookGitHubSecret != "" {
argoCDSecret.StringData[settingsWebhookGitHubSecretKey] = settings.WebhookGitHubSecret
}
@ -423,19 +432,15 @@ func UpdateSettings(defaultPassword string, settingsMgr *SettingsManager, update
errors.CheckError(err)
cdSettings.ServerSignature = signature
}
if cdSettings.LocalUsers == nil {
cdSettings.LocalUsers = make(map[string]string)
}
if _, ok := cdSettings.LocalUsers[common.ArgoCDAdminUsername]; !ok || updateSuperuser {
if cdSettings.AdminPasswordHash == "" || updateSuperuser {
passwordRaw := defaultPassword
if passwordRaw == "" {
passwordRaw = ReadAndConfirmPassword()
}
hashedPassword, err := password.HashPassword(passwordRaw)
errors.CheckError(err)
cdSettings.LocalUsers = map[string]string{
common.ArgoCDAdminUsername: hashedPassword,
}
cdSettings.AdminPasswordHash = hashedPassword
cdSettings.AdminPasswordMtime = time.Now().UTC()
}
if cdSettings.Certificate == nil {