mirror of
https://github.com/argoproj/argo-cd
synced 2026-05-23 17:28:44 +00:00
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:
parent
6a7df88cf4
commit
7b6b945cbf
6 changed files with 221 additions and 141 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue