Skip to content

Commit

Permalink
Take into account number of unavailable replicas to decided if deploy…
Browse files Browse the repository at this point in the history
…ment 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
  • Loading branch information
alexmt authored and Alexander Matyushentsev committed Jun 7, 2018
1 parent 18dc82d commit e4d0bd3
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 77 deletions.
60 changes: 42 additions & 18 deletions controller/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,54 @@ func (ctrl *kubeAppHealthManager) getDeploymentHealth(config *rest.Config, names
if err != nil {
return nil, err
}
deploy, err := clientSet.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
deployment, err := clientSet.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
if err != nil {
return nil, err
}
health := appv1.HealthStatus{
Status: appv1.HealthStatusUnknown,
}
for _, condition := range deploy.Status.Conditions {
// deployment is healthy is it successfully progressed
if condition.Type == v1.DeploymentProgressing && condition.Status == "True" {
health.Status = appv1.HealthStatusHealthy
} else if condition.Type == v1.DeploymentReplicaFailure && condition.Status == "True" {
health.Status = appv1.HealthStatusDegraded
} else if condition.Type == v1.DeploymentProgressing && condition.Status == "False" {
health.Status = appv1.HealthStatusDegraded
} else if condition.Type == v1.DeploymentAvailable && condition.Status == "False" {
health.Status = appv1.HealthStatusDegraded

if deployment.Generation <= deployment.Status.ObservedGeneration {
cond := getDeploymentCondition(deployment.Status, v1.DeploymentProgressing)
if cond != nil && cond.Reason == "ProgressDeadlineExceeded" {
return &appv1.HealthStatus{
Status: appv1.HealthStatusDegraded,
StatusDetails: fmt.Sprintf("Deployment %q exceeded its progress deadline", name),
}, nil
} else if deployment.Spec.Replicas != nil && deployment.Status.UpdatedReplicas < *deployment.Spec.Replicas {
return &appv1.HealthStatus{
Status: appv1.HealthStatusProgressing,
StatusDetails: fmt.Sprintf("Waiting for rollout to finish: %d out of %d new replicas have been updated...\n", deployment.Status.UpdatedReplicas, *deployment.Spec.Replicas),
}, nil
} else if deployment.Status.Replicas > deployment.Status.UpdatedReplicas {
return &appv1.HealthStatus{
Status: appv1.HealthStatusProgressing,
StatusDetails: fmt.Sprintf("Waiting for rollout to finish: %d old replicas are pending termination...\n", deployment.Status.Replicas-deployment.Status.UpdatedReplicas),
}, nil
} else if deployment.Status.AvailableReplicas < deployment.Status.UpdatedReplicas {
return &appv1.HealthStatus{
Status: appv1.HealthStatusProgressing,
StatusDetails: fmt.Sprintf("Waiting for rollout to finish: %d of %d updated replicas are available...\n", deployment.Status.AvailableReplicas, deployment.Status.UpdatedReplicas),
}, nil
}
if health.Status != appv1.HealthStatusUnknown {
health.StatusDetails = fmt.Sprintf("%s:%s", condition.Reason, condition.Message)
break
} else {
return &appv1.HealthStatus{
Status: appv1.HealthStatusProgressing,
StatusDetails: "Waiting for rollout to finish: observed deployment generation less then desired generation",
}, nil
}

return &appv1.HealthStatus{
Status: appv1.HealthStatusHealthy,
}, nil
}

func getDeploymentCondition(status v1.DeploymentStatus, condType v1.DeploymentConditionType) *v1.DeploymentCondition {
for i := range status.Conditions {
c := status.Conditions[i]
if c.Type == condType {
return &c
}
}
return &health, nil
return nil
}

func (ctrl *kubeAppHealthManager) GetAppHealth(server string, namespace string, comparisonResult *appv1.ComparisonResult) (*appv1.HealthStatus, error) {
Expand Down
2 changes: 1 addition & 1 deletion examples/guestbook/components/guestbook-ui.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ local appDeployment = deployment
params.replicas,
container
.new(params.name, params.image)
.withPorts(containerPort.new(targetPort)),
.withPorts(containerPort.new(targetPort)) + if params.command != null then { command: [ params.command ] } else {},
labels);

k.core.v1.list.new([appService, appDeployment])
3 changes: 2 additions & 1 deletion examples/guestbook/components/params.libsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
name: "guestbook-ui",
replicas: 1,
servicePort: 80,
type: "LoadBalancer",
type: "ClusterIP",
command: null,
},
},
}
2 changes: 1 addition & 1 deletion server/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ func (s *Server) setAppOperation(ctx context.Context, appName string, operationC
}
a.Operation = op
a.Status.OperationState = nil
_, err = s.Update(ctx, a)
_, err = s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Update(a)
if err != nil && apierr.IsConflict(err) {
log.Warnf("Failed to set operation for app '%s' due to update conflict. Retrying again...", appName)
} else {
Expand Down
81 changes: 50 additions & 31 deletions test/e2e/app_management_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"context"
"strconv"
"testing"
"time"
Expand All @@ -18,9 +17,7 @@ import (
)

func TestAppManagement(t *testing.T) {

testApp := &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{GenerateName: "e2e-test"},
Spec: v1alpha1.ApplicationSpec{
Source: v1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argo-cd.git", Path: ".", Environment: "minikube",
Expand Down Expand Up @@ -72,20 +69,12 @@ func TestAppManagement(t *testing.T) {
})

t.Run("TestTrackAppStateAndSyncApp", func(t *testing.T) {
ctrl := fixture.CreateController()
ctx, cancel := context.WithCancel(context.Background())
go ctrl.Run(ctx, 1, 1)
defer cancel()

// create app and ensure it reaches OutOfSync state
app := fixture.CreateApp(t, testApp)
WaitUntil(t, func() (done bool, err error) {
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
return err == nil && app.Status.ComparisonResult.Status != v1alpha1.ComparisonStatusUnknown, err
})

assert.Equal(t, v1alpha1.ComparisonStatusOutOfSync, app.Status.ComparisonResult.Status)

// sync app and make sure it reaches InSync state
_, err := fixture.RunCli("app", "sync", app.Name)
if err != nil {
Expand All @@ -103,30 +92,30 @@ func TestAppManagement(t *testing.T) {

t.Run("TestAppRollbackSuccessful", func(t *testing.T) {
appWithHistory := testApp.DeepCopy()
appWithHistory.Status.History = []v1alpha1.DeploymentInfo{{
ID: 1,
Revision: "abc",

// create app and ensure it's comparion status is not ComparisonStatusUnknown
app := fixture.CreateApp(t, appWithHistory)
app.Status.History = []v1alpha1.DeploymentInfo{{
ID: 1,
Revision: "abc",
ComponentParameterOverrides: app.Spec.Source.ComponentParameterOverrides,
}, {
ID: 2,
Revision: "cdb",
ID: 2,
Revision: "cdb",
ComponentParameterOverrides: app.Spec.Source.ComponentParameterOverrides,
}}
app, err := fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Update(app)
if err != nil {
t.Fatalf("Unable to update app %v", err)
}

ctrl := fixture.CreateController()
ctx, cancel := context.WithCancel(context.Background())
go ctrl.Run(ctx, 1, 1)
defer cancel()

// create app and ensure it reaches OutOfSync state
app := fixture.CreateApp(t, appWithHistory)
WaitUntil(t, func() (done bool, err error) {
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
return err == nil && app.Status.ComparisonResult.Status != v1alpha1.ComparisonStatusUnknown, err
})

assert.Equal(t, v1alpha1.ComparisonStatusOutOfSync, app.Status.ComparisonResult.Status)

// sync app and make sure it reaches InSync state
_, err := fixture.RunCli("app", "rollback", app.Name, "1")
_, err = fixture.RunCli("app", "rollback", app.Name, "1")
if err != nil {
t.Fatalf("Unable to sync app %v", err)
}
Expand All @@ -146,11 +135,6 @@ func TestAppManagement(t *testing.T) {
invalidApp := testApp.DeepCopy()
invalidApp.Spec.Destination.Server = "https://not-registered-cluster/api"

ctrl := fixture.CreateController()
ctx, cancel := context.WithCancel(context.Background())
go ctrl.Run(ctx, 1, 1)
defer cancel()

app := fixture.CreateApp(t, invalidApp)

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

assert.Equal(t, v1alpha1.ComparisonStatusError, app.Status.ComparisonResult.Status)
})

t.Run("TestArgoCDWaitEnsureAppIsNotCrashing", func(t *testing.T) {
updatedApp := testApp.DeepCopy()

// deploy app and make sure it is healthy
app := fixture.CreateApp(t, updatedApp)
_, err := fixture.RunCli("app", "sync", app.Name)
if err != nil {
t.Fatalf("Unable to sync app %v", err)
}

WaitUntil(t, func() (done bool, err error) {
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
return err == nil && app.Status.ComparisonResult.Status == v1alpha1.ComparisonStatusSynced && app.Status.Health.Status == v1alpha1.HealthStatusHealthy, err
})

// deploy app which fails and make sure it became unhealthy
app.Spec.Source.ComponentParameterOverrides = append(
app.Spec.Source.ComponentParameterOverrides,
v1alpha1.ComponentParameter{Name: "command", Value: "wrong-command", Component: "guestbook-ui"})
_, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Update(app)
if err != nil {
t.Fatalf("Unable to set app parameter %v", err)
}

_, err = fixture.RunCli("app", "sync", app.Name)
if err != nil {
t.Fatalf("Unable to sync app %v", err)
}

WaitUntil(t, func() (done bool, err error) {
app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
return err == nil && app.Status.ComparisonResult.Status == v1alpha1.ComparisonStatusSynced && app.Status.Health.Status == v1alpha1.HealthStatusDegraded, err
})
})
}
63 changes: 42 additions & 21 deletions test/e2e/fixture.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"bytes"
"context"
"fmt"
"log"
Expand All @@ -13,6 +12,8 @@ import (

"encoding/json"

"strings"

"github.com/argoproj/argo-cd/cmd/argocd/commands"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/controller"
Expand Down Expand Up @@ -128,22 +129,11 @@ func (f *Fixture) setup() error {
})

ctx, cancel := context.WithCancel(context.Background())
go func() {
err = repoServerGRPC.Serve(repoServerListener)
}()
go func() {
apiServer.Run(ctx, apiServerPort)
}()

f.tearDownCallback = func() {
cancel()
repoServerGRPC.Stop()
}
if err != nil {
return err
}

return waitUntilE(func() (done bool, err error) {
err = waitUntilE(func() (done bool, err error) {
clientset, err := f.NewApiClientset()
if err != nil {
return false, nil
Expand All @@ -156,6 +146,22 @@ func (f *Fixture) setup() error {
_, err = appClient.List(context.Background(), &application.ApplicationQuery{})
return err == nil, nil
})

ctrl := f.createController()
ctrlCtx, cancelCtrl := context.WithCancel(context.Background())
go ctrl.Run(ctrlCtx, 1, 1)

go func() {
err = repoServerGRPC.Serve(repoServerListener)
}()

f.tearDownCallback = func() {
cancel()
cancelCtrl()
repoServerGRPC.Stop()
}

return err
}

func (f *Fixture) ensureClusterRegistered() error {
Expand Down Expand Up @@ -251,22 +257,28 @@ func NewFixture() (*Fixture, error) {

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

application.Spec.Source.ComponentParameterOverrides = append(
application.Spec.Source.ComponentParameterOverrides,
v1alpha1.ComponentParameter{Name: "name", Value: application.Name, Component: "guestbook-ui"})

app, err := f.AppClient.ArgoprojV1alpha1().Applications(f.Namespace).Create(application)
if err != nil {
t.Fatal(fmt.Sprintf("Unable to create app %v", err))
}
return app
}

// CreateController creates new controller instance
func (f *Fixture) CreateController() *controller.ApplicationController {
// createController creates new controller instance
func (f *Fixture) createController() *controller.ApplicationController {
appStateManager := controller.NewAppStateManager(
f.DB, f.AppClient, reposerver.NewRepositoryServerClientset(f.RepoServerAddress), f.Namespace)

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

func (f *Fixture) RunCli(args ...string) (string, error) {
cmd := commands.NewCommand()
cmd.SetArgs(append(args, "--server", f.ApiServerAddress, "--plaintext"))
output := new(bytes.Buffer)
cmd.SetOutput(output)
err := cmd.Execute()
return output.String(), err
args = append([]string{"run", "../../cmd/argocd/main.go"}, args...)
cmd := exec.Command("go", append(args, "--server", f.ApiServerAddress, "--plaintext")...)
outBytes, err := cmd.Output()
if err != nil {
exErr, ok := err.(*exec.ExitError)
if !ok {
return "", err
}
errOutput := string(exErr.Stderr)
if outBytes != nil {
errOutput = string(outBytes) + "\n" + errOutput
}
return "", fmt.Errorf(strings.TrimSpace(errOutput))
}
return string(outBytes), nil
}

func waitUntilE(condition wait.ConditionFunc) error {
Expand Down
10 changes: 6 additions & 4 deletions util/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ func TestRepo(repo, username, password string, sshPrivateKey string) error {
cmd.Env = env
_, err = cmd.Output()
if err != nil {
exErr := err.(*exec.ExitError)
errOutput := strings.Split(string(exErr.Stderr), "\n")[0]
errOutput = redactPassword(errOutput, password)
return fmt.Errorf("%s: %s", repo, errOutput)
if exErr, ok := err.(*exec.ExitError); ok {
errOutput := strings.Split(string(exErr.Stderr), "\n")[0]
errOutput = redactPassword(errOutput, password)
return fmt.Errorf("%s: %s", repo, errOutput)
}
return err
}
return nil
}
Expand Down

0 comments on commit e4d0bd3

Please sign in to comment.