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",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"applyNestedSelectors": {
|
"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"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"generators": {
|
"generators": {
|
||||||
@@ -7357,6 +7357,9 @@
|
|||||||
"$ref": "#/definitions/v1alpha1ApplicationSetCondition"
|
"$ref": "#/definitions/v1alpha1ApplicationSetCondition"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"health": {
|
||||||
|
"$ref": "#/definitions/v1alpha1HealthStatus"
|
||||||
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"description": "Resources is a list of Applications resources managed by this application set.",
|
"description": "Resources is a list of Applications resources managed by this application set.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|||||||
@@ -295,7 +295,8 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
destination: {}
|
destination: {}
|
||||||
project: ""
|
project: ""
|
||||||
status: {}
|
status:
|
||||||
|
health: {}
|
||||||
---
|
---
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
@@ -325,7 +326,8 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
destination: {}
|
destination: {}
|
||||||
project: ""
|
project: ""
|
||||||
status: {}
|
status:
|
||||||
|
health: {}
|
||||||
---
|
---
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -395,12 +395,12 @@ func printApplicationSetNames(apps []arogappsetv1.ApplicationSet) {
|
|||||||
func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string) {
|
func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string) {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||||
var fmtStr string
|
var fmtStr string
|
||||||
headers := []any{"NAME", "PROJECT", "SYNCPOLICY", "CONDITIONS"}
|
headers := []any{"NAME", "PROJECT", "SYNCPOLICY", "HEALTH", "CONDITIONS"}
|
||||||
if *output == "wide" {
|
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")
|
headers = append(headers, "REPO", "PATH", "TARGET")
|
||||||
} else {
|
} 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...)
|
_, _ = fmt.Fprintf(w, fmtStr, headers...)
|
||||||
for _, app := range apps {
|
for _, app := range apps {
|
||||||
@@ -414,6 +414,7 @@ func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string
|
|||||||
app.QualifiedName(),
|
app.QualifiedName(),
|
||||||
app.Spec.Template.Spec.Project,
|
app.Spec.Template.Spec.Project,
|
||||||
app.Spec.SyncPolicy,
|
app.Spec.SyncPolicy,
|
||||||
|
app.Status.Health.Status,
|
||||||
conditions,
|
conditions,
|
||||||
}
|
}
|
||||||
if *output == "wide" {
|
if *output == "wide" {
|
||||||
@@ -437,6 +438,7 @@ func printAppSetSummaryTable(appSet *arogappsetv1.ApplicationSet) {
|
|||||||
fmt.Printf(printOpFmtStr, "Project:", appSet.Spec.Template.Spec.GetProject())
|
fmt.Printf(printOpFmtStr, "Project:", appSet.Spec.Template.Spec.GetProject())
|
||||||
fmt.Printf(printOpFmtStr, "Server:", getServerForAppSet(appSet))
|
fmt.Printf(printOpFmtStr, "Server:", getServerForAppSet(appSet))
|
||||||
fmt.Printf(printOpFmtStr, "Namespace:", appSet.Spec.Template.Spec.Destination.Namespace)
|
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() {
|
if !appSet.Spec.Template.Spec.HasMultipleSources() {
|
||||||
fmt.Println("Source:")
|
fmt.Println("Source:")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func TestPrintApplicationSetTable(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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)
|
assert.Equal(t, expectation, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +200,7 @@ func TestPrintAppSetSummaryTable(t *testing.T) {
|
|||||||
Project: default
|
Project: default
|
||||||
Server:
|
Server:
|
||||||
Namespace:
|
Namespace:
|
||||||
|
Health Status:
|
||||||
Source:
|
Source:
|
||||||
- Repo:
|
- Repo:
|
||||||
Target:
|
Target:
|
||||||
@@ -213,6 +214,7 @@ SyncPolicy: <none>
|
|||||||
Project: default
|
Project: default
|
||||||
Server:
|
Server:
|
||||||
Namespace:
|
Namespace:
|
||||||
|
Health Status:
|
||||||
Source:
|
Source:
|
||||||
- Repo:
|
- Repo:
|
||||||
Target:
|
Target:
|
||||||
@@ -226,6 +228,7 @@ SyncPolicy: Automated
|
|||||||
Project: default
|
Project: default
|
||||||
Server:
|
Server:
|
||||||
Namespace:
|
Namespace:
|
||||||
|
Health Status:
|
||||||
Source:
|
Source:
|
||||||
- Repo:
|
- Repo:
|
||||||
Target:
|
Target:
|
||||||
@@ -239,6 +242,7 @@ SyncPolicy: Automated
|
|||||||
Project: default
|
Project: default
|
||||||
Server:
|
Server:
|
||||||
Namespace:
|
Namespace:
|
||||||
|
Health Status:
|
||||||
Source:
|
Source:
|
||||||
- Repo: test1
|
- Repo: test1
|
||||||
Target: master1
|
Target: master1
|
||||||
@@ -253,6 +257,7 @@ SyncPolicy: <none>
|
|||||||
Project: default
|
Project: default
|
||||||
Server:
|
Server:
|
||||||
Namespace:
|
Namespace:
|
||||||
|
Health Status:
|
||||||
Sources:
|
Sources:
|
||||||
- Repo: test1
|
- Repo: test1
|
||||||
Target: master1
|
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
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
health:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
resources:
|
resources:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
10
manifests/core-install.yaml
generated
10
manifests/core-install.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
|||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
health:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
resources:
|
resources:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
10
manifests/crds/applicationset-crd.yaml
generated
10
manifests/crds/applicationset-crd.yaml
generated
@@ -23115,6 +23115,16 @@ spec:
|
|||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
health:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
resources:
|
resources:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
10
manifests/ha/install-with-hydrator.yaml
generated
10
manifests/ha/install-with-hydrator.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
|||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
health:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
resources:
|
resources:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
10
manifests/ha/install.yaml
generated
10
manifests/ha/install.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
|||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
health:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
resources:
|
resources:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
10
manifests/install-with-hydrator.yaml
generated
10
manifests/install-with-hydrator.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
|||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
health:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
resources:
|
resources:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
10
manifests/install.yaml
generated
10
manifests/install.yaml
generated
@@ -30119,6 +30119,16 @@ spec:
|
|||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
health:
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
resources:
|
resources:
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/argoproj/argo-cd/v3/common"
|
"github.com/argoproj/gitops-engine/pkg/health"
|
||||||
"github.com/argoproj/argo-cd/v3/util/security"
|
|
||||||
|
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"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.
|
// 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"`
|
Strategy *ApplicationSetStrategy `json:"strategy,omitempty" protobuf:"bytes,5,opt,name=strategy"`
|
||||||
PreservedFields *ApplicationPreservedFields `json:"preservedFields,omitempty" protobuf:"bytes,6,opt,name=preservedFields"`
|
PreservedFields *ApplicationPreservedFields `json:"preservedFields,omitempty" protobuf:"bytes,6,opt,name=preservedFields"`
|
||||||
GoTemplateOptions []string `json:"goTemplateOptions,omitempty" protobuf:"bytes,7,opt,name=goTemplateOptions"`
|
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
|
// Deprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future
|
||||||
// version of the ApplicationSet CRD.
|
// version of the ApplicationSet CRD.
|
||||||
ApplyNestedSelectors bool `json:"applyNestedSelectors,omitempty" protobuf:"bytes,8,name=applyNestedSelectors"`
|
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
|
// 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).
|
// 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"`
|
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
|
// 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
|
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.
|
// 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,
|
// 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
|
// 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)
|
return left.LastTransitionTime.Before(right.LastTransitionTime)
|
||||||
})
|
})
|
||||||
status.Conditions = newConditions
|
status.Conditions = newConditions
|
||||||
|
|
||||||
|
// Recalculate health based on the updated conditions
|
||||||
|
status.Health = status.CalculateHealth()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t ApplicationSetConditionType) findConditionIndex(conditions []ApplicationSetCondition) int {
|
func (t ApplicationSetConditionType) findConditionIndex(conditions []ApplicationSetCondition) int {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/argoproj/gitops-engine/pkg/health"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/utils/ptr"
|
"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) {
|
func TestSCMProviderGeneratorGitlab_WillIncludeSharedProjects(t *testing.T) {
|
||||||
settings := SCMProviderGeneratorGitlab{}
|
settings := SCMProviderGeneratorGitlab{}
|
||||||
assert.True(t, settings.WillIncludeSharedProjects())
|
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;
|
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
|
// Deprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future
|
||||||
// version of the ApplicationSet CRD.
|
// version of the ApplicationSet CRD.
|
||||||
optional bool applyNestedSelectors = 8;
|
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
|
// 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).
|
// the number of managed resources exceeds the limit imposed by the controller (to avoid making the status field too large).
|
||||||
optional int64 resourcesCount = 4;
|
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.
|
// 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)[i].DeepCopyInto(&(*out)[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
in.Health.DeepCopyInto(&out.Health)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2174,3 +2174,78 @@ func TestApplicationSetAPIListResourceEvents(t *testing.T) {
|
|||||||
}).
|
}).
|
||||||
When().Delete().Then().Expect(ApplicationsDoNotExist([]v1alpha1.Application{}))
|
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
|
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) {
|
func (a *Actions) runCli(args ...string) {
|
||||||
a.context.T().Helper()
|
a.context.T().Helper()
|
||||||
a.lastOutput, a.lastError = fixture.RunCli(args...)
|
a.lastOutput, a.lastError = fixture.RunCli(args...)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||||
|
"github.com/argoproj/gitops-engine/pkg/health"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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
|
// Error asserts that the last command was an error with substring match
|
||||||
func Error(message, err string) Expectation {
|
func Error(message, err string) Expectation {
|
||||||
return func(c *Consequences) (state, string) {
|
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
|
// ApplicationSetHasConditions checks whether each of the 'expectedConditions' exist in the ApplicationSet status, and are
|
||||||
// equivalent to provided values.
|
// equivalent to provided values.
|
||||||
func ApplicationSetHasConditions(expectedConditions []v1alpha1.ApplicationSetCondition) Expectation {
|
func ApplicationSetHasConditions(expectedConditions []v1alpha1.ApplicationSetCondition) Expectation {
|
||||||
|
|||||||
Reference in New Issue
Block a user