diff --git a/api/v1/checks.go b/api/v1/checks.go index 5bc57efdd..05570b352 100644 --- a/api/v1/checks.go +++ b/api/v1/checks.go @@ -5,12 +5,15 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/flanksource/canary-checker/api/external" + "github.com/flanksource/commons/duration" "github.com/flanksource/duty" "github.com/flanksource/duty/connection" "github.com/flanksource/duty/models" "github.com/flanksource/duty/types" + "github.com/samber/lo" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -785,6 +788,63 @@ type KubernetesResourceChecks struct { CanarySpec `yaml:",inline" json:",inline"` } +type KubernetesResourceCheckWaitFor struct { + // Expr is a cel expression that determines whether all the resources + // are in their desired state before running checks on them. + // Default: `dyn(resources).all(r, k8s.isHealthy(r))` + Expr string `json:"expr,omitempty"` + + // Disable waiting for resources to get to their desired state. + Disable bool `json:"disable,omitempty"` + + // Timeout to wait for all static & non-static resources to be ready. + // Default: 10m + Timeout string `json:"timeout,omitempty"` + + // Interval to check if all static & non-static resources are ready. + // Default: 30s + Interval string `json:"interval,omitempty"` + + parsedTimeout *time.Duration + parsedInterval *time.Duration +} + +func (t *KubernetesResourceCheckWaitFor) GetTimeout() (time.Duration, error) { + if t.parsedTimeout != nil { + return *t.parsedTimeout, nil + } + + if t.Timeout == "" { + return time.Duration(0), nil + } + + tt, err := duration.ParseDuration(t.Timeout) + if err != nil { + return time.Duration(0), err + } + t.parsedTimeout = lo.ToPtr(time.Duration(tt)) + + return *t.parsedTimeout, nil +} + +func (t *KubernetesResourceCheckWaitFor) GetInterval() (time.Duration, error) { + if t.parsedInterval != nil { + return *t.parsedInterval, nil + } + + if t.Interval == "" { + return time.Duration(0), nil + } + + tt, err := duration.ParseDuration(t.Interval) + if err != nil { + return time.Duration(0), err + } + t.parsedInterval = lo.ToPtr(time.Duration(tt)) + + return *t.parsedInterval, nil +} + type KubernetesResourceCheck struct { Description `yaml:",inline" json:",inline"` Templatable `yaml:",inline" json:",inline"` @@ -808,8 +868,11 @@ type KubernetesResourceCheck struct { // Kubeconfig is the kubeconfig or the path to the kubeconfig file. Kubeconfig *types.EnvVar `yaml:"kubeconfig,omitempty" json:"kubeconfig,omitempty"` - WaitForReady bool `json:"waitForReady,omitempty"` - Timeout string `json:"timeout,omitempty"` + WaitFor KubernetesResourceCheckWaitFor `json:"waitFor,omitempty"` +} + +func (c KubernetesResourceCheck) TotalResources() int { + return len(c.Resources) + len(c.StaticResources) } func (c KubernetesResourceCheck) GetType() string { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index e7f574c7c..9d09afa5f 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -29,6 +29,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + timex "time" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -2290,6 +2291,7 @@ func (in *KubernetesResourceCheck) DeepCopyInto(out *KubernetesResourceCheck) { *out = new(types.EnvVar) (*in).DeepCopyInto(*out) } + in.WaitFor.DeepCopyInto(&out.WaitFor) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesResourceCheck. @@ -2302,6 +2304,31 @@ func (in *KubernetesResourceCheck) DeepCopy() *KubernetesResourceCheck { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesResourceCheckWaitFor) DeepCopyInto(out *KubernetesResourceCheckWaitFor) { + *out = *in + if in.parsedTimeout != nil { + in, out := &in.parsedTimeout, &out.parsedTimeout + *out = new(timex.Duration) + **out = **in + } + if in.parsedInterval != nil { + in, out := &in.parsedInterval, &out.parsedInterval + *out = new(timex.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesResourceCheckWaitFor. +func (in *KubernetesResourceCheckWaitFor) DeepCopy() *KubernetesResourceCheckWaitFor { + if in == nil { + return nil + } + out := new(KubernetesResourceCheckWaitFor) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubernetesResourceChecks) DeepCopyInto(out *KubernetesResourceChecks) { *out = *in diff --git a/checks/kubernetes_resource.go b/checks/kubernetes_resource.go index a50969b54..da34153a7 100644 --- a/checks/kubernetes_resource.go +++ b/checks/kubernetes_resource.go @@ -1,15 +1,15 @@ package checks import ( + gocontext "context" "fmt" + "strconv" "strings" "time" "github.com/flanksource/gomplate/v3" - "github.com/flanksource/is-healthy/pkg/health" - "golang.org/x/sync/errgroup" + "github.com/sethvargo/go-retry" - "github.com/flanksource/commons/duration" "github.com/flanksource/commons/logger" "github.com/flanksource/commons/utils" "github.com/flanksource/duty/types" @@ -23,9 +23,9 @@ const ( // maximum number of static & non static resources a canary can have defaultMaxResourcesAllowed = 10 - // resourceWaitTimeout is the default timeout to wait for all resources - // to be ready. Timeout on the spec will take precedence over this. - resourceWaitTimeout = time.Minute * 10 + resourceWaitTimeoutDefault = time.Minute * 10 + resourceWaitIntervalDefault = time.Second * 5 + waitForExprDefault = `dyn(resources).all(r, k8s.isHealthy(r))` annotationkey = "flanksource.canary-checker/kubernetes-resource-canary" ) @@ -71,27 +71,31 @@ func (c *KubernetesResourceChecker) applyKubeconfig(ctx *context.Context, kubeCo return ctx, nil } +func (c *KubernetesResourceChecker) validate(ctx *context.Context, check v1.KubernetesResourceCheck) error { + if _, err := check.WaitFor.GetTimeout(); err != nil { + return fmt.Errorf("failed to parse wait for timeout(%s): %w", check.WaitFor.Timeout, err) + } + + if _, err := check.WaitFor.GetTimeout(); err != nil { + return fmt.Errorf("failed to parse wait for timeout(%s): %w", check.WaitFor.Timeout, err) + } + + maxResourcesAllowed := ctx.Properties().Int("checks.kubernetesResource.maxResources", defaultMaxResourcesAllowed) + if check.TotalResources() > maxResourcesAllowed { + return fmt.Errorf("too many resources (%d). only %d allowed", check.TotalResources(), maxResourcesAllowed) + } + + return nil +} + func (c *KubernetesResourceChecker) Check(ctx *context.Context, check v1.KubernetesResourceCheck) pkg.Results { result := pkg.Success(check, ctx.Canary) var err error var results pkg.Results results = append(results, result) - if check.Timeout != "" { - if d, err := duration.ParseDuration(check.Timeout); err != nil { - return results.Failf("failed to parse timeout: %v", err) - } else { - ctx2, cancel := ctx.WithTimeout(time.Duration(d)) - defer cancel() - - ctx = ctx.WithDutyContext(ctx2) - } - } - - totalResources := len(check.StaticResources) + len(check.Resources) - maxResourcesAllowed := ctx.Properties().Int("checks.kubernetesResource.maxResources", defaultMaxResourcesAllowed) - if totalResources > maxResourcesAllowed { - return results.Failf("too many resources (%d). only %d allowed", totalResources, maxResourcesAllowed) + if err := c.validate(ctx, check); err != nil { + return results.Failf("validation: %v", err) } if check.Kubeconfig != nil { @@ -127,30 +131,8 @@ func (c *KubernetesResourceChecker) Check(ctx *context.Context, check v1.Kuberne }() } - if check.WaitForReady { - timeout := resourceWaitTimeout - if deadline, ok := ctx.Deadline(); ok { - timeout = time.Until(deadline) - } - - logger.Debugf("waiting for %s for %d resources to be ready.", timeout, totalResources) - - kClient := pkg.NewKubeClient(ctx.Kommons().GetRESTConfig) - errG, _ := errgroup.WithContext(ctx) - for _, r := range append(check.StaticResources, check.Resources...) { - r := r - errG.Go(func() error { - if status, err := kClient.WaitForResource(ctx, r.GetKind(), r.GetNamespace(), r.GetName()); err != nil { - return fmt.Errorf("error waiting for resource(%s/%s/%s) to be ready: %w", r.GetKind(), r.GetNamespace(), r.GetName(), err) - } else if status.Status != health.HealthStatusHealthy { - return fmt.Errorf("resource(%s/%s/%s) didn't become healthy. message (%s)", r.GetKind(), r.GetNamespace(), r.GetName(), status.Message) - } - - return nil - }) - } - - if err := errG.Wait(); err != nil { + if !check.WaitFor.Disable { + if err := c.evalWaitFor(ctx, check); err != nil { return results.Failf("%v", err) } } @@ -192,3 +174,50 @@ func (c *KubernetesResourceChecker) Check(ctx *context.Context, check v1.Kuberne return results } + +func (c *KubernetesResourceChecker) evalWaitFor(ctx *context.Context, check v1.KubernetesResourceCheck) error { + waitTimeout := resourceWaitTimeoutDefault + if wt, _ := check.WaitFor.GetTimeout(); wt > 0 { + waitTimeout = wt + } + + waitInterval := resourceWaitIntervalDefault + if wt, _ := check.WaitFor.GetInterval(); wt > 0 { + waitInterval = wt + } + + var attempts int + backoff := retry.WithMaxDuration(waitTimeout, retry.NewConstant(waitInterval)) + retryErr := retry.Do(ctx, backoff, func(_ctx gocontext.Context) error { + ctx = _ctx.(*context.Context) + attempts++ + ctx.Tracef("waiting for %d resources to be ready. (attempts: %d)", check.TotalResources(), attempts) + + var templateVar = map[string]any{} + kClient := pkg.NewKubeClient(ctx.Kommons().GetRESTConfig) + if response, err := kClient.FetchResources(ctx, append(check.StaticResources, check.Resources...)...); err != nil { + return fmt.Errorf("wait for evaluation. fetching resources: %w", err) + } else if len(response) != check.TotalResources() { + return fmt.Errorf("unxpected error. expected %d resources, got %d", check.TotalResources(), len(response)) + } else { + templateVar["resources"] = response + } + + waitForExpr := check.WaitFor.Expr + if waitForExpr == "" { + waitForExpr = waitForExprDefault + } + + if response, err := gomplate.RunTemplate(templateVar, gomplate.Template{Expression: waitForExpr}); err != nil { + return fmt.Errorf("wait for expression evaluation: %w", err) + } else if parsed, err := strconv.ParseBool(response); err != nil { + return fmt.Errorf("wait for expression (%q) didn't evaluate to a boolean", check.WaitFor.Expr) + } else if !parsed { + return retry.RetryableError(fmt.Errorf("not all resources are ready")) + } + + return nil + }) + + return retryErr +} diff --git a/checks/namespace.go b/checks/namespace.go index 5e2f300ba..ae3f4c182 100644 --- a/checks/namespace.go +++ b/checks/namespace.go @@ -48,6 +48,7 @@ func NewNamespaceChecker() *NamespaceChecker { // Run: Check every entry from config according to Checker interface // Returns check result and metrics func (c *NamespaceChecker) Run(ctx *context.Context) pkg.Results { + logger.Warnf("namespace check is deprecated. Please use the kubernetes resource check") var err error var results pkg.Results for _, conf := range ctx.Canary.Spec.Namespace { @@ -107,7 +108,6 @@ func (c *NamespaceChecker) getConditionTimes(ns *v1.Namespace, pod *v1.Pod) (tim func (c *NamespaceChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { check := extConfig.(canaryv1.NamespaceCheck) result := pkg.Success(check, ctx.Canary) - result.AppendMsgf("namespace check is deprecated. Please use the kubernetes resource check") var results pkg.Results results = append(results, result) if !c.lock.TryAcquire(1) { diff --git a/checks/pod.go b/checks/pod.go index 2374f1dfc..51c24a065 100644 --- a/checks/pod.go +++ b/checks/pod.go @@ -12,6 +12,7 @@ import ( gocontext "context" "github.com/flanksource/canary-checker/api/context" + "github.com/flanksource/commons/logger" "github.com/flanksource/canary-checker/api/external" networkingv1 "k8s.io/api/networking/v1" @@ -58,6 +59,8 @@ func NewPodChecker() *PodChecker { // Run: Check every entry from config according to Checker interface // Returns check result and metrics func (c *PodChecker) Run(ctx *context.Context) pkg.Results { + logger.Warnf("pod check is deprecated. Please use the kubernetes resource check") + var results pkg.Results if len(ctx.Canary.Spec.Pod) > 0 { if c.k8s == nil { @@ -141,7 +144,6 @@ func (c *PodChecker) Check(ctx *context.Context, extConfig external.Check) pkg.R } result := pkg.Success(podCheck, ctx.Canary) - result.AppendMsgf("pod check is deprecated. Please use the kubernetes resource check") var results pkg.Results results = append(results, result) startTimer := NewTimer() diff --git a/config/deploy/crd.yaml b/config/deploy/crd.yaml index c040be550..440da1ff4 100644 --- a/config/deploy/crd.yaml +++ b/config/deploy/crd.yaml @@ -5110,8 +5110,6 @@ spec: template: type: string type: object - timeout: - type: string transform: properties: expr: @@ -5126,8 +5124,28 @@ spec: transformDeleteStrategy: description: Transformed checks have a delete strategy on deletion they can either be marked healthy, unhealthy or left as is type: string - waitForReady: - type: boolean + waitFor: + properties: + disable: + description: Disable waiting for resources to get to their desired state. + type: boolean + expr: + description: |- + Expr is a cel expression that determines whether all the resources + are in their desired state before running checks on them. + Default: `dyn(resources).all(r, k8s.isHealthy(r))` + type: string + interval: + description: |- + Interval to check if all static & non-static resources are ready. + Deafult: 30s + type: string + timeout: + description: |- + Timeout to wait for all static & non-static resources to be ready. + Deafult: 10m + type: string + type: object required: - name - resources diff --git a/config/deploy/manifests.yaml b/config/deploy/manifests.yaml index 7db49a00b..d6b6867d2 100644 --- a/config/deploy/manifests.yaml +++ b/config/deploy/manifests.yaml @@ -5109,8 +5109,6 @@ spec: template: type: string type: object - timeout: - type: string transform: properties: expr: @@ -5125,8 +5123,28 @@ spec: transformDeleteStrategy: description: Transformed checks have a delete strategy on deletion they can either be marked healthy, unhealthy or left as is type: string - waitForReady: - type: boolean + waitFor: + properties: + disable: + description: Disable waiting for resources to get to their desired state. + type: boolean + expr: + description: |- + Expr is a cel expression that determines whether all the resources + are in their desired state before running checks on them. + Default: `dyn(resources).all(r, k8s.isHealthy(r))` + type: string + interval: + description: |- + Interval to check if all static & non-static resources are ready. + Deafult: 30s + type: string + timeout: + description: |- + Timeout to wait for all static & non-static resources to be ready. + Deafult: 10m + type: string + type: object required: - name - resources diff --git a/config/schemas/canary.schema.json b/config/schemas/canary.schema.json index e3936ff05..2772e5581 100644 --- a/config/schemas/canary.schema.json +++ b/config/schemas/canary.schema.json @@ -2323,11 +2323,8 @@ "kubeconfig": { "$ref": "#/$defs/EnvVar" }, - "waitForReady": { - "type": "boolean" - }, - "timeout": { - "type": "string" + "waitFor": { + "$ref": "#/$defs/KubernetesResourceCheckWaitFor" } }, "additionalProperties": false, @@ -2337,6 +2334,24 @@ "resources" ] }, + "KubernetesResourceCheckWaitFor": { + "properties": { + "expr": { + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "timeout": { + "type": "string" + }, + "interval": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, "KubernetesResourceChecks": { "properties": { "replicas": { diff --git a/config/schemas/component.schema.json b/config/schemas/component.schema.json index b9c8b9c5d..0b1d18a47 100644 --- a/config/schemas/component.schema.json +++ b/config/schemas/component.schema.json @@ -2577,11 +2577,8 @@ "kubeconfig": { "$ref": "#/$defs/EnvVar" }, - "waitForReady": { - "type": "boolean" - }, - "timeout": { - "type": "string" + "waitFor": { + "$ref": "#/$defs/KubernetesResourceCheckWaitFor" } }, "additionalProperties": false, @@ -2591,6 +2588,24 @@ "resources" ] }, + "KubernetesResourceCheckWaitFor": { + "properties": { + "expr": { + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "timeout": { + "type": "string" + }, + "interval": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, "KubernetesResourceChecks": { "properties": { "replicas": { diff --git a/config/schemas/topology.schema.json b/config/schemas/topology.schema.json index f4ab7d8c2..9bc8a933c 100644 --- a/config/schemas/topology.schema.json +++ b/config/schemas/topology.schema.json @@ -2547,11 +2547,8 @@ "kubeconfig": { "$ref": "#/$defs/EnvVar" }, - "waitForReady": { - "type": "boolean" - }, - "timeout": { - "type": "string" + "waitFor": { + "$ref": "#/$defs/KubernetesResourceCheckWaitFor" } }, "additionalProperties": false, @@ -2561,6 +2558,24 @@ "resources" ] }, + "KubernetesResourceCheckWaitFor": { + "properties": { + "expr": { + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "timeout": { + "type": "string" + }, + "interval": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, "KubernetesResourceChecks": { "properties": { "replicas": { diff --git a/fixtures/k8s/kubernetes_resource_ingress_fail.yaml b/fixtures/k8s/kubernetes_resource_ingress_fail.yaml index 86654b8bc..8feeb3029 100644 --- a/fixtures/k8s/kubernetes_resource_ingress_fail.yaml +++ b/fixtures/k8s/kubernetes_resource_ingress_fail.yaml @@ -12,8 +12,6 @@ spec: - name: ingress-accessibility-check-2 namespace: default description: "deploy httpbin & check that it's accessible via ingress" - waitForReady: true - timeout: 10m staticResources: - apiVersion: networking.k8s.io/v1 kind: Ingress diff --git a/fixtures/k8s/kubernetes_resource_ingress_pass.yaml b/fixtures/k8s/kubernetes_resource_ingress_pass.yaml index 85a51f34c..969724dd7 100644 --- a/fixtures/k8s/kubernetes_resource_ingress_pass.yaml +++ b/fixtures/k8s/kubernetes_resource_ingress_pass.yaml @@ -12,8 +12,9 @@ spec: - name: ingress-accessibility-check namespace: default description: "deploy httpbin & check that it's accessible via ingress" - waitForReady: true - timeout: 10m + waitFor: + interval: 2s + timeout: 5m staticResources: - apiVersion: v1 kind: Namespace diff --git a/fixtures/k8s/kubernetes_resource_service_pass.yaml b/fixtures/k8s/kubernetes_resource_service_pass.yaml index 47286529a..629e4ff21 100644 --- a/fixtures/k8s/kubernetes_resource_service_pass.yaml +++ b/fixtures/k8s/kubernetes_resource_service_pass.yaml @@ -12,8 +12,6 @@ spec: - name: service accessibility test namespace: default description: "deploy httpbin & check that it's accessible via its service" - waitForReady: true - timeout: 10m resources: - apiVersion: v1 kind: Pod diff --git a/go.mod b/go.mod index 14b962971..d737b1a4a 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/flanksource/commons v1.22.1 github.com/flanksource/duty v1.0.408 github.com/flanksource/gomplate/v3 v3.24.2 - github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 + github.com/flanksource/is-healthy v1.0.2 github.com/flanksource/kommons v0.31.4 github.com/friendsofgo/errors v0.9.2 github.com/go-git/go-git/v5 v5.11.0 @@ -238,6 +238,7 @@ require ( github.com/rodaine/table v1.1.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect + github.com/sethvargo/go-retry v0.2.4 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect diff --git a/go.sum b/go.sum index 206f643ec..3abb75386 100644 --- a/go.sum +++ b/go.sum @@ -854,8 +854,8 @@ github.com/flanksource/gomplate/v3 v3.20.4/go.mod h1:27BNWhzzSjDed1z8YShO6W+z6G9 github.com/flanksource/gomplate/v3 v3.24.2 h1:WZSriw1MaBhzrDV1IOP9eNsupIPxIHy0yTaMOVhCvsk= github.com/flanksource/gomplate/v3 v3.24.2/go.mod h1:94BxYobZqouGdVezuz6LNto5C+yLMG0LnNnM9CUPyoo= github.com/flanksource/is-healthy v0.0.0-20230705092916-3b4cf510c5fc/go.mod h1:4pQhmF+TnVqJroQKY8wSnSp+T18oLson6YQ2M0qPHfQ= -github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 h1:s6jf6P1pRfdvksVFjIXFRfnimvEYUR0/Mmla1EIjiRM= -github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7/go.mod h1:BH5gh9JyEAuuWVP6Q5y9h43VozS0RfKyjNpM9L4v4hw= +github.com/flanksource/is-healthy v1.0.2 h1:QJvtwIFoz4k8D2atHOciaXCsaFepUXz8laP4c0RBc4E= +github.com/flanksource/is-healthy v1.0.2/go.mod h1:cFejm0MapnJzgeoG3iizMv+tCIPthe0XqO+3nrhM79c= github.com/flanksource/kommons v0.31.4 h1:zksAgYjZuwPgS8XTejDIWEYB0nPSU1i3Jxcavm/vovI= github.com/flanksource/kommons v0.31.4/go.mod h1:70BPMzjTvejsqRyVyAm/ZCeZ176toCvauaZjU03svnE= github.com/flanksource/kubectl-neat v1.0.4 h1:t5/9CqgE84oEtB0KitgJ2+WIeLfD+RhXSxYrqb4X8yI= @@ -1486,6 +1486,8 @@ github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec= +github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw= github.com/sevennt/echo-pprof v0.1.1-0.20220616082843-66a461746b5f h1:mx2Z/21bNtP+jXvuB9qHJbihaIhT3SsqL+qJUqbwoGg= github.com/sevennt/echo-pprof v0.1.1-0.20220616082843-66a461746b5f/go.mod h1:QPpsWWcK1TiLQ8uaSnmKJamNb2HryXeBxZapurHcGn0= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/hack/generate-schemas/go.mod b/hack/generate-schemas/go.mod index c08d9dcc2..96e03e02d 100644 --- a/hack/generate-schemas/go.mod +++ b/hack/generate-schemas/go.mod @@ -51,7 +51,7 @@ require ( github.com/exaring/otelpgx v0.5.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flanksource/duty v1.0.408 // indirect - github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 // indirect + github.com/flanksource/is-healthy v1.0.2 // indirect github.com/flanksource/kommons v0.31.4 // indirect github.com/flanksource/kubectl-neat v1.0.4 // indirect github.com/flanksource/postq v1.0.0 // indirect @@ -187,7 +187,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/postgres v1.5.4 // indirect - gorm.io/gorm v1.25.5 // indirect + gorm.io/gorm v1.25.9 // indirect k8s.io/api v0.28.8 // indirect k8s.io/apiextensions-apiserver v0.28.3 // indirect k8s.io/apimachinery v0.28.8 // indirect diff --git a/hack/generate-schemas/go.sum b/hack/generate-schemas/go.sum index 5017b8630..a23eecdae 100644 --- a/hack/generate-schemas/go.sum +++ b/hack/generate-schemas/go.sum @@ -736,8 +736,8 @@ github.com/flanksource/gomplate/v3 v3.20.4/go.mod h1:27BNWhzzSjDed1z8YShO6W+z6G9 github.com/flanksource/gomplate/v3 v3.24.2 h1:WZSriw1MaBhzrDV1IOP9eNsupIPxIHy0yTaMOVhCvsk= github.com/flanksource/gomplate/v3 v3.24.2/go.mod h1:94BxYobZqouGdVezuz6LNto5C+yLMG0LnNnM9CUPyoo= github.com/flanksource/is-healthy v0.0.0-20230705092916-3b4cf510c5fc/go.mod h1:4pQhmF+TnVqJroQKY8wSnSp+T18oLson6YQ2M0qPHfQ= -github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7 h1:s6jf6P1pRfdvksVFjIXFRfnimvEYUR0/Mmla1EIjiRM= -github.com/flanksource/is-healthy v0.0.0-20231003215854-76c51e3a3ff7/go.mod h1:BH5gh9JyEAuuWVP6Q5y9h43VozS0RfKyjNpM9L4v4hw= +github.com/flanksource/is-healthy v1.0.2 h1:QJvtwIFoz4k8D2atHOciaXCsaFepUXz8laP4c0RBc4E= +github.com/flanksource/is-healthy v1.0.2/go.mod h1:cFejm0MapnJzgeoG3iizMv+tCIPthe0XqO+3nrhM79c= github.com/flanksource/kommons v0.31.4 h1:zksAgYjZuwPgS8XTejDIWEYB0nPSU1i3Jxcavm/vovI= github.com/flanksource/kommons v0.31.4/go.mod h1:70BPMzjTvejsqRyVyAm/ZCeZ176toCvauaZjU03svnE= github.com/flanksource/kubectl-neat v1.0.4 h1:t5/9CqgE84oEtB0KitgJ2+WIeLfD+RhXSxYrqb4X8yI= @@ -1979,8 +1979,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= -gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= -gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= +gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/api.go b/pkg/api.go index 17e6089f1..69efbd3c7 100644 --- a/pkg/api.go +++ b/pkg/api.go @@ -366,15 +366,6 @@ func (result CheckResult) String() string { return fmt.Sprintf("%s duration=%d %s %s", console.Redf("FAIL"), result.Duration, result.Message, result.Error) } -func (result *CheckResult) AppendMsgf(msg string, args ...any) { - if result.Message == "" { - result.Message = fmt.Sprintf(msg, args...) - return - } - - result.Message += "\n" + fmt.Sprintf(msg, args...) -} - type GenericCheck struct { v1.Description `yaml:",inline" json:",inline"` Type string diff --git a/pkg/kube.go b/pkg/kube.go index 518193f31..ba0fa7e5e 100644 --- a/pkg/kube.go +++ b/pkg/kube.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "golang.org/x/sync/errgroup" "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes/fake" @@ -32,12 +33,14 @@ import ( "github.com/flanksource/commons/files" clogger "github.com/flanksource/commons/logger" "github.com/flanksource/is-healthy/pkg/health" + "github.com/flanksource/is-healthy/pkg/lua" "github.com/flanksource/kommons" "github.com/henvic/httpretty" "github.com/pkg/errors" "gopkg.in/flanksource/yaml.v3" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -157,6 +160,34 @@ func NewKubeClient(restConfigFn func() (*rest.Config, error)) *kubeClient { return &kubeClient{GetRESTConfig: restConfigFn} } +func (c *kubeClient) FetchResources(ctx context.Context, resources ...unstructured.Unstructured) ([]unstructured.Unstructured, error) { + if len(resources) == 0 { + return nil, nil + } + + eg, ctx := errgroup.WithContext(ctx) + var items []unstructured.Unstructured + for i := range resources { + resource := resources[i] + client, err := c.GetClientByKind(resource.GetKind()) + if err != nil { + return nil, err + } + + eg.Go(func() error { + item, err := client.Namespace(resource.GetNamespace()).Get(ctx, resource.GetName(), metav1.GetOptions{}) + if err != nil { + return err + } + + items = append(items, *item) + return nil + }) + } + + return items, eg.Wait() +} + func (c *kubeClient) WaitForResource(ctx context.Context, kind, namespace, name string) (*health.HealthStatus, error) { client, err := c.GetClientByKind(kind) if err != nil { @@ -166,10 +197,10 @@ func (c *kubeClient) WaitForResource(ctx context.Context, kind, namespace, name for { item, err := client.Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return nil, fmt.Errorf("error getting item (kind=%s, namespace=%s, name=%s)", kind, namespace, name) + return nil, fmt.Errorf("error getting item (kind=%s, namespace=%s, name=%s): %w", kind, namespace, name, err) } - status, err := health.GetResourceHealth(item, nil) + status, err := health.GetResourceHealth(item, lua.ResourceHealthOverrides{}) if err != nil { return nil, fmt.Errorf("error getting resource health: %w", err) }