diff --git a/go.mod b/go.mod
index 82a0b1ab00..88f908dc5d 100644
--- a/go.mod
+++ b/go.mod
@@ -39,7 +39,6 @@ require (
github.com/opencontainers/image-spec v1.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.18.0
- github.com/prometheus/prometheus v0.47.2
github.com/pterm/pterm v0.12.79
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
github.com/sigstore/cosign/v2 v2.2.3
@@ -78,12 +77,10 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/gorilla/handlers v1.5.1 // indirect
- github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
- github.com/jpillora/backoff v1.0.0 // indirect
- github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
+ github.com/onsi/gomega v1.32.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
github.com/redis/go-redis/v9 v9.3.0 // indirect
@@ -433,7 +430,7 @@ require (
github.com/pkg/profile v1.7.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
- github.com/prometheus/common v0.45.0
+ github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf // indirect
github.com/rakyll/hey v0.1.4 // indirect
@@ -531,7 +528,7 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
- gopkg.in/yaml.v2 v2.4.0
+ gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/gorm v1.25.5 // indirect
k8s.io/apiextensions-apiserver v0.30.0 // indirect
diff --git a/go.sum b/go.sum
index d301432f4a..2c9de98304 100644
--- a/go.sum
+++ b/go.sum
@@ -1022,8 +1022,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
-github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww=
-github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@@ -1144,8 +1142,6 @@ github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -1356,8 +1352,6 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
@@ -1475,8 +1469,6 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/prometheus/prometheus v0.47.2 h1:jWcnuQHz1o1Wu3MZ6nMJDuTI0kU5yJp9pkxh8XEkNvI=
-github.com/prometheus/prometheus v0.47.2/go.mod h1:J/bmOSjgH7lFxz2gZhrWEZs2i64vMS+HIuZfmYNhJ/M=
github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf h1:014O62zIzQwvoD7Ekj3ePDF5bv9Xxy0w6AZk0qYbjUk=
github.com/protocolbuffers/txtpbfmt v0.0.0-20231025115547-084445ff1adf/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
diff --git a/site/src/content/docs/commands/zarf_dev_deploy.md b/site/src/content/docs/commands/zarf_dev_deploy.md
index 29293c1d9a..41ee7f0b85 100644
--- a/site/src/content/docs/commands/zarf_dev_deploy.md
+++ b/site/src/content/docs/commands/zarf_dev_deploy.md
@@ -31,7 +31,7 @@ zarf dev deploy [flags]
--registry-override stringToString Specify a map of domains to override on package create when pulling images (e.g. --registry-override docker.io=dockerio-reg.enterprise.intranet) (default [])
--retries int Number of retries to perform for Zarf deploy operations like git/image pushes or Helm installs (default 3)
--skip-webhooks [alpha] Skip waiting for external webhooks to execute as each package component is deployed
- --timeout duration Timeout for Helm operations such as installs and rollbacks (default 15m0s)
+ --timeout duration Timeout for health checks and Helm operations such as installs and rollbacks (default 15m0s)
```
### Options inherited from parent commands
diff --git a/site/src/content/docs/commands/zarf_init.md b/site/src/content/docs/commands/zarf_init.md
index 8fc88244c3..5702caa72b 100644
--- a/site/src/content/docs/commands/zarf_init.md
+++ b/site/src/content/docs/commands/zarf_init.md
@@ -78,7 +78,7 @@ $ zarf init --artifact-push-password={PASSWORD} --artifact-push-username={USERNA
--set stringToString Specify deployment variables to set on the command line (KEY=value) (default [])
--skip-webhooks [alpha] Skip waiting for external webhooks to execute as each package component is deployed
--storage-class string Specify the storage class to use for the registry and git server. E.g. --storage-class=standard
- --timeout duration Timeout for Helm operations such as installs and rollbacks (default 15m0s)
+ --timeout duration Timeout for health checks and Helm operations such as installs and rollbacks (default 15m0s)
```
### Options inherited from parent commands
diff --git a/site/src/content/docs/commands/zarf_package_deploy.md b/site/src/content/docs/commands/zarf_package_deploy.md
index 07f2fa46de..d89b0f1bbc 100644
--- a/site/src/content/docs/commands/zarf_package_deploy.md
+++ b/site/src/content/docs/commands/zarf_package_deploy.md
@@ -30,7 +30,7 @@ zarf package deploy [ PACKAGE_SOURCE ] [flags]
--set stringToString Specify deployment variables to set on the command line (KEY=value) (default [])
--shasum string Shasum of the package to deploy. Required if deploying a remote package and "--insecure" is not provided
--skip-webhooks [alpha] Skip waiting for external webhooks to execute as each package component is deployed
- --timeout duration Timeout for Helm operations such as installs and rollbacks (default 15m0s)
+ --timeout duration Timeout for health checks and Helm operations such as installs and rollbacks (default 15m0s)
```
### Options inherited from parent commands
diff --git a/site/src/content/docs/ref/components.mdx b/site/src/content/docs/ref/components.mdx
index 50be327f1b..419d43bc6d 100644
--- a/site/src/content/docs/ref/components.mdx
+++ b/site/src/content/docs/ref/components.mdx
@@ -267,6 +267,24 @@ When merging components together Zarf will adopt the following strategies depend
+### Health Checks
+
+
+
+Health checks wait until the specified resources are fully reconciled, meaning that their desired and current states match. Internally, [kstatus](https://github.com/kubernetes-sigs/cli-utils/blob/master/pkg/kstatus/README.md#kstatus) is used to assess when reconciliation is complete. Health checks supports all Kubernetes resources that implement the [status](https://kubernetes.io/docs/concepts/overview/working-with-objects/#object-spec-and-status) field, including custom resource definitions. If the status field is not implemented on a resource, it will automatically pass the health check.
+
+```yaml
+ healthChecks:
+ - name: my-pod
+ namespace: my-namespace
+ apiVersion: v1
+ kind: Pod
+ - name: my-stateful-set
+ namespace: my-namespace
+ apiVersion: apps/v1
+ kind: StatefulSet
+```
+
## Deploying Components
When deploying a Zarf package, components are deployed in the order they are defined in the `zarf.yaml`.
diff --git a/src/api/v1alpha1/component.go b/src/api/v1alpha1/component.go
index 9739de8644..9827410b43 100644
--- a/src/api/v1alpha1/component.go
+++ b/src/api/v1alpha1/component.go
@@ -61,6 +61,21 @@ type ZarfComponent struct {
// Custom commands to run at various stages of a package lifecycle.
Actions ZarfComponentActions `json:"actions,omitempty"`
+
+ // List of resources to health check after deployment
+ HealthChecks []NamespacedObjectKindReference `json:"healthChecks,omitempty"`
+}
+
+// NamespacedObjectKindReference is a reference to a specific resource in a namespace using its kind and API version.
+type NamespacedObjectKindReference struct {
+ // API Version of the resource
+ APIVersion string `json:"apiVersion"`
+ // Kind of the resource
+ Kind string `json:"kind"`
+ // Namespace of the resource
+ Namespace string `json:"namespace"`
+ // Name of the resource
+ Name string `json:"name"`
}
// RequiresCluster returns if the component requires a cluster connection to deploy.
@@ -70,8 +85,9 @@ func (c ZarfComponent) RequiresCluster() bool {
hasManifests := len(c.Manifests) > 0
hasRepos := len(c.Repos) > 0
hasDataInjections := len(c.DataInjections) > 0
+ hasHealthChecks := len(c.HealthChecks) > 0
- if hasImages || hasCharts || hasManifests || hasRepos || hasDataInjections {
+ if hasImages || hasCharts || hasManifests || hasRepos || hasDataInjections || hasHealthChecks {
return true
}
diff --git a/src/api/v1beta1/component.go b/src/api/v1beta1/component.go
index 2d27777c54..aa8ec9bff4 100644
--- a/src/api/v1beta1/component.go
+++ b/src/api/v1beta1/component.go
@@ -49,6 +49,21 @@ type ZarfComponent struct {
// Custom commands to run at various stages of a package lifecycle.
Actions ZarfComponentActions `json:"actions,omitempty"`
+
+ // List of resources to health check after deployment
+ HealthChecks []NamespacedObjectKindReference `json:"healthChecks,omitempty"`
+}
+
+// NamespacedObjectKindReference is a reference to a specific resource in a namespace using its kind and API version.
+type NamespacedObjectKindReference struct {
+ // API Version of the resource
+ APIVersion string `json:"apiVersion"`
+ // Kind of the resource
+ Kind string `json:"kind"`
+ // Namespace of the resource
+ Namespace string `json:"namespace"`
+ // Name of the resource
+ Name string `json:"name"`
}
// RequiresCluster returns if the component requires a cluster connection to deploy.
diff --git a/src/cmd/common/table.go b/src/cmd/common/table.go
new file mode 100644
index 0000000000..ee5f9ee190
--- /dev/null
+++ b/src/cmd/common/table.go
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
+
+package common
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/defenseunicorns/pkg/helpers/v2"
+ "github.com/fatih/color"
+
+ "github.com/zarf-dev/zarf/src/pkg/lint"
+ "github.com/zarf-dev/zarf/src/pkg/message"
+)
+
+// PrintFindings prints the findings in the LintError as a table.
+func PrintFindings(lintErr *lint.LintError) {
+ mapOfFindingsByPath := lint.GroupFindingsByPath(lintErr.Findings, lintErr.PackageName)
+ for _, findings := range mapOfFindingsByPath {
+ lintData := [][]string{}
+ for _, finding := range findings {
+ sevColor := color.FgWhite
+ switch finding.Severity {
+ case lint.SevErr:
+ sevColor = color.FgRed
+ case lint.SevWarn:
+ sevColor = color.FgYellow
+ }
+
+ lintData = append(lintData, []string{
+ colorWrap(string(finding.Severity), sevColor),
+ colorWrap(finding.YqPath, color.FgCyan),
+ finding.ItemizedDescription(),
+ })
+ }
+ var packagePathFromUser string
+ if helpers.IsOCIURL(findings[0].PackagePathOverride) {
+ packagePathFromUser = findings[0].PackagePathOverride
+ } else {
+ packagePathFromUser = filepath.Join(lintErr.BaseDir, findings[0].PackagePathOverride)
+ }
+ message.Notef("Linting package %q at %s", findings[0].PackageNameOverride, packagePathFromUser)
+ message.Table([]string{"Type", "Path", "Message"}, lintData)
+ }
+}
+
+func colorWrap(str string, attr color.Attribute) string {
+ if !message.ColorEnabled() || str == "" {
+ return str
+ }
+ return fmt.Sprintf("\x1b[%dm%s\x1b[0m", attr, str)
+}
diff --git a/src/cmd/dev.go b/src/cmd/dev.go
index 322b82fb84..7077f5dccd 100644
--- a/src/cmd/dev.go
+++ b/src/cmd/dev.go
@@ -59,7 +59,12 @@ var devDeployCmd = &cobra.Command{
}
defer pkgClient.ClearTempPaths()
- if err := pkgClient.DevDeploy(cmd.Context()); err != nil {
+ err = pkgClient.DevDeploy(cmd.Context())
+ var lintErr *lint.LintError
+ if errors.As(err, &lintErr) {
+ common.PrintFindings(lintErr)
+ }
+ if err != nil {
return fmt.Errorf("failed to dev deploy: %w", err)
}
return nil
@@ -235,7 +240,12 @@ var devFindImagesCmd = &cobra.Command{
}
defer pkgClient.ClearTempPaths()
- if _, err := pkgClient.FindImages(cmd.Context()); err != nil {
+ _, err = pkgClient.FindImages(cmd.Context())
+ var lintErr *lint.LintError
+ if errors.As(err, &lintErr) {
+ common.PrintFindings(lintErr)
+ }
+ if err != nil {
return fmt.Errorf("unable to find images: %w", err)
}
return nil
@@ -282,7 +292,19 @@ var devLintCmd = &cobra.Command{
}
defer pkgClient.ClearTempPaths()
- return lint.Validate(cmd.Context(), pkgConfig.CreateOpts)
+ err = lint.Validate(cmd.Context(), pkgConfig.CreateOpts)
+ var lintErr *lint.LintError
+ if errors.As(err, &lintErr) {
+ common.PrintFindings(lintErr)
+ // Do not return an error if the findings are all warnings.
+ if lintErr.OnlyWarnings() {
+ return nil
+ }
+ }
+ if err != nil {
+ return err
+ }
+ return nil
},
}
diff --git a/src/cmd/package.go b/src/cmd/package.go
index 8b1405e25c..a40439d53f 100644
--- a/src/cmd/package.go
+++ b/src/cmd/package.go
@@ -14,6 +14,7 @@ import (
"github.com/zarf-dev/zarf/src/cmd/common"
"github.com/zarf-dev/zarf/src/config/lang"
+ "github.com/zarf-dev/zarf/src/pkg/lint"
"github.com/zarf-dev/zarf/src/pkg/message"
"github.com/zarf-dev/zarf/src/pkg/packager/sources"
"github.com/zarf-dev/zarf/src/types"
@@ -60,7 +61,12 @@ var packageCreateCmd = &cobra.Command{
}
defer pkgClient.ClearTempPaths()
- if err := pkgClient.Create(cmd.Context()); err != nil {
+ err = pkgClient.Create(cmd.Context())
+ var lintErr *lint.LintError
+ if errors.As(err, &lintErr) {
+ common.PrintFindings(lintErr)
+ }
+ if err != nil {
return fmt.Errorf("failed to create package: %w", err)
}
return nil
diff --git a/src/config/lang/english.go b/src/config/lang/english.go
index 50ce790c44..1afdfab83c 100644
--- a/src/config/lang/english.go
+++ b/src/config/lang/english.go
@@ -276,7 +276,7 @@ $ zarf package mirror-resources \
CmdPackageDeployFlagShasum = "Shasum of the package to deploy. Required if deploying a remote package and \"--insecure\" is not provided"
CmdPackageDeployFlagSget = "[Deprecated] Path to public sget key file for remote packages signed via cosign. This flag will be removed in v1.0.0 please use the --key flag instead."
CmdPackageDeployFlagSkipWebhooks = "[alpha] Skip waiting for external webhooks to execute as each package component is deployed"
- CmdPackageDeployFlagTimeout = "Timeout for Helm operations such as installs and rollbacks"
+ CmdPackageDeployFlagTimeout = "Timeout for health checks and Helm operations such as installs and rollbacks"
CmdPackageDeployValidateArchitectureErr = "this package architecture is %s, but the target cluster only has the %s architecture(s). These architectures must be compatible when \"images\" are present"
CmdPackageDeployValidateLastNonBreakingVersionWarn = "The version of this Zarf binary '%s' is less than the LastNonBreakingVersion of '%s'. You may need to upgrade your Zarf version to at least '%s' to deploy this package"
CmdPackageDeployInvalidCLIVersionWarn = "CLIVersion is set to '%s' which can cause issues with package creation and deployment. To avoid such issues, please set the value to the valid semantic version for this version of Zarf."
diff --git a/src/pkg/lint/findings.go b/src/pkg/lint/findings.go
index a8ad9b5eac..92486f77d5 100644
--- a/src/pkg/lint/findings.go
+++ b/src/pkg/lint/findings.go
@@ -6,11 +6,15 @@ package lint
import (
"fmt"
- "path/filepath"
+)
+
+// Severity is the type of finding.
+type Severity string
- "github.com/defenseunicorns/pkg/helpers/v2"
- "github.com/fatih/color"
- "github.com/zarf-dev/zarf/src/pkg/message"
+// Severity definitions.
+const (
+ SevErr = "Error"
+ SevWarn = "Warning"
)
// PackageFinding is a struct that contains a finding about something wrong with a package
@@ -26,71 +30,18 @@ type PackageFinding struct {
// PackagePathOverride shows the path to the package that the error originated from
// If it is not set the base package will be used when displaying the error
PackagePathOverride string
- Severity Severity
+ // Severity of finding.
+ Severity Severity
}
-// Severity is the type of finding
-type Severity int
-
-// different severities of package errors
-const (
- SevErr Severity = iota + 1
- SevWarn
-)
-
-func (f PackageFinding) itemizedDescription() string {
+// ItemizedDescription returns a string with the description and item if finding contains one.
+func (f PackageFinding) ItemizedDescription() string {
if f.Item == "" {
return f.Description
}
return fmt.Sprintf("%s - %s", f.Description, f.Item)
}
-func colorWrapSev(s Severity) string {
- if s == SevErr {
- return message.ColorWrap("Error", color.FgRed)
- } else if s == SevWarn {
- return message.ColorWrap("Warning", color.FgYellow)
- }
- return "unknown"
-}
-
-func filterLowerSeverity(findings []PackageFinding, severity Severity) []PackageFinding {
- findings = helpers.RemoveMatches(findings, func(finding PackageFinding) bool {
- return finding.Severity > severity
- })
- return findings
-}
-
-// PrintFindings prints the findings of the given severity in a table
-func PrintFindings(findings []PackageFinding, severity Severity, baseDir string, packageName string) {
- findings = filterLowerSeverity(findings, severity)
- if len(findings) == 0 {
- return
- }
- mapOfFindingsByPath := GroupFindingsByPath(findings, packageName)
-
- header := []string{"Type", "Path", "Message"}
-
- for _, findings := range mapOfFindingsByPath {
- lintData := [][]string{}
- for _, finding := range findings {
- lintData = append(lintData, []string{
- colorWrapSev(finding.Severity),
- message.ColorWrap(finding.YqPath, color.FgCyan),
- finding.itemizedDescription(),
- })
- }
- var packagePathFromUser string
- if helpers.IsOCIURL(findings[0].PackagePathOverride) {
- packagePathFromUser = findings[0].PackagePathOverride
- } else {
- packagePathFromUser = filepath.Join(baseDir, findings[0].PackagePathOverride)
- }
- message.Notef("Linting package %q at %s", findings[0].PackageNameOverride, packagePathFromUser)
- message.Table(header, lintData)
- }
-}
-
// GroupFindingsByPath groups findings by their package path
func GroupFindingsByPath(findings []PackageFinding, packageName string) map[string][]PackageFinding {
for i := range findings {
@@ -108,8 +59,3 @@ func GroupFindingsByPath(findings []PackageFinding, packageName string) map[stri
}
return mapOfFindingsByPath
}
-
-// HasSevOrHigher returns true if the findings contain a severity equal to or greater than the given severity
-func HasSevOrHigher(findings []PackageFinding, severity Severity) bool {
- return len(filterLowerSeverity(findings, severity)) > 0
-}
diff --git a/src/pkg/lint/findings_test.go b/src/pkg/lint/findings_test.go
index f3c09673c8..b922927b1a 100644
--- a/src/pkg/lint/findings_test.go
+++ b/src/pkg/lint/findings_test.go
@@ -53,53 +53,3 @@ func TestGroupFindingsByPath(t *testing.T) {
})
}
}
-
-func TestHasSeverity(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- severity Severity
- expected bool
- findings []PackageFinding
- }{
- {
- name: "error severity present",
- findings: []PackageFinding{
- {
- Severity: SevErr,
- },
- },
- severity: SevErr,
- expected: true,
- },
- {
- name: "error severity not present",
- findings: []PackageFinding{
- {
- Severity: SevWarn,
- },
- },
- severity: SevErr,
- expected: false,
- },
- {
- name: "err and warning severity present",
- findings: []PackageFinding{
- {
- Severity: SevWarn,
- },
- {
- Severity: SevErr,
- },
- },
- severity: SevErr,
- expected: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- require.Equal(t, tt.expected, HasSevOrHigher(tt.findings, tt.severity))
- })
- }
-}
diff --git a/src/pkg/lint/lint.go b/src/pkg/lint/lint.go
index 3c4be87eaf..344f3b9db0 100644
--- a/src/pkg/lint/lint.go
+++ b/src/pkg/lint/lint.go
@@ -6,7 +6,6 @@ package lint
import (
"context"
- "errors"
"fmt"
"os"
@@ -14,12 +13,34 @@ import (
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/config/lang"
"github.com/zarf-dev/zarf/src/pkg/layout"
- "github.com/zarf-dev/zarf/src/pkg/message"
"github.com/zarf-dev/zarf/src/pkg/packager/composer"
"github.com/zarf-dev/zarf/src/pkg/utils"
"github.com/zarf-dev/zarf/src/types"
)
+// LintError represents an error containing lint findings.
+//
+//nolint:revive // ignore name
+type LintError struct {
+ BaseDir string
+ PackageName string
+ Findings []PackageFinding
+}
+
+func (e *LintError) Error() string {
+ return fmt.Sprintf("linting error found %d instance(s)", len(e.Findings))
+}
+
+// OnlyWarnings returns true if all findings have severity warning.
+func (e *LintError) OnlyWarnings() bool {
+ for _, f := range e.Findings {
+ if f.Severity == SevErr {
+ return false
+ }
+ }
+ return true
+}
+
// Validate lints the given Zarf package
func Validate(ctx context.Context, createOpts types.ZarfCreateOptions) error {
var findings []PackageFinding
@@ -41,16 +62,14 @@ func Validate(ctx context.Context, createOpts types.ZarfCreateOptions) error {
return err
}
findings = append(findings, schemaFindings...)
-
if len(findings) == 0 {
- message.Successf("0 findings for %q", pkg.Metadata.Name)
return nil
}
- PrintFindings(findings, SevWarn, createOpts.BaseDir, pkg.Metadata.Name)
- if HasSevOrHigher(findings, SevErr) {
- return errors.New("errors during lint")
+ return &LintError{
+ BaseDir: createOpts.BaseDir,
+ PackageName: pkg.Metadata.Name,
+ Findings: findings,
}
- return nil
}
func lintComponents(ctx context.Context, pkg v1alpha1.ZarfPackage, createOpts types.ZarfCreateOptions) ([]PackageFinding, error) {
diff --git a/src/pkg/lint/lint_test.go b/src/pkg/lint/lint_test.go
index d499ba6e45..84ea5e4cd2 100644
--- a/src/pkg/lint/lint_test.go
+++ b/src/pkg/lint/lint_test.go
@@ -15,6 +15,33 @@ import (
"github.com/zarf-dev/zarf/src/types"
)
+func TestLintError(t *testing.T) {
+ t.Parallel()
+
+ lintErr := &LintError{
+ Findings: []PackageFinding{
+ {
+ Severity: SevWarn,
+ },
+ },
+ }
+ require.Equal(t, "linting error found 1 instance(s)", lintErr.Error())
+ require.True(t, lintErr.OnlyWarnings())
+
+ lintErr = &LintError{
+ Findings: []PackageFinding{
+ {
+ Severity: SevWarn,
+ },
+ {
+ Severity: SevErr,
+ },
+ },
+ }
+ require.Equal(t, "linting error found 2 instance(s)", lintErr.Error())
+ require.False(t, lintErr.OnlyWarnings())
+}
+
func TestLintComponents(t *testing.T) {
t.Run("Test composable components with bad path", func(t *testing.T) {
t.Parallel()
diff --git a/src/pkg/message/message.go b/src/pkg/message/message.go
index e271cbf9d4..0eea8b326c 100644
--- a/src/pkg/message/message.go
+++ b/src/pkg/message/message.go
@@ -12,7 +12,6 @@ import (
"time"
"github.com/defenseunicorns/pkg/helpers/v2"
- "github.com/fatih/color"
"github.com/pterm/pterm"
)
@@ -276,16 +275,6 @@ func Table(header []string, data [][]string) {
pterm.DefaultTable.WithHasHeader().WithData(table).Render()
}
-// ColorWrap changes a string to an ansi color code and appends the default color to the end
-// preventing future characters from taking on the given color
-// returns string as normal if color is disabled
-func ColorWrap(str string, attr color.Attribute) string {
- if !ColorEnabled() || str == "" {
- return str
- }
- return fmt.Sprintf("\x1b[%dm%s\x1b[0m", attr, str)
-}
-
func debugPrinter(offset int, a ...any) {
printer := pterm.Debug.WithShowLineNumber(logLevel > 2).WithLineNumberOffset(offset)
now := time.Now().Format(time.RFC3339)
diff --git a/src/pkg/packager/composer/list.go b/src/pkg/packager/composer/list.go
index 291fdb71db..b021355739 100644
--- a/src/pkg/packager/composer/list.go
+++ b/src/pkg/packager/composer/list.go
@@ -337,6 +337,7 @@ func (ic *ImportChain) Compose(ctx context.Context) (composed *v1alpha1.ZarfComp
overrideDeprecated(composed, node.ZarfComponent)
overrideResources(composed, node.ZarfComponent)
overrideActions(composed, node.ZarfComponent)
+ composed.HealthChecks = append(composed.HealthChecks, node.ZarfComponent.HealthChecks...)
bigbang.Compose(composed, node.ZarfComponent, node.relativeToHead)
diff --git a/src/pkg/packager/composer/list_test.go b/src/pkg/packager/composer/list_test.go
index 1703348b3d..9cbeab2a3b 100644
--- a/src/pkg/packager/composer/list_test.go
+++ b/src/pkg/packager/composer/list_test.go
@@ -82,6 +82,50 @@ func TestCompose(t *testing.T) {
Name: "no-import",
},
},
+ {
+ name: "Health Checks",
+ ic: createChainFromSlice(t, []v1alpha1.ZarfComponent{
+ {
+ Name: "base",
+ HealthChecks: []v1alpha1.NamespacedObjectKindReference{
+ {
+ APIVersion: "v1",
+ Kind: "Pods",
+ Namespace: "base-ns",
+ Name: "base-pod",
+ },
+ },
+ },
+ {
+ Name: "import-one",
+ HealthChecks: []v1alpha1.NamespacedObjectKindReference{
+ {
+ APIVersion: "v1",
+ Kind: "Pods",
+ Namespace: "import-ns",
+ Name: "import-pod",
+ },
+ },
+ },
+ }),
+ expectedComposed: v1alpha1.ZarfComponent{
+ Name: "base",
+ HealthChecks: []v1alpha1.NamespacedObjectKindReference{
+ {
+ APIVersion: "v1",
+ Kind: "Pods",
+ Namespace: "import-ns",
+ Name: "import-pod",
+ },
+ {
+ APIVersion: "v1",
+ Kind: "Pods",
+ Namespace: "base-ns",
+ Name: "base-pod",
+ },
+ },
+ },
+ },
{
name: "Multiple Components",
ic: createChainFromSlice(t, []v1alpha1.ZarfComponent{
diff --git a/src/pkg/packager/creator/creator_test.go b/src/pkg/packager/creator/creator_test.go
index 95803cc0f4..6d87d09aac 100644
--- a/src/pkg/packager/creator/creator_test.go
+++ b/src/pkg/packager/creator/creator_test.go
@@ -35,7 +35,7 @@ func TestLoadPackageDefinition(t *testing.T) {
{
name: "invalid package definition",
testDir: "invalid",
- expectedErr: "found errors in schema",
+ expectedErr: "linting error found 1 instance(s)",
creator: NewPackageCreator(types.ZarfCreateOptions{}, ""),
},
{
@@ -47,7 +47,7 @@ func TestLoadPackageDefinition(t *testing.T) {
{
name: "invalid package definition",
testDir: "invalid",
- expectedErr: "found errors in schema",
+ expectedErr: "linting error found 1 instance(s)",
creator: NewSkeletonCreator(types.ZarfCreateOptions{}, types.ZarfPublishOptions{}),
},
}
diff --git a/src/pkg/packager/creator/utils.go b/src/pkg/packager/creator/utils.go
index 1bb88750e0..3d5c5ef016 100644
--- a/src/pkg/packager/creator/utils.go
+++ b/src/pkg/packager/creator/utils.go
@@ -23,18 +23,18 @@ func Validate(pkg v1alpha1.ZarfPackage, baseDir string, setVariables map[string]
if err := lint.ValidatePackage(pkg); err != nil {
return fmt.Errorf("package validation failed: %w", err)
}
-
findings, err := lint.ValidatePackageSchema(setVariables)
if err != nil {
return fmt.Errorf("unable to check schema: %w", err)
}
-
- if lint.HasSevOrHigher(findings, lint.SevErr) {
- lint.PrintFindings(findings, lint.SevErr, baseDir, pkg.Metadata.Name)
- return fmt.Errorf("found errors in schema")
+ if len(findings) == 0 {
+ return nil
+ }
+ return &lint.LintError{
+ BaseDir: baseDir,
+ PackageName: pkg.Metadata.Name,
+ Findings: findings,
}
-
- return nil
}
// recordPackageMetadata records various package metadata during package create.
diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go
index 517bff7d60..ee3796ac6c 100644
--- a/src/pkg/packager/deploy.go
+++ b/src/pkg/packager/deploy.go
@@ -19,11 +19,15 @@ import (
"golang.org/x/sync/errgroup"
"github.com/avast/retry-go/v4"
- "github.com/defenseunicorns/pkg/helpers/v2"
+ pkgkubernetes "github.com/defenseunicorns/pkg/kubernetes"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/cli-utils/pkg/kstatus/watcher"
+ "sigs.k8s.io/cli-utils/pkg/object"
+ "github.com/defenseunicorns/pkg/helpers/v2"
"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/config"
"github.com/zarf-dev/zarf/src/internal/git"
@@ -234,6 +238,30 @@ func (p *Packager) deployComponents(ctx context.Context) ([]types.DeployedCompon
return deployedComponents, nil
}
+func runHealthChecks(ctx context.Context, watcher watcher.StatusWatcher, healthChecks []v1alpha1.NamespacedObjectKindReference) error {
+ objs := []object.ObjMetadata{}
+ for _, hc := range healthChecks {
+ gv, err := schema.ParseGroupVersion(hc.APIVersion)
+ if err != nil {
+ return err
+ }
+ obj := object.ObjMetadata{
+ GroupKind: schema.GroupKind{
+ Group: gv.Group,
+ Kind: hc.Kind,
+ },
+ Namespace: hc.Namespace,
+ Name: hc.Name,
+ }
+ objs = append(objs, obj)
+ }
+ err := pkgkubernetes.WaitForReady(ctx, watcher, objs)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func (p *Packager) deployInitComponent(ctx context.Context, component v1alpha1.ZarfComponent) ([]types.InstalledChart, error) {
hasExternalRegistry := p.cfg.InitOpts.RegistryInfo.Address != ""
isSeedRegistry := component.Name == "zarf-seed-registry"
@@ -370,6 +398,17 @@ func (p *Packager) deployComponent(ctx context.Context, component v1alpha1.ZarfC
return nil, fmt.Errorf("unable to run component after action: %w", err)
}
+ if len(component.HealthChecks) > 0 {
+ healthCheckContext, cancel := context.WithTimeout(ctx, p.cfg.DeployOpts.Timeout)
+ defer cancel()
+ spinner := message.NewProgressSpinner("Running health checks")
+ defer spinner.Stop()
+ if err = runHealthChecks(healthCheckContext, p.cluster.Watcher, component.HealthChecks); err != nil {
+ return nil, fmt.Errorf("health checks failed: %w", err)
+ }
+ spinner.Success()
+ }
+
err = g.Wait()
if err != nil {
return nil, err
diff --git a/src/pkg/packager/deploy_test.go b/src/pkg/packager/deploy_test.go
index a1f32aa346..f5dba049e1 100644
--- a/src/pkg/packager/deploy_test.go
+++ b/src/pkg/packager/deploy_test.go
@@ -4,12 +4,22 @@
package packager
import (
+ "context"
"testing"
+ "time"
"github.com/stretchr/testify/require"
"github.com/zarf-dev/zarf/src/api/v1alpha1"
"github.com/zarf-dev/zarf/src/pkg/packager/sources"
"github.com/zarf-dev/zarf/src/types"
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/util/yaml"
+ dynamicfake "k8s.io/client-go/dynamic/fake"
+ "k8s.io/kubectl/pkg/scheme"
+ "sigs.k8s.io/cli-utils/pkg/kstatus/watcher"
+ "sigs.k8s.io/cli-utils/pkg/testutil"
)
func TestGenerateValuesOverrides(t *testing.T) {
@@ -272,3 +282,82 @@ func TestServiceInfoFromServiceURL(t *testing.T) {
})
}
}
+
+var podCurrentYaml = `
+apiVersion: v1
+kind: Pod
+metadata:
+ name: good-pod
+ namespace: ns
+status:
+ conditions:
+ - type: Ready
+ status: "True"
+ phase: Running
+`
+
+var podYaml = `
+apiVersion: v1
+kind: Pod
+metadata:
+ name: in-progress-pod
+ namespace: ns
+`
+
+func yamlToUnstructured(t *testing.T, yml string) *unstructured.Unstructured {
+ t.Helper()
+ m := make(map[string]interface{})
+ err := yaml.Unmarshal([]byte(yml), &m)
+ require.NoError(t, err)
+ return &unstructured.Unstructured{Object: m}
+}
+
+func TestRunHealthChecks(t *testing.T) {
+ t.Parallel()
+ tests := []struct {
+ name string
+ podYaml string
+ expectErr error
+ }{
+ {
+ name: "Pod is running",
+ podYaml: podCurrentYaml,
+ expectErr: nil,
+ },
+ {
+ name: "Pod is never ready",
+ podYaml: podYaml,
+ expectErr: context.DeadlineExceeded,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme)
+ fakeMapper := testutil.NewFakeRESTMapper(
+ v1.SchemeGroupVersion.WithKind("Pod"),
+ )
+ ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
+ defer cancel()
+ pod := yamlToUnstructured(t, tt.podYaml)
+ statusWatcher := watcher.NewDefaultStatusWatcher(fakeClient, fakeMapper)
+ podGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}
+ require.NoError(t, fakeClient.Tracker().Create(podGVR, pod, pod.GetNamespace()))
+ objs := []v1alpha1.NamespacedObjectKindReference{
+ {
+ APIVersion: pod.GetAPIVersion(),
+ Kind: pod.GetKind(),
+ Namespace: pod.GetNamespace(),
+ Name: pod.GetName(),
+ },
+ }
+ err := runHealthChecks(ctx, statusWatcher, objs)
+ if tt.expectErr != nil {
+ require.ErrorIs(t, err, tt.expectErr)
+ return
+ }
+ require.NoError(t, err)
+ })
+ }
+}
diff --git a/src/pkg/packager/prepare.go b/src/pkg/packager/prepare.go
index 37b80f0ec4..1e43335898 100644
--- a/src/pkg/packager/prepare.go
+++ b/src/pkg/packager/prepare.go
@@ -245,7 +245,6 @@ func (p *Packager) findImages(ctx context.Context) (map[string][]string, error)
// Break the manifest into separate resources
yamls, err := utils.SplitYAML(contents)
if err != nil {
- fmt.Println("got this err")
return nil, err
}
resources = append(resources, yamls...)
diff --git a/src/test/e2e/36_health_check_test.go b/src/test/e2e/36_health_check_test.go
new file mode 100644
index 0000000000..3ad84162bd
--- /dev/null
+++ b/src/test/e2e/36_health_check_test.go
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2021-Present The Zarf Authors
+
+// Package test provides e2e tests for Zarf.
+package test
+
+import (
+ "fmt"
+
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestHealthChecks(t *testing.T) {
+ t.Log("E2E: Health Checks")
+
+ _, _, err := e2e.Zarf(t, "package", "create", "src/test/packages/36-health-checks", "-o=build", "--confirm")
+ require.NoError(t, err)
+
+ path := fmt.Sprintf("build/zarf-package-health-checks-%s.tar.zst", e2e.Arch)
+
+ _, _, err = e2e.Zarf(t, "package", "deploy", path, "--confirm")
+ require.NoError(t, err)
+
+ defer func() {
+ _, _, err = e2e.Zarf(t, "package", "remove", "health-checks", "--confirm")
+ require.NoError(t, err)
+ }()
+
+ stdOut, _, err := e2e.Kubectl(t, "get", "pod", "ready-pod", "-n", "health-checks", "-o", "jsonpath={.status.phase}")
+ require.NoError(t, err)
+ require.Equal(t, "Running", stdOut)
+}
diff --git a/src/test/packages/36-health-checks/ready-pod.yaml b/src/test/packages/36-health-checks/ready-pod.yaml
new file mode 100644
index 0000000000..836ce373a6
--- /dev/null
+++ b/src/test/packages/36-health-checks/ready-pod.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: ready-pod
+spec:
+ # Extra security to ensure the pod isn't ready before the health checks run
+ initContainers:
+ - name: init-wait
+ image: ghcr.io/stefanprodan/podinfo:6.4.0
+ command: ["sh", "-c", "sleep 3"]
+ containers:
+ - name: podinfo
+ image: ghcr.io/stefanprodan/podinfo:6.4.0
diff --git a/src/test/packages/36-health-checks/zarf.yaml b/src/test/packages/36-health-checks/zarf.yaml
new file mode 100644
index 0000000000..a34f125cd0
--- /dev/null
+++ b/src/test/packages/36-health-checks/zarf.yaml
@@ -0,0 +1,21 @@
+kind: ZarfPackageConfig
+metadata:
+ name: health-checks
+ description: Deploys a simple pod to test health checks
+
+components:
+ - name: health-checks
+ required: true
+ manifests:
+ - name: ready-pod
+ namespace: health-checks
+ noWait: true
+ files:
+ - ready-pod.yaml
+ images:
+ - ghcr.io/stefanprodan/podinfo:6.4.0
+ healthChecks:
+ - name: ready-pod
+ namespace: health-checks
+ apiVersion: v1
+ kind: Pod
diff --git a/zarf.schema.json b/zarf.schema.json
index b1fca50208..d5067e42ad 100644
--- a/zarf.schema.json
+++ b/zarf.schema.json
@@ -169,6 +169,38 @@
"^x-": {}
}
},
+ "NamespacedObjectKindReference": {
+ "properties": {
+ "apiVersion": {
+ "type": "string",
+ "description": "API Version of the resource"
+ },
+ "kind": {
+ "type": "string",
+ "description": "Kind of the resource"
+ },
+ "namespace": {
+ "type": "string",
+ "description": "Namespace of the resource"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the resource"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "required": [
+ "apiVersion",
+ "kind",
+ "namespace",
+ "name"
+ ],
+ "description": "NamespacedObjectKindReference is a reference to a specific resource in a namespace using its kind and API version.",
+ "patternProperties": {
+ "^x-": {}
+ }
+ },
"Shell": {
"properties": {
"windows": {
@@ -512,6 +544,13 @@
"actions": {
"$ref": "#/$defs/ZarfComponentActions",
"description": "Custom commands to run at various stages of a package lifecycle."
+ },
+ "healthChecks": {
+ "items": {
+ "$ref": "#/$defs/NamespacedObjectKindReference"
+ },
+ "type": "array",
+ "description": "List of resources to health check after deployment"
}
},
"additionalProperties": false,