diff --git a/common/common.go b/common/common.go index d2e47aa5b1607a..eea862644d1663 100644 --- a/common/common.go +++ b/common/common.go @@ -185,6 +185,10 @@ const ( // AnnotationCompareOptions is a comma-separated list of options for comparison AnnotationCompareOptions = "argocd.argoproj.io/compare-options" + // AnnotationIgnoreHealthCheck when set on an Application's immediate child indicates that its health check + // can be disregarded. + AnnotationIgnoreHealthCheck = "argocd.argoproj.io/ignore-healthcheck" + // AnnotationKeyManagedBy is annotation name which indicates that k8s resource is managed by an application. AnnotationKeyManagedBy = "managed-by" // AnnotationValueManagedByArgoCD is a 'managed-by' annotation value for resources managed by Argo CD diff --git a/controller/health.go b/controller/health.go index f713a574f57d31..2081ad80716276 100644 --- a/controller/health.go +++ b/controller/health.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/pkg/apis/application" appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/util/lua" @@ -24,6 +25,9 @@ func setApplicationHealth(resources []managedResource, statuses []appv1.Resource if res.Target != nil && hookutil.Skip(res.Target) { continue } + if res.Target != nil && res.Target.GetAnnotations() != nil && res.Target.GetAnnotations()[common.AnnotationIgnoreHealthCheck] == "true" { + continue + } if res.Live != nil && (hookutil.IsHook(res.Live) || ignore.Ignore(res.Live)) { continue diff --git a/controller/health_test.go b/controller/health_test.go index efaf4b2a8fc807..8fe848ce5c398e 100644 --- a/controller/health_test.go +++ b/controller/health_test.go @@ -65,6 +65,15 @@ func TestSetApplicationHealth(t *testing.T) { healthStatus, err = setApplicationHealth(resources, resourceStatuses, nil, app, true) require.NoError(t, err) assert.Equal(t, health.HealthStatusHealthy, healthStatus.Status) + + // now we set the `argocd.argoproj.io/ignore-healthcheck: "true"` annotation on the job's target. + // The app is considered healthy + failedJob.SetAnnotations(nil) + failedJobIgnoreHealthcheck := resourceFromFile("./testdata/job-failed-ignore-healthcheck.yaml") + resources[1].Target = &failedJobIgnoreHealthcheck + healthStatus, err = setApplicationHealth(resources, resourceStatuses, nil, app, true) + require.NoError(t, err) + assert.Equal(t, health.HealthStatusHealthy, healthStatus.Status) } func TestSetApplicationHealth_ResourceHealthNotPersisted(t *testing.T) { diff --git a/controller/testdata/job-failed-ignore-healthcheck.yaml b/controller/testdata/job-failed-ignore-healthcheck.yaml new file mode 100644 index 00000000000000..62a952203bd124 --- /dev/null +++ b/controller/testdata/job-failed-ignore-healthcheck.yaml @@ -0,0 +1,36 @@ +apiVersion: batch/v1 +kind: Job +metadata: + annotations: + argocd.argoproj.io/ignore-healthcheck: "true" + labels: + job-name: fail + name: fail + namespace: argoci-workflows + selfLink: /apis/batch/v1/namespaces/argoci-workflows/jobs/fail +spec: + backoffLimit: 0 + completions: 1 + parallelism: 1 + template: + metadata: + creationTimestamp: null + labels: + job-name: fail + spec: + containers: + - command: + - sh + - -c + - exit 1 + image: alpine:latest + imagePullPolicy: Always + name: fail + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Never + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 diff --git a/docs/operator-manual/health.md b/docs/operator-manual/health.md index c034157e7f22ef..ffce008b1f1928 100644 --- a/docs/operator-manual/health.md +++ b/docs/operator-manual/health.md @@ -229,3 +229,16 @@ App (healthy) └── CustomResource (healthy) <- This resource's health check needs to be fixed to mark the App as unhealthy └── CustomChildResource (unhealthy) ``` +## Ignoring Child Resource Health Check in Applications + +To ignore the health check of an immediate child resource within an Application, set the annotation `argocd.argoproj.io/ignore-healthcheck` to `true`. For example: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + argocd.argoproj.io/ignore-healthcheck: "true" +``` + +By doing this, the health status of the Deployment will not affect the health of its parent Application. \ No newline at end of file diff --git a/docs/operator-manual/metrics.md b/docs/operator-manual/metrics.md index 02a490998307a9..36d3d4dd03b375 100644 --- a/docs/operator-manual/metrics.md +++ b/docs/operator-manual/metrics.md @@ -11,6 +11,7 @@ Metrics about applications. Scraped at the `argocd-metrics:8082/metrics` endpoin | `argocd_app_condition` | gauge | Report Applications conditions. It contains the conditions currently present in the application status. | | `argocd_app_k8s_request_total` | counter | Number of Kubernetes requests executed during application reconciliation | | `argocd_app_labels` | gauge | Argo Application labels converted to Prometheus labels. Disabled by default. See section below about how to enable it. | +| `argocd_app_orphaned_resources_count` | gauge | Number of orphaned resources per application. | | `argocd_app_reconcile` | histogram | Application reconciliation performance in seconds. | | `argocd_app_sync_total` | counter | Counter for application sync history | | `argocd_cluster_api_resource_objects` | gauge | Number of k8s resource objects in the cache. | diff --git a/pkg/apis/application/v1alpha1/types_test.go b/pkg/apis/application/v1alpha1/types_test.go index 83f7028484395f..1d29219d05d9ef 100644 --- a/pkg/apis/application/v1alpha1/types_test.go +++ b/pkg/apis/application/v1alpha1/types_test.go @@ -107,12 +107,16 @@ func TestAppProject_IsNegatedSourcePermitted(t *testing.T) { } func TestAppProject_IsDestinationPermitted(t *testing.T) { + t.Parallel() + testData := []struct { + name string projDest []ApplicationDestination appDest ApplicationDestination isPermitted bool }{ { + name: "server an namespace match", projDest: []ApplicationDestination{{ Server: "https://kubernetes.default.svc", Namespace: "default", }}, @@ -120,6 +124,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: true, }, { + name: "namespace does not match", projDest: []ApplicationDestination{{ Server: "https://kubernetes.default.svc", Namespace: "default", }}, @@ -127,6 +132,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: false, }, { + name: "server does not match", projDest: []ApplicationDestination{{ Server: "https://my-cluster", Namespace: "default", }}, @@ -134,6 +140,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: false, }, { + name: "wildcard namespace", projDest: []ApplicationDestination{{ Server: "https://kubernetes.default.svc", Namespace: "*", }}, @@ -141,6 +148,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: true, }, { + name: "wildcard server", projDest: []ApplicationDestination{{ Server: "https://*.default.svc", Namespace: "default", }}, @@ -148,6 +156,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: true, }, { + name: "wildcard server and namespace", projDest: []ApplicationDestination{{ Server: "https://team1-*", Namespace: "default", }}, @@ -155,6 +164,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: false, }, { + name: "wildcard namespace with prefix", projDest: []ApplicationDestination{{ Server: "https://kubernetes.default.svc", Namespace: "test-*", }}, @@ -162,6 +172,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: true, }, { + name: "wildcard namespace without prefix", projDest: []ApplicationDestination{{ Server: "https://kubernetes.default.svc", Namespace: "test-*", }}, @@ -169,6 +180,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: false, }, { + name: "wildcard server and namespace", projDest: []ApplicationDestination{{ Server: "*", Namespace: "*", }}, @@ -176,6 +188,7 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: true, }, { + name: "wildcard server and namespace with name", projDest: []ApplicationDestination{{ Server: "", Namespace: "*", Name: "test", }}, @@ -183,24 +196,51 @@ func TestAppProject_IsDestinationPermitted(t *testing.T) { isPermitted: true, }, { + name: "wildcard server and namespace with different name", projDest: []ApplicationDestination{{ Server: "", Namespace: "*", Name: "test2", }}, appDest: ApplicationDestination{Name: "test", Namespace: "test"}, isPermitted: false, }, + /** + - name: host-cluster + namespace: '!{kube-system,argocd}' + server: 'https://kubernetes.default.svc' + - name: destination-cluster-01 + namespace: '*' + server: 'https://eks-cluster-endpoint.ap-southeast-1.eks.amazonaws.com' + + destination: + server: https://eks-cluster-endpoint.ap-southeast-1.eks.amazonaws.com + namespace: karpenter + */ + { + name: "negated namespace with multiple values", + projDest: []ApplicationDestination{ + {Name: "host-cluster", Server: "https://kubernetes.default.svc", Namespace: "!{kube-system,argocd}"}, + {Name: "destination-cluster-01", Server: "https://eks-cluster-endpoint.ap-southeast-1.eks.amazonaws.com", Namespace: "*"}, + }, + appDest: ApplicationDestination{Server: "https://eks-cluster-endpoint.ap-southeast-1.eks.amazonaws.com", Namespace: "kube-system"}, + isPermitted: true, + }, } for _, data := range testData { - proj := AppProject{ - Spec: AppProjectSpec{ - Destinations: data.projDest, - }, - } - permitted, _ := proj.IsDestinationPermitted(data.appDest, func(project string) ([]*Cluster, error) { - return []*Cluster{}, nil + data := data + t.Run(data.name, func(t *testing.T) { + t.Parallel() + + proj := AppProject{ + Spec: AppProjectSpec{ + Destinations: data.projDest, + }, + } + permitted, _ := proj.IsDestinationPermitted(data.appDest, func(project string) ([]*Cluster, error) { + return []*Cluster{}, nil + }) + assert.Equal(t, data.isPermitted, permitted) }) - assert.Equal(t, data.isPermitted, permitted) } } diff --git a/util/app/discovery/discovery.go b/util/app/discovery/discovery.go index 580e4cd06935db..9f477e3a35a237 100644 --- a/util/app/discovery/discovery.go +++ b/util/app/discovery/discovery.go @@ -170,6 +170,7 @@ func cmpSupports(ctx context.Context, pluginSockFilePath, appPath, repoPath, fil cfg, err := cmpClient.CheckPluginConfiguration(ctx, &empty.Empty{}) if err != nil { log.Errorf("error checking plugin configuration %s, %v", fileName, err) + io.Close(conn) return nil, nil, false } @@ -178,6 +179,7 @@ func cmpSupports(ctx context.Context, pluginSockFilePath, appPath, repoPath, fil if namedPlugin { return conn, cmpClient, true } + io.Close(conn) return nil, nil, false }