Skip to content

Commit e4d0bd3

Browse files
alexmtAlexander Matyushentsev
authored andcommitted
Take into account number of unavailable replicas to decided if deployment is healthy or not (#270)
* Take into account number of unavailable replicas to decided if deployment is healthy or not * Run one controller for all e2e tests to reduce tests duration * Apply reviewer notes: use logic from kubectl/rollout_status.go to check deployment health
1 parent 18dc82d commit e4d0bd3

File tree

7 files changed

+144
-77
lines changed

7 files changed

+144
-77
lines changed

controller/health.go

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,30 +60,54 @@ func (ctrl *kubeAppHealthManager) getDeploymentHealth(config *rest.Config, names
6060
if err != nil {
6161
return nil, err
6262
}
63-
deploy, err := clientSet.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
63+
deployment, err := clientSet.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
6464
if err != nil {
6565
return nil, err
6666
}
67-
health := appv1.HealthStatus{
68-
Status: appv1.HealthStatusUnknown,
69-
}
70-
for _, condition := range deploy.Status.Conditions {
71-
// deployment is healthy is it successfully progressed
72-
if condition.Type == v1.DeploymentProgressing && condition.Status == "True" {
73-
health.Status = appv1.HealthStatusHealthy
74-
} else if condition.Type == v1.DeploymentReplicaFailure && condition.Status == "True" {
75-
health.Status = appv1.HealthStatusDegraded
76-
} else if condition.Type == v1.DeploymentProgressing && condition.Status == "False" {
77-
health.Status = appv1.HealthStatusDegraded
78-
} else if condition.Type == v1.DeploymentAvailable && condition.Status == "False" {
79-
health.Status = appv1.HealthStatusDegraded
67+
68+
if deployment.Generation <= deployment.Status.ObservedGeneration {
69+
cond := getDeploymentCondition(deployment.Status, v1.DeploymentProgressing)
70+
if cond != nil && cond.Reason == "ProgressDeadlineExceeded" {
71+
return &appv1.HealthStatus{
72+
Status: appv1.HealthStatusDegraded,
73+
StatusDetails: fmt.Sprintf("Deployment %q exceeded its progress deadline", name),
74+
}, nil
75+
} else if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas {
76+
return &appv1.HealthStatus{
77+
Status: appv1.HealthStatusProgressing,
78+
StatusDetails: fmt.Sprintf("Waiting for rollout to finish: %d out of %d new replicas have been updated...\n", deployment.Status.UpdatedReplicas, *deployment.Spec.Replicas),
79+
}, nil
80+
} else if deployment.Status.Replicas > deployment.Status.UpdatedReplicas {
81+
return &appv1.HealthStatus{
82+
Status: appv1.HealthStatusProgressing,
83+
StatusDetails: fmt.Sprintf("Waiting for rollout to finish: %d old replicas are pending termination...\n", deployment.Status.Replicas-deployment.Status.UpdatedReplicas),
84+
}, nil
85+
} else if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas {
86+
return &appv1.HealthStatus{
87+
Status: appv1.HealthStatusProgressing,
88+
StatusDetails: fmt.Sprintf("Waiting for rollout to finish: %d of %d updated replicas are available...\n", deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas),
89+
}, nil
8090
}
81-
if health.Status != appv1.HealthStatusUnknown {
82-
health.StatusDetails = fmt.Sprintf("%s:%s", condition.Reason, condition.Message)
83-
break
91+
} else {
92+
return &appv1.HealthStatus{
93+
Status: appv1.HealthStatusProgressing,
94+
StatusDetails: "Waiting for rollout to finish: observed deployment generation less then desired generation",
95+
}, nil
96+
}
97+
98+
return &appv1.HealthStatus{
99+
Status: appv1.HealthStatusHealthy,
100+
}, nil
101+
}
102+
103+
func getDeploymentCondition(status v1.DeploymentStatus, condType v1.DeploymentConditionType) *v1.DeploymentCondition {
104+
for i := range status.Conditions {
105+
c := status.Conditions[i]
106+
if c.Type == condType {
107+
return &c
84108
}
85109
}
86-
return &health, nil
110+
return nil
87111
}
88112

89113
func (ctrl *kubeAppHealthManager) GetAppHealth(server string, namespace string, comparisonResult *appv1.ComparisonResult) (*appv1.HealthStatus, error) {

examples/guestbook/components/guestbook-ui.jsonnet

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ local appDeployment = deployment
2323
params.replicas,
2424
container
2525
.new(params.name, params.image)
26-
.withPorts(containerPort.new(targetPort)),
26+
.withPorts(containerPort.new(targetPort)) + if params.command != null then { command: [ params.command ] } else {},
2727
labels);
2828

2929
k.core.v1.list.new([appService, appDeployment])

examples/guestbook/components/params.libsonnet

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
name: "guestbook-ui",
1313
replicas: 1,
1414
servicePort: 80,
15-
type: "LoadBalancer",
15+
type: "ClusterIP",
16+
command: null,
1617
},
1718
},
1819
}

server/application/application.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ func (s *Server) setAppOperation(ctx context.Context, appName string, operationC
510510
}
511511
a.Operation = op
512512
a.Status.OperationState = nil
513-
_, err = s.Update(ctx, a)
513+
_, err = s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Update(a)
514514
if err != nil && apierr.IsConflict(err) {
515515
log.Warnf("Failed to set operation for app '%s' due to update conflict. Retrying again...", appName)
516516
} else {

test/e2e/app_management_test.go

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package e2e
22

33
import (
4-
"context"
54
"strconv"
65
"testing"
76
"time"
@@ -18,9 +17,7 @@ import (
1817
)
1918

2019
func TestAppManagement(t *testing.T) {
21-
2220
testApp := &v1alpha1.Application{
23-
ObjectMeta: metav1.ObjectMeta{GenerateName: "e2e-test"},
2421
Spec: v1alpha1.ApplicationSpec{
2522
Source: v1alpha1.ApplicationSource{
2623
RepoURL: "https://github.com/argoproj/argo-cd.git", Path: ".", Environment: "minikube",
@@ -72,20 +69,12 @@ func TestAppManagement(t *testing.T) {
7269
})
7370

7471
t.Run("TestTrackAppStateAndSyncApp", func(t *testing.T) {
75-
ctrl := fixture.CreateController()
76-
ctx, cancel := context.WithCancel(context.Background())
77-
go ctrl.Run(ctx, 1, 1)
78-
defer cancel()
79-
80-
// create app and ensure it reaches OutOfSync state
8172
app := fixture.CreateApp(t, testApp)
8273
WaitUntil(t, func() (done bool, err error) {
8374
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
8475
return err == nil && app.Status.ComparisonResult.Status != v1alpha1.ComparisonStatusUnknown, err
8576
})
8677

87-
assert.Equal(t, v1alpha1.ComparisonStatusOutOfSync, app.Status.ComparisonResult.Status)
88-
8978
// sync app and make sure it reaches InSync state
9079
_, err := fixture.RunCli("app", "sync", app.Name)
9180
if err != nil {
@@ -103,30 +92,30 @@ func TestAppManagement(t *testing.T) {
10392

10493
t.Run("TestAppRollbackSuccessful", func(t *testing.T) {
10594
appWithHistory := testApp.DeepCopy()
106-
appWithHistory.Status.History = []v1alpha1.DeploymentInfo{{
107-
ID: 1,
108-
Revision: "abc",
95+
96+
// create app and ensure it's comparion status is not ComparisonStatusUnknown
97+
app := fixture.CreateApp(t, appWithHistory)
98+
app.Status.History = []v1alpha1.DeploymentInfo{{
99+
ID: 1,
100+
Revision: "abc",
101+
ComponentParameterOverrides: app.Spec.Source.ComponentParameterOverrides,
109102
}, {
110-
ID: 2,
111-
Revision: "cdb",
103+
ID: 2,
104+
Revision: "cdb",
105+
ComponentParameterOverrides: app.Spec.Source.ComponentParameterOverrides,
112106
}}
107+
app, err := fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Update(app)
108+
if err != nil {
109+
t.Fatalf("Unable to update app %v", err)
110+
}
113111

114-
ctrl := fixture.CreateController()
115-
ctx, cancel := context.WithCancel(context.Background())
116-
go ctrl.Run(ctx, 1, 1)
117-
defer cancel()
118-
119-
// create app and ensure it reaches OutOfSync state
120-
app := fixture.CreateApp(t, appWithHistory)
121112
WaitUntil(t, func() (done bool, err error) {
122113
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
123114
return err == nil && app.Status.ComparisonResult.Status != v1alpha1.ComparisonStatusUnknown, err
124115
})
125116

126-
assert.Equal(t, v1alpha1.ComparisonStatusOutOfSync, app.Status.ComparisonResult.Status)
127-
128117
// sync app and make sure it reaches InSync state
129-
_, err := fixture.RunCli("app", "rollback", app.Name, "1")
118+
_, err = fixture.RunCli("app", "rollback", app.Name, "1")
130119
if err != nil {
131120
t.Fatalf("Unable to sync app %v", err)
132121
}
@@ -146,11 +135,6 @@ func TestAppManagement(t *testing.T) {
146135
invalidApp := testApp.DeepCopy()
147136
invalidApp.Spec.Destination.Server = "https://not-registered-cluster/api"
148137

149-
ctrl := fixture.CreateController()
150-
ctx, cancel := context.WithCancel(context.Background())
151-
go ctrl.Run(ctx, 1, 1)
152-
defer cancel()
153-
154138
app := fixture.CreateApp(t, invalidApp)
155139

156140
WaitUntil(t, func() (done bool, err error) {
@@ -165,4 +149,39 @@ func TestAppManagement(t *testing.T) {
165149

166150
assert.Equal(t, v1alpha1.ComparisonStatusError, app.Status.ComparisonResult.Status)
167151
})
152+
153+
t.Run("TestArgoCDWaitEnsureAppIsNotCrashing", func(t *testing.T) {
154+
updatedApp := testApp.DeepCopy()
155+
156+
// deploy app and make sure it is healthy
157+
app := fixture.CreateApp(t, updatedApp)
158+
_, err := fixture.RunCli("app", "sync", app.Name)
159+
if err != nil {
160+
t.Fatalf("Unable to sync app %v", err)
161+
}
162+
163+
WaitUntil(t, func() (done bool, err error) {
164+
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
165+
return err == nil && app.Status.ComparisonResult.Status == v1alpha1.ComparisonStatusSynced && app.Status.Health.Status == v1alpha1.HealthStatusHealthy, err
166+
})
167+
168+
// deploy app which fails and make sure it became unhealthy
169+
app.Spec.Source.ComponentParameterOverrides = append(
170+
app.Spec.Source.ComponentParameterOverrides,
171+
v1alpha1.ComponentParameter{Name: "command", Value: "wrong-command", Component: "guestbook-ui"})
172+
_, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Update(app)
173+
if err != nil {
174+
t.Fatalf("Unable to set app parameter %v", err)
175+
}
176+
177+
_, err = fixture.RunCli("app", "sync", app.Name)
178+
if err != nil {
179+
t.Fatalf("Unable to sync app %v", err)
180+
}
181+
182+
WaitUntil(t, func() (done bool, err error) {
183+
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
184+
return err == nil && app.Status.ComparisonResult.Status == v1alpha1.ComparisonStatusSynced && app.Status.Health.Status == v1alpha1.HealthStatusDegraded, err
185+
})
186+
})
168187
}

test/e2e/fixture.go

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package e2e
22

33
import (
4-
"bytes"
54
"context"
65
"fmt"
76
"log"
@@ -13,6 +12,8 @@ import (
1312

1413
"encoding/json"
1514

15+
"strings"
16+
1617
"github.com/argoproj/argo-cd/cmd/argocd/commands"
1718
"github.com/argoproj/argo-cd/common"
1819
"github.com/argoproj/argo-cd/controller"
@@ -128,22 +129,11 @@ func (f *Fixture) setup() error {
128129
})
129130

130131
ctx, cancel := context.WithCancel(context.Background())
131-
go func() {
132-
err = repoServerGRPC.Serve(repoServerListener)
133-
}()
134132
go func() {
135133
apiServer.Run(ctx, apiServerPort)
136134
}()
137135

138-
f.tearDownCallback = func() {
139-
cancel()
140-
repoServerGRPC.Stop()
141-
}
142-
if err != nil {
143-
return err
144-
}
145-
146-
return waitUntilE(func() (done bool, err error) {
136+
err = waitUntilE(func() (done bool, err error) {
147137
clientset, err := f.NewApiClientset()
148138
if err != nil {
149139
return false, nil
@@ -156,6 +146,22 @@ func (f *Fixture) setup() error {
156146
_, err = appClient.List(context.Background(), &application.ApplicationQuery{})
157147
return err == nil, nil
158148
})
149+
150+
ctrl := f.createController()
151+
ctrlCtx, cancelCtrl := context.WithCancel(context.Background())
152+
go ctrl.Run(ctrlCtx, 1, 1)
153+
154+
go func() {
155+
err = repoServerGRPC.Serve(repoServerListener)
156+
}()
157+
158+
f.tearDownCallback = func() {
159+
cancel()
160+
cancelCtrl()
161+
repoServerGRPC.Stop()
162+
}
163+
164+
return err
159165
}
160166

161167
func (f *Fixture) ensureClusterRegistered() error {
@@ -251,22 +257,28 @@ func NewFixture() (*Fixture, error) {
251257

252258
// CreateApp creates application with appropriate controller instance id.
253259
func (f *Fixture) CreateApp(t *testing.T, application *v1alpha1.Application) *v1alpha1.Application {
260+
application = application.DeepCopy()
261+
application.Name = fmt.Sprintf("e2e-test-%v", time.Now().Unix())
254262
labels := application.ObjectMeta.Labels
255263
if labels == nil {
256264
labels = make(map[string]string)
257265
application.ObjectMeta.Labels = labels
258266
}
259267
labels[common.LabelKeyApplicationControllerInstanceID] = f.InstanceID
260268

269+
application.Spec.Source.ComponentParameterOverrides = append(
270+
application.Spec.Source.ComponentParameterOverrides,
271+
v1alpha1.ComponentParameter{Name: "name", Value: application.Name, Component: "guestbook-ui"})
272+
261273
app, err := f.AppClient.ArgoprojV1alpha1().Applications(f.Namespace).Create(application)
262274
if err != nil {
263275
t.Fatal(fmt.Sprintf("Unable to create app %v", err))
264276
}
265277
return app
266278
}
267279

268-
// CreateController creates new controller instance
269-
func (f *Fixture) CreateController() *controller.ApplicationController {
280+
// createController creates new controller instance
281+
func (f *Fixture) createController() *controller.ApplicationController {
270282
appStateManager := controller.NewAppStateManager(
271283
f.DB, f.AppClient, reposerver.NewRepositoryServerClientset(f.RepoServerAddress), f.Namespace)
272284

@@ -292,12 +304,21 @@ func (f *Fixture) NewApiClientset() (argocdclient.Client, error) {
292304
}
293305

294306
func (f *Fixture) RunCli(args ...string) (string, error) {
295-
cmd := commands.NewCommand()
296-
cmd.SetArgs(append(args, "--server", f.ApiServerAddress, "--plaintext"))
297-
output := new(bytes.Buffer)
298-
cmd.SetOutput(output)
299-
err := cmd.Execute()
300-
return output.String(), err
307+
args = append([]string{"run", "../../cmd/argocd/main.go"}, args...)
308+
cmd := exec.Command("go", append(args, "--server", f.ApiServerAddress, "--plaintext")...)
309+
outBytes, err := cmd.Output()
310+
if err != nil {
311+
exErr, ok := err.(*exec.ExitError)
312+
if !ok {
313+
return "", err
314+
}
315+
errOutput := string(exErr.Stderr)
316+
if outBytes != nil {
317+
errOutput = string(outBytes) + "\n" + errOutput
318+
}
319+
return "", fmt.Errorf(strings.TrimSpace(errOutput))
320+
}
321+
return string(outBytes), nil
301322
}
302323

303324
func waitUntilE(condition wait.ConditionFunc) error {

util/git/git.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,12 @@ func TestRepo(repo, username, password string, sshPrivateKey string) error {
9898
cmd.Env = env
9999
_, err = cmd.Output()
100100
if err != nil {
101-
exErr := err.(*exec.ExitError)
102-
errOutput := strings.Split(string(exErr.Stderr), "\n")[0]
103-
errOutput = redactPassword(errOutput, password)
104-
return fmt.Errorf("%s: %s", repo, errOutput)
101+
if exErr, ok := err.(*exec.ExitError); ok {
102+
errOutput := strings.Split(string(exErr.Stderr), "\n")[0]
103+
errOutput = redactPassword(errOutput, password)
104+
return fmt.Errorf("%s: %s", repo, errOutput)
105+
}
106+
return err
105107
}
106108
return nil
107109
}

0 commit comments

Comments
 (0)