mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
feat(appset): add Health field to ApplicationSet status (#25753)
Signed-off-by: Peter Jiang <peterjiang823@gmail.com>
This commit is contained in:
5
assets/swagger.json
generated
5
assets/swagger.json
generated
@@ -7299,7 +7299,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"applyNestedSelectors": {
|
||||
"description": "ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators\nDeprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future\nversion of the ApplicationSet CRD.",
|
||||
"description": "ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators.\n\nDeprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future\nversion of the ApplicationSet CRD.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"generators": {
|
||||
@@ -7357,6 +7357,9 @@
|
||||
"$ref": "#/definitions/v1alpha1ApplicationSetCondition"
|
||||
}
|
||||
},
|
||||
"health": {
|
||||
"$ref": "#/definitions/v1alpha1HealthStatus"
|
||||
},
|
||||
"resources": {
|
||||
"description": "Resources is a list of Applications resources managed by this application set.",
|
||||
"type": "array",
|
||||
|
||||
@@ -295,7 +295,8 @@ spec:
|
||||
spec:
|
||||
destination: {}
|
||||
project: ""
|
||||
status: {}
|
||||
status:
|
||||
health: {}
|
||||
---
|
||||
`,
|
||||
},
|
||||
@@ -325,7 +326,8 @@ spec:
|
||||
spec:
|
||||
destination: {}
|
||||
project: ""
|
||||
status: {}
|
||||
status:
|
||||
health: {}
|
||||
---
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -395,12 +395,12 @@ func printApplicationSetNames(apps []arogappsetv1.ApplicationSet) {
|
||||
func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
var fmtStr string
|
||||
headers := []any{"NAME", "PROJECT", "SYNCPOLICY", "CONDITIONS"}
|
||||
headers := []any{"NAME", "PROJECT", "SYNCPOLICY", "HEALTH", "CONDITIONS"}
|
||||
if *output == "wide" {
|
||||
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
|
||||
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
|
||||
headers = append(headers, "REPO", "PATH", "TARGET")
|
||||
} else {
|
||||
fmtStr = "%s\t%s\t%s\t%s\n"
|
||||
fmtStr = "%s\t%s\t%s\t%s\t%s\n"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, fmtStr, headers...)
|
||||
for _, app := range apps {
|
||||
@@ -414,6 +414,7 @@ func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string
|
||||
app.QualifiedName(),
|
||||
app.Spec.Template.Spec.Project,
|
||||
app.Spec.SyncPolicy,
|
||||
app.Status.Health.Status,
|
||||
conditions,
|
||||
}
|
||||
if *output == "wide" {
|
||||
@@ -437,6 +438,7 @@ func printAppSetSummaryTable(appSet *arogappsetv1.ApplicationSet) {
|
||||
fmt.Printf(printOpFmtStr, "Project:", appSet.Spec.Template.Spec.GetProject())
|
||||
fmt.Printf(printOpFmtStr, "Server:", getServerForAppSet(appSet))
|
||||
fmt.Printf(printOpFmtStr, "Namespace:", appSet.Spec.Template.Spec.Destination.Namespace)
|
||||
fmt.Printf(printOpFmtStr, "Health Status:", appSet.Status.Health.Status)
|
||||
if !appSet.Spec.Template.Spec.HasMultipleSources() {
|
||||
fmt.Println("Source:")
|
||||
} else {
|
||||
|
||||
@@ -107,7 +107,7 @@ func TestPrintApplicationSetTable(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expectation := "NAME PROJECT SYNCPOLICY CONDITIONS\napp-name default nil [{ResourcesUpToDate <nil> True }]\nteam-two/app-name default nil [{ResourcesUpToDate <nil> True }]\n"
|
||||
expectation := "NAME PROJECT SYNCPOLICY HEALTH CONDITIONS\napp-name default nil [{ResourcesUpToDate <nil> True }]\nteam-two/app-name default nil [{ResourcesUpToDate <nil> True }]\n"
|
||||
assert.Equal(t, expectation, output)
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ func TestPrintAppSetSummaryTable(t *testing.T) {
|
||||
Project: default
|
||||
Server:
|
||||
Namespace:
|
||||
Health Status:
|
||||
Source:
|
||||
- Repo:
|
||||
Target:
|
||||
@@ -213,6 +214,7 @@ SyncPolicy: <none>
|
||||
Project: default
|
||||
Server:
|
||||
Namespace:
|
||||
Health Status:
|
||||
Source:
|
||||
- Repo:
|
||||
Target:
|
||||
@@ -226,6 +228,7 @@ SyncPolicy: Automated
|
||||
Project: default
|
||||
Server:
|
||||
Namespace:
|
||||
Health Status:
|
||||
Source:
|
||||
- Repo:
|
||||
Target:
|
||||
@@ -239,6 +242,7 @@ SyncPolicy: Automated
|
||||
Project: default
|
||||
Server:
|
||||
Namespace:
|
||||
Health Status:
|
||||
Source:
|
||||
- Repo: test1
|
||||
Target: master1
|
||||
@@ -253,6 +257,7 @@ SyncPolicy: <none>
|
||||
Project: default
|
||||
Server:
|
||||
Namespace:
|
||||
Health Status:
|
||||
Sources:
|
||||
- Repo: test1
|
||||
Target: master1
|
||||
|
||||
10
manifests/core-install-with-hydrator.yaml
generated
10
manifests/core-install-with-hydrator.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
health:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
properties:
|
||||
|
||||
10
manifests/core-install.yaml
generated
10
manifests/core-install.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
health:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
properties:
|
||||
|
||||
10
manifests/crds/applicationset-crd.yaml
generated
10
manifests/crds/applicationset-crd.yaml
generated
@@ -23115,6 +23115,16 @@ spec:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
health:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
properties:
|
||||
|
||||
10
manifests/ha/install-with-hydrator.yaml
generated
10
manifests/ha/install-with-hydrator.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
health:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
properties:
|
||||
|
||||
10
manifests/ha/install.yaml
generated
10
manifests/ha/install.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
health:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
properties:
|
||||
|
||||
10
manifests/install-with-hydrator.yaml
generated
10
manifests/install-with-hydrator.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
health:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
properties:
|
||||
|
||||
10
manifests/install.yaml
generated
10
manifests/install.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
health:
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
resources:
|
||||
items:
|
||||
properties:
|
||||
|
||||
@@ -20,12 +20,13 @@ import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/util/security"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/util/security"
|
||||
)
|
||||
|
||||
// SecretRef struct for a reference to a secret key.
|
||||
@@ -70,7 +71,8 @@ type ApplicationSetSpec struct {
|
||||
Strategy *ApplicationSetStrategy `json:"strategy,omitempty" protobuf:"bytes,5,opt,name=strategy"`
|
||||
PreservedFields *ApplicationPreservedFields `json:"preservedFields,omitempty" protobuf:"bytes,6,opt,name=preservedFields"`
|
||||
GoTemplateOptions []string `json:"goTemplateOptions,omitempty" protobuf:"bytes,7,opt,name=goTemplateOptions"`
|
||||
// ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators
|
||||
// ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators.
|
||||
//
|
||||
// Deprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future
|
||||
// version of the ApplicationSet CRD.
|
||||
ApplyNestedSelectors bool `json:"applyNestedSelectors,omitempty" protobuf:"bytes,8,name=applyNestedSelectors"`
|
||||
@@ -811,6 +813,8 @@ type ApplicationSetStatus struct {
|
||||
// ResourcesCount is the total number of resources managed by this application set. The count may be higher than actual number of items in the Resources field when
|
||||
// the number of managed resources exceeds the limit imposed by the controller (to avoid making the status field too large).
|
||||
ResourcesCount int64 `json:"resourcesCount,omitempty" protobuf:"varint,4,opt,name=resourcesCount"`
|
||||
// Health contains information about the applicationset's current health status based on the applicationset conditions
|
||||
Health HealthStatus `json:"health,omitempty" protobuf:"bytes,5,opt,name=health"`
|
||||
}
|
||||
|
||||
// ApplicationSetCondition contains details about an applicationset condition, which is usually an error or warning
|
||||
@@ -937,6 +941,61 @@ func (a *ApplicationSet) RefreshRequired() bool {
|
||||
return found
|
||||
}
|
||||
|
||||
// CalculateHealth derives the health status from the applicationset conditions.
|
||||
// Health is determined by priority:
|
||||
// 1. ErrorOccurred=True → Degraded
|
||||
// 2. RolloutProgressing=True → Progressing
|
||||
// 3. ResourcesUpToDate=True → Healthy
|
||||
// 4. Otherwise → Unknown
|
||||
func (status *ApplicationSetStatus) CalculateHealth() HealthStatus {
|
||||
if len(status.Conditions) == 0 {
|
||||
return HealthStatus{
|
||||
Status: health.HealthStatusUnknown,
|
||||
Message: "No status conditions found for ApplicationSet",
|
||||
}
|
||||
}
|
||||
var (
|
||||
progressing *ApplicationSetCondition
|
||||
healthy *ApplicationSetCondition
|
||||
)
|
||||
|
||||
for _, c := range status.Conditions {
|
||||
if c.Status != ApplicationSetConditionStatusTrue {
|
||||
continue
|
||||
}
|
||||
switch c.Type {
|
||||
case ApplicationSetConditionErrorOccurred:
|
||||
return HealthStatus{
|
||||
Status: health.HealthStatusDegraded,
|
||||
Message: c.Message,
|
||||
}
|
||||
case ApplicationSetConditionRolloutProgressing:
|
||||
progressing = &c
|
||||
case ApplicationSetConditionResourcesUpToDate:
|
||||
healthy = &c
|
||||
}
|
||||
}
|
||||
|
||||
if progressing != nil {
|
||||
return HealthStatus{
|
||||
Status: health.HealthStatusProgressing,
|
||||
Message: progressing.Message,
|
||||
}
|
||||
}
|
||||
|
||||
if healthy != nil {
|
||||
return HealthStatus{
|
||||
Status: health.HealthStatusHealthy,
|
||||
Message: healthy.Message,
|
||||
}
|
||||
}
|
||||
|
||||
return HealthStatus{
|
||||
Status: health.HealthStatusUnknown,
|
||||
Message: "Waiting for health status to be determined",
|
||||
}
|
||||
}
|
||||
|
||||
// SetConditions updates the applicationset status conditions for a subset of evaluated types.
|
||||
// If the applicationset has a pre-existing condition of a type that is not in the evaluated list,
|
||||
// it will be preserved. If the applicationset has a pre-existing condition of a type, status, reason that
|
||||
@@ -995,6 +1054,9 @@ func (status *ApplicationSetStatus) SetConditions(conditions []ApplicationSetCon
|
||||
return left.LastTransitionTime.Before(right.LastTransitionTime)
|
||||
})
|
||||
status.Conditions = newConditions
|
||||
|
||||
// Recalculate health based on the updated conditions
|
||||
status.Health = status.CalculateHealth()
|
||||
}
|
||||
|
||||
func (t ApplicationSetConditionType) findConditionIndex(conditions []ApplicationSetCondition) int {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
@@ -172,6 +173,143 @@ func assertAppSetConditions(t *testing.T, expected []ApplicationSetCondition, ac
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationSetCalculateHealth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conditions []ApplicationSetCondition
|
||||
expectedHealth health.HealthStatusCode
|
||||
expectedMsg string
|
||||
}{
|
||||
{
|
||||
name: "no conditions returns unknown",
|
||||
conditions: []ApplicationSetCondition{},
|
||||
expectedHealth: health.HealthStatusUnknown,
|
||||
expectedMsg: "No status conditions found for ApplicationSet",
|
||||
},
|
||||
{
|
||||
name: "error occurred returns degraded",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionErrorOccurred, Status: ApplicationSetConditionStatusTrue, Message: "generator failed"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusDegraded,
|
||||
expectedMsg: "generator failed",
|
||||
},
|
||||
{
|
||||
name: "error occurred false does not indicate degraded",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionErrorOccurred, Status: ApplicationSetConditionStatusFalse, Message: "no error"},
|
||||
{Type: ApplicationSetConditionResourcesUpToDate, Status: ApplicationSetConditionStatusTrue, Message: "all good"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusHealthy,
|
||||
expectedMsg: "all good",
|
||||
},
|
||||
{
|
||||
name: "rollout progressing returns progressing",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionRolloutProgressing, Status: ApplicationSetConditionStatusTrue, Message: "rolling out 2/5"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusProgressing,
|
||||
expectedMsg: "rolling out 2/5",
|
||||
},
|
||||
{
|
||||
name: "resources up to date returns healthy",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionResourcesUpToDate, Status: ApplicationSetConditionStatusTrue, Message: "all applications synced"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusHealthy,
|
||||
expectedMsg: "all applications synced",
|
||||
},
|
||||
{
|
||||
name: "error takes priority over resources up to date",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionResourcesUpToDate, Status: ApplicationSetConditionStatusTrue, Message: "synced"},
|
||||
{Type: ApplicationSetConditionErrorOccurred, Status: ApplicationSetConditionStatusTrue, Message: "validation error"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusDegraded,
|
||||
expectedMsg: "validation error",
|
||||
},
|
||||
{
|
||||
name: "error takes priority over progressing",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionRolloutProgressing, Status: ApplicationSetConditionStatusTrue, Message: "rolling"},
|
||||
{Type: ApplicationSetConditionErrorOccurred, Status: ApplicationSetConditionStatusTrue, Message: "error during rollout"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusDegraded,
|
||||
expectedMsg: "error during rollout",
|
||||
},
|
||||
{
|
||||
name: "progressing takes priority over resources up to date",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionResourcesUpToDate, Status: ApplicationSetConditionStatusTrue, Message: "synced"},
|
||||
{Type: ApplicationSetConditionRolloutProgressing, Status: ApplicationSetConditionStatusTrue, Message: "rolling out"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusProgressing,
|
||||
expectedMsg: "rolling out",
|
||||
},
|
||||
{
|
||||
name: "parameters generated only returns unknown",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionParametersGenerated, Status: ApplicationSetConditionStatusTrue, Message: "params ok"},
|
||||
},
|
||||
expectedHealth: health.HealthStatusUnknown,
|
||||
expectedMsg: "Waiting for health status to be determined",
|
||||
},
|
||||
{
|
||||
name: "all conditions false returns unknown",
|
||||
conditions: []ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionErrorOccurred, Status: ApplicationSetConditionStatusFalse},
|
||||
{Type: ApplicationSetConditionResourcesUpToDate, Status: ApplicationSetConditionStatusFalse},
|
||||
},
|
||||
expectedHealth: health.HealthStatusUnknown,
|
||||
expectedMsg: "Waiting for health status to be determined",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
status := &ApplicationSetStatus{Conditions: tt.conditions}
|
||||
healthStatus := status.CalculateHealth()
|
||||
assert.Equal(t, tt.expectedHealth, healthStatus.Status)
|
||||
assert.Equal(t, tt.expectedMsg, healthStatus.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetConditionsUpdatesHealth(t *testing.T) {
|
||||
testRepo := "https://github.com/org/repo"
|
||||
namespace := "test"
|
||||
a := newTestAppSet("sample-app-set", namespace, testRepo)
|
||||
|
||||
// Initially no conditions, health should be unknown
|
||||
assert.Equal(t, health.HealthStatusCode(""), a.Status.Health.Status)
|
||||
|
||||
// Set ResourcesUpToDate condition
|
||||
a.Status.SetConditions([]ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionResourcesUpToDate, Status: ApplicationSetConditionStatusTrue, Message: "all synced"},
|
||||
}, map[ApplicationSetConditionType]bool{
|
||||
ApplicationSetConditionResourcesUpToDate: true,
|
||||
})
|
||||
assert.Equal(t, health.HealthStatusHealthy, a.Status.Health.Status)
|
||||
assert.Equal(t, "all synced", a.Status.Health.Message)
|
||||
|
||||
// Add error condition, health should become degraded
|
||||
a.Status.SetConditions([]ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionErrorOccurred, Status: ApplicationSetConditionStatusTrue, Message: "something broke"},
|
||||
}, map[ApplicationSetConditionType]bool{
|
||||
ApplicationSetConditionErrorOccurred: true,
|
||||
})
|
||||
assert.Equal(t, health.HealthStatusDegraded, a.Status.Health.Status)
|
||||
assert.Equal(t, "something broke", a.Status.Health.Message)
|
||||
|
||||
// Clear error, health should return to healthy
|
||||
a.Status.SetConditions([]ApplicationSetCondition{
|
||||
{Type: ApplicationSetConditionErrorOccurred, Status: ApplicationSetConditionStatusFalse, Message: ""},
|
||||
}, map[ApplicationSetConditionType]bool{
|
||||
ApplicationSetConditionErrorOccurred: true,
|
||||
})
|
||||
assert.Equal(t, health.HealthStatusHealthy, a.Status.Health.Status)
|
||||
}
|
||||
|
||||
func TestSCMProviderGeneratorGitlab_WillIncludeSharedProjects(t *testing.T) {
|
||||
settings := SCMProviderGeneratorGitlab{}
|
||||
assert.True(t, settings.WillIncludeSharedProjects())
|
||||
|
||||
1601
pkg/apis/application/v1alpha1/generated.pb.go
generated
1601
pkg/apis/application/v1alpha1/generated.pb.go
generated
File diff suppressed because it is too large
Load Diff
@@ -355,7 +355,8 @@ message ApplicationSetSpec {
|
||||
|
||||
repeated string goTemplateOptions = 7;
|
||||
|
||||
// ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators
|
||||
// ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators.
|
||||
//
|
||||
// Deprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future
|
||||
// version of the ApplicationSet CRD.
|
||||
optional bool applyNestedSelectors = 8;
|
||||
@@ -379,6 +380,9 @@ message ApplicationSetStatus {
|
||||
// ResourcesCount is the total number of resources managed by this application set. The count may be higher than actual number of items in the Resources field when
|
||||
// the number of managed resources exceeds the limit imposed by the controller (to avoid making the status field too large).
|
||||
optional int64 resourcesCount = 4;
|
||||
|
||||
// Health contains information about the applicationset's current health status based on the applicationset conditions
|
||||
optional HealthStatus health = 5;
|
||||
}
|
||||
|
||||
// ApplicationSetStrategy configures how generated Applications are updated in sequence.
|
||||
|
||||
@@ -834,6 +834,7 @@ func (in *ApplicationSetStatus) DeepCopyInto(out *ApplicationSetStatus) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.Health.DeepCopyInto(&out.Health)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2174,3 +2174,78 @@ func TestApplicationSetAPIListResourceEvents(t *testing.T) {
|
||||
}).
|
||||
When().Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{}))
|
||||
}
|
||||
|
||||
// TestApplicationSetHealthStatusCLI tests that the CLI commands display the health status field for an ApplicationSet.
|
||||
func TestApplicationSetHealthStatusCLI(t *testing.T) {
|
||||
expectedApp := v1alpha1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: application.ApplicationKind,
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "health-cli-guestbook",
|
||||
Namespace: utils.ArgoCDNamespace,
|
||||
Finalizers: []string{v1alpha1.ResourcesFinalizerName},
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &v1alpha1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Given(t).
|
||||
When().Create(v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "health-status-cli-test",
|
||||
Namespace: utils.ArgoCDNamespace,
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
GoTemplate: true,
|
||||
Template: v1alpha1.ApplicationSetTemplate{
|
||||
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "health-cli-guestbook"},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Source: &v1alpha1.ApplicationSource{
|
||||
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
TargetRevision: "HEAD",
|
||||
Path: "guestbook",
|
||||
},
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: "https://kubernetes.default.svc",
|
||||
Namespace: "guestbook",
|
||||
},
|
||||
},
|
||||
},
|
||||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
List: &v1alpha1.ListGenerator{
|
||||
Elements: []apiextensionsv1.JSON{{
|
||||
Raw: []byte(`{"cluster": "my-cluster"}`),
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Then().
|
||||
// Wait for the ApplicationSet to be ready
|
||||
Expect(ApplicationSetHasConditions("health-status-cli-test", ExpectedConditions)).
|
||||
|
||||
// Test 'argocd appset get' shows Health Status field
|
||||
When().AppSetGet("health-status-cli-test").
|
||||
Then().Expect(OutputContains("Health Status:")).
|
||||
|
||||
// Test 'argocd appset list' shows HEALTH column header
|
||||
When().AppSetList().
|
||||
Then().Expect(OutputContains("HEALTH")).
|
||||
|
||||
// Cleanup
|
||||
When().Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{expectedApp}))
|
||||
}
|
||||
|
||||
@@ -537,6 +537,29 @@ func (a *Actions) AppSet(appName string, flags ...string) *Actions {
|
||||
return a
|
||||
}
|
||||
|
||||
// AppSetGet runs 'argocd appset get' CLI command and stores the output
|
||||
func (a *Actions) AppSetGet(appSetName string, flags ...string) *Actions {
|
||||
a.context.t.Helper()
|
||||
args := []string{"appset", "get", appSetName}
|
||||
args = append(args, flags...)
|
||||
a.runCli(args...)
|
||||
return a
|
||||
}
|
||||
|
||||
// AppSetList runs 'argocd appset list' CLI command and stores the output
|
||||
func (a *Actions) AppSetList(flags ...string) *Actions {
|
||||
a.context.t.Helper()
|
||||
args := []string{"appset", "list"}
|
||||
args = append(args, flags...)
|
||||
a.runCli(args...)
|
||||
return a
|
||||
}
|
||||
|
||||
// GetLastOutput returns the output from the last CLI command
|
||||
func (a *Actions) GetLastOutput() string {
|
||||
return a.lastOutput
|
||||
}
|
||||
|
||||
func (a *Actions) runCli(args ...string) {
|
||||
a.context.T().Helper()
|
||||
a.lastOutput, a.lastError = fixture.RunCli(args...)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
@@ -39,6 +40,19 @@ func Success(message string) Expectation {
|
||||
}
|
||||
}
|
||||
|
||||
// OutputContains asserts that the last command output contains the expected substring
|
||||
func OutputContains(expected string) Expectation {
|
||||
return func(c *Consequences) (state, string) {
|
||||
if c.actions.lastError != nil {
|
||||
return failed, fmt.Sprintf("error: %v", c.actions.lastError)
|
||||
}
|
||||
if !strings.Contains(c.actions.lastOutput, expected) {
|
||||
return failed, fmt.Sprintf("output did not contain '%s', got: %s", expected, c.actions.lastOutput)
|
||||
}
|
||||
return succeeded, fmt.Sprintf("output contained '%s'", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// Error asserts that the last command was an error with substring match
|
||||
func Error(message, err string) Expectation {
|
||||
return func(c *Consequences) (state, string) {
|
||||
@@ -79,6 +93,23 @@ func ApplicationsExist(expectedApps []v1alpha1.Application) Expectation {
|
||||
}
|
||||
}
|
||||
|
||||
// ApplicationSetHasHealthStatus checks whether the ApplicationSet has the expected health status.
|
||||
func ApplicationSetHasHealthStatus(applicationSetName string, expectedHealthStatus health.HealthStatusCode) Expectation {
|
||||
return func(c *Consequences) (state, string) {
|
||||
// retrieve the application set
|
||||
foundApplicationSet := c.applicationSet(applicationSetName)
|
||||
if foundApplicationSet == nil {
|
||||
return pending, fmt.Sprintf("application set '%s' not found", applicationSetName)
|
||||
}
|
||||
|
||||
if foundApplicationSet.Status.Health.Status != expectedHealthStatus {
|
||||
return pending, fmt.Sprintf("application set health status is '%s', expected '%s'",
|
||||
foundApplicationSet.Status.Health.Status, expectedHealthStatus)
|
||||
}
|
||||
return succeeded, fmt.Sprintf("application set has expected health status '%s'", expectedHealthStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// ApplicationSetHasConditions checks whether each of the 'expectedConditions' exist in the ApplicationSet status, and are
|
||||
// equivalent to provided values.
|
||||
func ApplicationSetHasConditions(expectedConditions []v1alpha1.ApplicationSetCondition) Expectation {
|
||||
|
||||
Reference in New Issue
Block a user