feat: kubedog integration with unified resource handling (#2383)

* feat: add kubedog-based resource tracking integration

Add kubedog tracking as an alternative to Helm's --wait flag with:
- Real-time deployment progress tracking
- Container log streaming
- Fine-grained resource filtering (trackKinds/skipKinds/trackResources)

Features:
- New pkg/resource package for unified manifest parsing and filtering
- New pkg/kubedog package wrapping kubedog library
- CLI flags: --track-mode, --track-timeout, --track-logs
- Helmfile YAML support for trackMode, trackTimeout, trackLogs, trackKinds, skipKinds, trackResources
- Case-insensitive kind matching for filtering
- Multi-context support with proper kubeconfig/kubeContext handling

Tracking supports: Deployment, StatefulSet, DaemonSet, Job

Resource filtering priority (highest to lowest):
1. trackResources - explicit resource whitelist
2. skipKinds - blacklist specific kinds
3. trackKinds - whitelist specific kinds

Integration:
- Disable Helm --wait when using kubedog tracking
- Track after successful Helm sync/apply
- Respect release.Namespace as fallback for resources without namespace
- Use getKubeContext() for correct cluster targeting

Tests:
- Unit tests for resource filtering and kubedog options
- Integration test with httpbin chart
- E2E snapshot tests for YAML serialization
- Documentation in docs/advanced-features.md

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: address PR #2383 review comments (round 4)

1. resource/filter.go: Skip empty whitelist entries in matchWhitelist
   - At least one field (kind/name/namespace) must be specified
   - Prevents matching all resources with empty TrackResources entries

2. config/apply.go: Add ValidateConfig for track-mode validation
   - Validate --track-mode must be 'helm' or 'kubedog'
   - Reject invalid values like --track-mode foo

3. config/sync.go: Add ValidateConfig for track-mode validation
   - Same validation as apply command
   - Ensures consistent behavior across commands

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2026-03-02 17:15:12 +08:00 committed by GitHub
parent c70bd04f3a
commit 6e21671228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1550 additions and 17 deletions

View File

@ -67,7 +67,10 @@ func NewApplyCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.StringArrayVar(&applyOptions.PostRendererArgs, "post-renderer-args", nil, `pass --post-renderer-args to "helm template" or "helm upgrade --install"`)
f.BoolVar(&applyOptions.SkipSchemaValidation, "skip-schema-validation", false, `pass --skip-schema-validation to "helm template" or "helm upgrade --install"`)
f.StringVar(&applyOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background")
f.StringArrayVar(&applyOptions.SuppressOutputLineRegex, "suppress-output-line-regex", nil, "a list of regex patterns to suppress output lines from the diff output")
f.StringArrayVar(&applyOptions.SuppressOutputLineRegex, "suppress-output-line-regex", nil, "a list of regex patterns to suppress output lines from diff output")
f.StringVar(&applyOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default) or 'kubedog'")
f.IntVar(&applyOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
f.BoolVar(&applyOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
return cmd
}

View File

@ -54,6 +54,9 @@ func NewSyncCmd(globalCfg *config.GlobalImpl) *cobra.Command {
f.StringArrayVar(&syncOptions.PostRendererArgs, "post-renderer-args", nil, `pass --post-renderer-args to "helm template" or "helm upgrade --install"`)
f.BoolVar(&syncOptions.SkipSchemaValidation, "skip-schema-validation", false, `pass --skip-schema-validation to "helm template" or "helm upgrade --install"`)
f.StringVar(&syncOptions.Cascade, "cascade", "", "pass cascade to helm exec, default: background")
f.StringVar(&syncOptions.TrackMode, "track-mode", "", "Track mode for releases: 'helm' (default) or 'kubedog'")
f.IntVar(&syncOptions.TrackTimeout, "track-timeout", 0, `Timeout in seconds for kubedog tracking (0 to use default 300s timeout)`)
f.BoolVar(&syncOptions.TrackLogs, "track-logs", false, "Enable log streaming with kubedog tracking")
return cmd
}

View File

@ -1,10 +1,97 @@
## Advanced Features
- [Resource Tracking with Kubedog](#resource-tracking-with-kubedog)
- [Import Configuration Parameters into Helmfile](#import-configuration-parameters-into-helmfile)
- [Deploy Kustomization with Helmfile](#deploy-kustomizations-with-helmfile)
- [Adhoc Kustomization of Helm Charts](#adhoc-kustomization-of-helm-charts)
- [Adding dependencies without forking the chart](#adding-dependencies-without-forking-the-chart)
### Resource Tracking with Kubedog
Helmfile can use [kubedog](https://github.com/werf/kubedog) for advanced resource tracking instead of Helm's built-in `--wait` flag. This provides more detailed feedback and control over deployment progress.
#### Basic Usage
Enable kubedog tracking in your `helmfile.yaml`:
```yaml
releases:
- name: myapp
chart: ./charts/myapp
trackMode: kubedog
trackTimeout: 300 # seconds
trackLogs: true
```
Or use command-line flags:
```bash
helmfile apply --track-mode kubedog --track-timeout 300 --track-logs
```
#### Configuration Options
- **`trackMode`**: Set to `kubedog` to enable kubedog tracking (default: `helm`)
- **`trackTimeout`**: Timeout in seconds for tracking resources (default: 300)
- **`trackLogs`**: Enable real-time log streaming from tracked resources
#### Resource Filtering
Control which resources to track using whitelist/blacklist:
```yaml
releases:
- name: myapp
chart: ./charts/myapp
trackMode: kubedog
# Track only specific resource kinds
trackKinds:
- Deployment
- StatefulSet
# Skip certain resource kinds
skipKinds:
- ConfigMap
- Secret
```
#### Specific Resource Tracking
Track only specific resources by name and namespace:
```yaml
releases:
- name: myapp
chart: ./charts/myapp
trackMode: kubedog
trackResources:
- kind: Deployment
name: myapp-deployment
namespace: default
- kind: Job
name: myapp-job
```
#### Priority Rules
Resource filtering follows this priority (highest to lowest):
1. **`trackResources`**: Whitelist specific resources (takes highest priority)
2. **`skipKinds`**: Blacklist resource kinds
3. **`trackKinds`**: Whitelist resource kinds
#### Benefits
- **Real-time feedback**: See deployment progress with detailed status updates
- **Log streaming**: View container logs during deployment
- **Fine-grained control**: Track only the resources you care about
- **Better debugging**: Immediate visibility into deployment issues
#### Compatibility
- Kubedog tracking is compatible with Helm 3.x
- Kubedog is a compiled dependency and is only used when `trackMode: kubedog` is set
- Works with charts that deploy supported workload kinds (currently `Deployment`, `StatefulSet`, `DaemonSet`, and `Job`); other resource kinds are created by Helm/Helmfile as usual but are ignored by the kubedog tracker
### Import Configuration Parameters into Helmfile
Helmfile integrates [vals]() to import configuration parameters from following backends:

14
go.mod
View File

@ -25,6 +25,7 @@ require (
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/tj/assert v0.0.3
github.com/variantdev/dag v1.1.0
github.com/werf/kubedog v0.13.0
github.com/zclconf/go-cty v1.18.0
github.com/zclconf/go-cty-yaml v1.2.0
go.szostok.io/version v1.2.0
@ -107,7 +108,7 @@ require (
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.1 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
sigs.k8s.io/yaml v1.6.0
)
require (
@ -144,6 +145,7 @@ require (
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/antchfx/jsonquery v1.3.6 // indirect
github.com/antchfx/xpath v1.3.5 // indirect
@ -152,6 +154,7 @@ require (
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect
@ -178,6 +181,7 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/chanced/caps v1.0.2 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/containerd/containerd v1.7.30 // indirect
@ -199,6 +203,7 @@ require (
github.com/extism/go-sdk v1.7.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/cli-utils v0.37.0-flux.1 // indirect
github.com/fluxcd/flagger v1.36.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect
github.com/getsops/sops/v3 v3.12.1 // indirect
@ -241,6 +246,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
@ -286,6 +292,7 @@ require (
github.com/rs/zerolog v1.26.1 // indirect
github.com/rubenv/sql-migrate v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
@ -300,9 +307,13 @@ require (
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/urfave/cli v1.22.17 // indirect
github.com/werf/logboek v0.6.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yandex-cloud/go-genproto v0.56.0 // indirect
github.com/yandex-cloud/go-sdk v0.31.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
@ -320,6 +331,7 @@ require (
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect

25
go.sum
View File

@ -130,6 +130,8 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/antchfx/jsonquery v1.3.6 h1:TaSfeAh7n6T11I74bsZ1FswreIfrbJ0X+OyLflx6mx4=
@ -147,6 +149,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 h1:HrMVYtly2IVqg9EBooHsakQ256ueojP7QuG32K71X/U=
github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774/go.mod h1:5wi5YYOpfuAKwL5XLFYopbgIl/v7NZxaJpa/4X6yFKE=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
@ -214,6 +218,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/chanced/caps v1.0.2 h1:RELvNN4lZajqSXJGzPaU7z8B4LK2+o2Oc/upeWdgMOA=
github.com/chanced/caps v1.0.2/go.mod h1:SJhRzeYLKJ3OmzyQXhdZ7Etj7lqqWoPtQ1zcSJRtQjs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
@ -237,8 +243,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyberark/conjur-api-go v0.13.16 h1:4PT/cja78hIIyUy1EOQPhppedVMY76BfzmWR20c8kog=
github.com/cyberark/conjur-api-go v0.13.16/go.mod h1:BQmiYeA8hJmGSduF+wgfXY4Ktdky30+cevXm+tzr63k=
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
@ -305,6 +311,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluxcd/cli-utils v0.37.0-flux.1 h1:k/VvPNT3tGa/l2N+qzHduaQr3GVbgoWS6nw7tGZz16w=
github.com/fluxcd/cli-utils v0.37.0-flux.1/go.mod h1:aND5wX3LuTFtB7eUT7vsWr8mmxRVSPR2Wkvbn0SqPfw=
github.com/fluxcd/flagger v1.36.1 h1:X2PumtNwZz9YSGaOtZLFm2zAKLgHhFkbNv8beg7ifyc=
github.com/fluxcd/flagger v1.36.1/go.mod h1:qmtLsxheVDTI8XeCaXUxW5UCmfcSKnY9fizG9NmW/Fk=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@ -462,6 +470,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1Znvmczt
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@ -696,6 +706,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
@ -763,11 +775,16 @@ github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/variantdev/dag v1.1.0 h1:xodYlSng33KWGvIGMpKUyLcIZRXKiNUx612mZJqYrDg=
github.com/variantdev/dag v1.1.0/go.mod h1:pH1TQsNSLj2uxMo9NNl9zdGy01Wtn+/2MT96BrKmVyE=
github.com/werf/kubedog v0.13.0 h1:ys+GyZbIMqm0r2po0HClbONcEnS5cWSFR2BayIfBqsY=
github.com/werf/kubedog v0.13.0/go.mod h1:Y6pesrIN5uhFKqmHnHSoeW4jmVyZlWPFWv5SjB0rUPg=
github.com/werf/logboek v0.6.1 h1:oEe6FkmlKg0z0n80oZjLplj6sXcBeLleCkjfOOZEL2g=
github.com/werf/logboek v0.6.1/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@ -776,6 +793,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yandex-cloud/go-genproto v0.56.0 h1:dMBLqeWc4X0gkdevJEnBXkYV9JGci7EHb9NdCbU3N0c=
github.com/yandex-cloud/go-genproto v0.56.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
github.com/yandex-cloud/go-sdk v0.31.0 h1:iPixKMu7t64xziWRIEW3pKkq3kGuvgNmiwH/Vl1FcqY=
@ -876,6 +895,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@ -808,7 +808,14 @@ func (a *App) loadDesiredStateFromYamlWithBaseDir(file string, baseDir string, o
valsRuntime: a.valsRuntime,
}
return ld.Load(file, op)
st, err := ld.Load(file, op)
if err != nil {
return nil, err
}
st.SetKubeconfig(a.Kubeconfig)
return st, nil
}
type helmKey struct {
@ -1762,6 +1769,9 @@ Do you really want to apply?
HideNotes: c.HideNotes(),
TakeOwnership: c.TakeOwnership(),
SyncReleaseLabels: c.SyncReleaseLabels(),
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
}))
@ -2227,6 +2237,9 @@ Do you really want to sync?
TakeOwnership: c.TakeOwnership(),
SkipSchemaValidation: c.SkipSchemaValidation(),
SyncReleaseLabels: c.SyncReleaseLabels(),
TrackMode: c.TrackMode(),
TrackTimeout: c.TrackTimeout(),
TrackLogs: c.TrackLogs(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
}))

View File

@ -80,7 +80,6 @@ func TestSync(t *testing.T) {
}
syncErr := app.Sync(applyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
concurrency: tc.concurrency,
logger: logger,
skipDiffOnInstall: tc.skipDiffOnInstall,

View File

@ -2380,6 +2380,9 @@ type applyConfig struct {
takeOwnership bool
syncReleaseLabels bool
enforceNeedsAreInstalled bool
trackMode string
trackTimeout int
trackLogs bool
// template-only options
includeCRDs, skipTests bool
@ -2590,6 +2593,18 @@ func (a applyConfig) SyncReleaseLabels() bool {
return a.syncReleaseLabels
}
func (a applyConfig) TrackMode() string {
return a.trackMode
}
func (a applyConfig) TrackTimeout() int {
return a.trackTimeout
}
func (a applyConfig) TrackLogs() bool {
return a.trackLogs
}
type depsConfig struct {
skipRepos bool
includeTransitiveNeeds bool

View File

@ -87,6 +87,10 @@ type ApplyConfigProvider interface {
DAGConfig
TrackMode() string
TrackTimeout() int
TrackLogs() bool
concurrencyConfig
interactive
loggingConfig
@ -119,6 +123,9 @@ type SyncConfigProvider interface {
IncludeTransitiveNeeds() bool
SyncReleaseLabels() bool
TrackMode() string
TrackTimeout() int
TrackLogs() bool
DAGConfig

11
pkg/cluster/release.go Normal file
View File

@ -0,0 +1,11 @@
package cluster
import (
"github.com/helmfile/helmfile/pkg/resource"
)
type (
Resource = resource.Resource
FilterConfig = resource.FilterConfig
ResourceFilter = resource.ResourceFilter
)

View File

@ -1,5 +1,7 @@
package config
import "fmt"
// ApplyOptoons is the options for the apply command
type ApplyOptions struct {
// Set is a list of key value pairs to be merged into the command
@ -75,6 +77,12 @@ type ApplyOptions struct {
TakeOwnership bool
SyncReleaseLabels bool
// TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources
TrackMode string
// TrackTimeout specifies timeout for kubedog tracking (in seconds)
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
}
// NewApply creates a new Apply
@ -280,3 +288,25 @@ func (a *ApplyImpl) TakeOwnership() bool {
func (a *ApplyImpl) SyncReleaseLabels() bool {
return a.ApplyOptions.SyncReleaseLabels
}
// TrackMode returns the track mode.
func (a *ApplyImpl) TrackMode() string {
return a.ApplyOptions.TrackMode
}
// TrackTimeout returns the track timeout.
func (a *ApplyImpl) TrackTimeout() int {
return a.ApplyOptions.TrackTimeout
}
// TrackLogs returns the track logs flag.
func (a *ApplyImpl) TrackLogs() bool {
return a.ApplyOptions.TrackLogs
}
func (a *ApplyImpl) ValidateConfig() error {
if a.ApplyOptions.TrackMode != "" && a.ApplyOptions.TrackMode != "helm" && a.ApplyOptions.TrackMode != "kubedog" {
return fmt.Errorf("--track-mode must be 'helm' or 'kubedog', got: %s", a.ApplyOptions.TrackMode)
}
return a.GlobalImpl.ValidateConfig()
}

View File

@ -1,5 +1,7 @@
package config
import "fmt"
// SyncOptions is the options for the build command
type SyncOptions struct {
// Set is the set flag
@ -48,6 +50,12 @@ type SyncOptions struct {
TakeOwnership bool
// SyncReleaseLabels is the sync release labels flag
SyncReleaseLabels bool
// TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources
TrackMode string
// TrackTimeout specifies timeout for kubedog tracking (in seconds)
TrackTimeout int
// TrackLogs enables log streaming with kubedog
TrackLogs bool
}
// NewSyncOptions creates a new Apply
@ -187,3 +195,25 @@ func (t *SyncImpl) TakeOwnership() bool {
func (t *SyncImpl) SyncReleaseLabels() bool {
return t.SyncOptions.SyncReleaseLabels
}
// TrackMode returns the track mode.
func (t *SyncImpl) TrackMode() string {
return t.SyncOptions.TrackMode
}
// TrackTimeout returns the track timeout.
func (t *SyncImpl) TrackTimeout() int {
return t.SyncOptions.TrackTimeout
}
// TrackLogs returns the track logs flag.
func (t *SyncImpl) TrackLogs() bool {
return t.SyncOptions.TrackLogs
}
func (t *SyncImpl) ValidateConfig() error {
if t.SyncOptions.TrackMode != "" && t.SyncOptions.TrackMode != "helm" && t.SyncOptions.TrackMode != "kubedog" {
return fmt.Errorf("--track-mode must be 'helm' or 'kubedog', got: %s", t.SyncOptions.TrackMode)
}
return t.GlobalImpl.ValidateConfig()
}

43
pkg/kubedog/options.go Normal file
View File

@ -0,0 +1,43 @@
package kubedog
import (
"time"
"github.com/helmfile/helmfile/pkg/resource"
)
type TrackMode string
const (
TrackModeHelm TrackMode = "helm"
TrackModeKubedog TrackMode = "kubedog"
)
type TrackOptions struct {
Timeout time.Duration
Logs bool
LogsSince time.Duration
Filter *resource.FilterConfig
}
func NewTrackOptions() *TrackOptions {
return &TrackOptions{
Timeout: 5 * time.Minute,
LogsSince: 10 * time.Minute,
}
}
func (o *TrackOptions) WithTimeout(timeout time.Duration) *TrackOptions {
o.Timeout = timeout
return o
}
func (o *TrackOptions) WithLogs(logs bool) *TrackOptions {
o.Logs = logs
return o
}
func (o *TrackOptions) WithFilterConfig(config *resource.FilterConfig) *TrackOptions {
o.Filter = config
return o
}

200
pkg/kubedog/tracker.go Normal file
View File

@ -0,0 +1,200 @@
package kubedog
import (
"context"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/werf/kubedog/pkg/kube"
"github.com/werf/kubedog/pkg/tracker"
"github.com/werf/kubedog/pkg/trackers/rollout/multitrack"
"go.uber.org/zap"
"k8s.io/client-go/kubernetes"
"github.com/helmfile/helmfile/pkg/resource"
)
type cacheKey struct {
kubeContext string
kubeconfig string
}
var (
kubeInitMu sync.Mutex
clientCache = make(map[cacheKey]kubernetes.Interface)
)
type Tracker struct {
logger *zap.SugaredLogger
clientSet kubernetes.Interface
trackOptions *TrackOptions
filter *resource.ResourceFilter
namespace string
}
type TrackerConfig struct {
Logger *zap.SugaredLogger
Namespace string
KubeContext string
Kubeconfig string
TrackOptions *TrackOptions
}
func NewTracker(config *TrackerConfig) (*Tracker, error) {
logger := config.Logger
if logger == nil {
logger = zap.NewNop().Sugar()
}
kubeconfig := config.Kubeconfig
if kubeconfig == "" {
kubeconfig = os.Getenv("KUBECONFIG")
}
clientSet, err := getOrCreateClient(config.KubeContext, kubeconfig)
if err != nil {
return nil, fmt.Errorf("failed to initialize kubernetes client: %w", err)
}
options := config.TrackOptions
if options == nil {
options = NewTrackOptions()
}
var filter *resource.ResourceFilter
if options.Filter != nil {
filter = resource.NewResourceFilter(options.Filter, logger)
}
return &Tracker{
logger: logger,
clientSet: clientSet,
trackOptions: options,
filter: filter,
namespace: config.Namespace,
}, nil
}
func getOrCreateClient(kubeContext, kubeconfig string) (kubernetes.Interface, error) {
key := cacheKey{
kubeContext: kubeContext,
kubeconfig: kubeconfig,
}
kubeInitMu.Lock()
defer kubeInitMu.Unlock()
if client, ok := clientCache[key]; ok {
return client, nil
}
initOpts := kube.InitOptions{
KubeConfigOptions: kube.KubeConfigOptions{
Context: kubeContext,
ConfigPath: kubeconfig,
},
}
if err := kube.Init(initOpts); err != nil {
return nil, err
}
client := kube.Kubernetes
clientCache[key] = client
return client, nil
}
func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Resource) error {
if len(resources) == 0 {
t.logger.Info("No resources to track")
return nil
}
filtered := t.filterResources(resources)
if len(filtered) == 0 {
t.logger.Info("No resources to track after filtering")
return nil
}
t.logger.Infof("Tracking %d resources with kubedog (filtered from %d total)", len(filtered), len(resources))
specs := multitrack.MultitrackSpecs{}
for _, res := range filtered {
namespace := res.Namespace
if namespace == "" {
namespace = t.namespace
}
switch strings.ToLower(res.Kind) {
case "deployment", "deploy":
specs.Deployments = append(specs.Deployments, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
case "statefulset", "sts":
specs.StatefulSets = append(specs.StatefulSets, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
case "daemonset", "ds":
specs.DaemonSets = append(specs.DaemonSets, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
case "job":
specs.Jobs = append(specs.Jobs, multitrack.MultitrackSpec{
ResourceName: res.Name,
Namespace: namespace,
SkipLogs: !t.trackOptions.Logs,
})
default:
t.logger.Debugf("Skipping unsupported kind %s for resource %s/%s", res.Kind, namespace, res.Name)
}
}
if len(specs.Deployments)+len(specs.StatefulSets)+len(specs.DaemonSets)+len(specs.Jobs) == 0 {
t.logger.Info("No trackable resources found (only Deployment, StatefulSet, DaemonSet, and Job are supported)")
return nil
}
opts := multitrack.MultitrackOptions{
Options: tracker.Options{
ParentContext: ctx,
Timeout: t.trackOptions.Timeout,
LogsFromTime: time.Now().Add(-t.trackOptions.LogsSince),
},
StatusProgressPeriod: 5 * time.Second,
}
err := multitrack.Multitrack(t.clientSet, specs, opts)
if err != nil {
return fmt.Errorf("tracking failed: %w", err)
}
t.logger.Info("All resources tracked successfully")
return nil
}
func (t *Tracker) filterResources(resources []*resource.Resource) []*resource.Resource {
if t.filter == nil {
return resources
}
var result []*resource.Resource
for _, res := range resources {
if t.filter.ShouldTrack(res) {
result = append(result, res)
} else {
t.logger.Debugf("Skipping resource %s/%s (kind: %s) based on configuration", res.Namespace, res.Name, res.Kind)
}
}
return result
}

View File

@ -0,0 +1,88 @@
package kubedog
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/helmfile/helmfile/pkg/resource"
)
func TestTrackMode(t *testing.T) {
assert.Equal(t, "helm", string(TrackModeHelm))
assert.Equal(t, "kubedog", string(TrackModeKubedog))
}
func TestNewTrackOptions(t *testing.T) {
opts := NewTrackOptions()
assert.NotNil(t, opts)
assert.Equal(t, 5*time.Minute, opts.Timeout)
assert.Equal(t, false, opts.Logs)
assert.Equal(t, 10*time.Minute, opts.LogsSince)
}
func TestTrackOptions_WithTimeout(t *testing.T) {
opts := NewTrackOptions()
opts = opts.WithTimeout(10 * time.Second)
assert.Equal(t, 10*time.Second, opts.Timeout)
}
func TestTrackOptions_WithLogs(t *testing.T) {
opts := NewTrackOptions()
opts = opts.WithLogs(true)
assert.True(t, opts.Logs)
}
func TestTrackOptions_Chaining(t *testing.T) {
opts := NewTrackOptions()
opts = opts.
WithTimeout(20 * time.Second).
WithLogs(true)
assert.Equal(t, 20*time.Second, opts.Timeout)
assert.True(t, opts.Logs)
}
func TestResource(t *testing.T) {
res := &resource.Resource{
Name: "test-resource",
Namespace: "test-ns",
Kind: "deployment",
}
assert.Equal(t, "test-resource", res.Name)
assert.Equal(t, "test-ns", res.Namespace)
assert.Equal(t, "deployment", res.Kind)
}
func TestTrackerConfig(t *testing.T) {
config := &TrackerConfig{
Logger: nil,
Namespace: "test-ns",
KubeContext: "test-ctx",
Kubeconfig: "/test/kubeconfig",
TrackOptions: NewTrackOptions(),
}
assert.NotNil(t, config)
assert.Equal(t, "test-ns", config.Namespace)
assert.Equal(t, "test-ctx", config.KubeContext)
assert.Equal(t, "/test/kubeconfig", config.Kubeconfig)
assert.NotNil(t, config.TrackOptions)
}
func TestTrackOptions_WithFilterConfig(t *testing.T) {
opts := NewTrackOptions()
filter := &resource.FilterConfig{
TrackKinds: []string{"Deployment", "StatefulSet"},
SkipKinds: []string{"ConfigMap"},
}
opts = opts.WithFilterConfig(filter)
assert.NotNil(t, opts.Filter)
assert.Equal(t, []string{"Deployment", "StatefulSet"}, opts.Filter.TrackKinds)
assert.Equal(t, []string{"ConfigMap"}, opts.Filter.SkipKinds)
}

94
pkg/resource/filter.go Normal file
View File

@ -0,0 +1,94 @@
package resource
import (
"strings"
"go.uber.org/zap"
)
type ResourceFilter struct {
logger *zap.SugaredLogger
config *FilterConfig
skipKinds map[string]bool
trackKinds map[string]bool
}
func NewResourceFilter(config *FilterConfig, logger *zap.SugaredLogger) *ResourceFilter {
f := &ResourceFilter{
config: config,
logger: logger,
}
if config != nil {
f.skipKinds = make(map[string]bool)
for _, kind := range config.SkipKinds {
f.skipKinds[strings.ToLower(kind)] = true
}
f.trackKinds = make(map[string]bool)
for _, kind := range config.TrackKinds {
f.trackKinds[strings.ToLower(kind)] = true
}
}
return f
}
func (f *ResourceFilter) Filter(resources []Resource) []Resource {
if f.config == nil {
return resources
}
var filtered []Resource
for i := range resources {
if f.ShouldTrack(&resources[i]) {
filtered = append(filtered, resources[i])
} else if f.logger != nil {
res := resources[i]
f.logger.Debugf("Skipping resource %s/%s (kind: %s) based on configuration", res.Namespace, res.Name, res.Kind)
}
}
return filtered
}
func (f *ResourceFilter) ShouldTrack(r *Resource) bool {
if f.config == nil {
return true
}
if len(f.config.TrackResources) > 0 {
return f.matchWhitelist(r)
}
kindLower := strings.ToLower(r.Kind)
if f.skipKinds[kindLower] {
return false
}
if len(f.trackKinds) > 0 {
return f.trackKinds[kindLower]
}
return true
}
func (f *ResourceFilter) matchWhitelist(r *Resource) bool {
for _, tr := range f.config.TrackResources {
// At least one field must be specified for a match
if tr.Kind == "" && tr.Name == "" && tr.Namespace == "" {
continue
}
if tr.Kind != "" && !strings.EqualFold(tr.Kind, r.Kind) {
continue
}
if tr.Name != "" && tr.Name != r.Name {
continue
}
if tr.Namespace != "" && tr.Namespace != r.Namespace {
continue
}
return true
}
return false
}

169
pkg/resource/filter_test.go Normal file
View File

@ -0,0 +1,169 @@
package resource
import (
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
)
func TestResourceFilter_Filter(t *testing.T) {
resources := []Resource{
{Kind: "Deployment", Name: "app1", Namespace: "default"},
{Kind: "StatefulSet", Name: "db1", Namespace: "default"},
{Kind: "ConfigMap", Name: "cm1", Namespace: "default"},
{Kind: "Secret", Name: "sec1", Namespace: "kube-system"},
}
tests := []struct {
name string
config *FilterConfig
expected int
}{
{
name: "nil filter returns all",
config: nil,
expected: 4,
},
{
name: "TrackKinds whitelist",
config: &FilterConfig{
TrackKinds: []string{"Deployment", "StatefulSet"},
},
expected: 2,
},
{
name: "SkipKinds blacklist",
config: &FilterConfig{
SkipKinds: []string{"ConfigMap", "Secret"},
},
expected: 2,
},
{
name: "TrackResources whitelist by kind and namespace",
config: &FilterConfig{
TrackResources: []Resource{
{Kind: "Deployment", Namespace: "default"},
},
},
expected: 1,
},
{
name: "TrackResources whitelist by name",
config: &FilterConfig{
TrackResources: []Resource{
{Name: "app1"},
{Name: "db1"},
},
},
expected: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filter := NewResourceFilter(tt.config, zap.NewNop().Sugar())
filtered := filter.Filter(resources)
assert.Equal(t, tt.expected, len(filtered))
})
}
}
func TestResourceFilter_ShouldTrack(t *testing.T) {
logger := zap.NewNop().Sugar()
tests := []struct {
name string
config *FilterConfig
resource *Resource
expected bool
}{
{
name: "nil config tracks all",
config: nil,
resource: &Resource{Kind: "Deployment", Name: "app1"},
expected: true,
},
{
name: "TrackKinds matches",
config: &FilterConfig{
TrackKinds: []string{"Deployment"},
},
resource: &Resource{Kind: "Deployment", Name: "app1"},
expected: true,
},
{
name: "TrackKinds no match",
config: &FilterConfig{
TrackKinds: []string{"StatefulSet"},
},
resource: &Resource{Kind: "Deployment", Name: "app1"},
expected: false,
},
{
name: "SkipKinds matches",
config: &FilterConfig{
SkipKinds: []string{"ConfigMap"},
},
resource: &Resource{Kind: "ConfigMap", Name: "cm1"},
expected: false,
},
{
name: "SkipKinds no match",
config: &FilterConfig{
SkipKinds: []string{"ConfigMap"},
},
resource: &Resource{Kind: "Deployment", Name: "app1"},
expected: true,
},
{
name: "TrackResources whitelist matches all criteria",
config: &FilterConfig{
TrackResources: []Resource{
{Kind: "Deployment", Name: "app1", Namespace: "default"},
},
},
resource: &Resource{Kind: "Deployment", Name: "app1", Namespace: "default"},
expected: true,
},
{
name: "TrackResources whitelist partial match",
config: &FilterConfig{
TrackResources: []Resource{
{Kind: "Deployment", Name: "app1"},
},
},
resource: &Resource{Kind: "Deployment", Name: "app1", Namespace: "other"},
expected: true,
},
{
name: "TrackResources whitelist no match",
config: &FilterConfig{
TrackResources: []Resource{
{Kind: "Deployment", Name: "app2"},
},
},
resource: &Resource{Kind: "Deployment", Name: "app1"},
expected: false,
},
{
name: "TrackResources takes precedence over TrackKinds",
config: &FilterConfig{
TrackResources: []Resource{
{Kind: "Deployment", Name: "app1"},
},
TrackKinds: []string{"StatefulSet"},
},
resource: &Resource{Kind: "Deployment", Name: "app1"},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filter := NewResourceFilter(tt.config, logger)
result := filter.ShouldTrack(tt.resource)
assert.Equal(t, tt.expected, result)
})
}
}

62
pkg/resource/parser.go Normal file
View File

@ -0,0 +1,62 @@
package resource
import (
"bytes"
"errors"
"fmt"
"io"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
)
func ParseManifest(manifest []byte, defaultNamespace string, logger *zap.SugaredLogger) ([]Resource, error) {
var resources []Resource
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifest), 4096)
for {
var obj unstructured.Unstructured
err := decoder.Decode(&obj)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("failed to decode manifest: %w", err)
}
if len(obj.Object) == 0 {
continue
}
kind := obj.GetKind()
if kind == "" {
if logger != nil {
logger.Debugf("Skipping resource without kind")
}
continue
}
name := obj.GetName()
if name == "" {
if logger != nil {
logger.Debugf("Skipping %s resource without name", kind)
}
continue
}
namespace := obj.GetNamespace()
if namespace == "" {
namespace = defaultNamespace
}
resources = append(resources, Resource{
Kind: kind,
Name: name,
Namespace: namespace,
})
}
return resources, nil
}

13
pkg/resource/types.go Normal file
View File

@ -0,0 +1,13 @@
package resource
type Resource struct {
Kind string
Name string
Namespace string
}
type FilterConfig struct {
TrackKinds []string
SkipKinds []string
TrackResources []Resource
}

View File

@ -1,18 +1,22 @@
package state
import (
"context"
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"time"
"github.com/helmfile/chartify"
"helm.sh/helm/v4/pkg/storage/driver"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/kubedog"
"github.com/helmfile/helmfile/pkg/remote"
"github.com/helmfile/helmfile/pkg/resource"
)
type Dependency struct {
@ -165,6 +169,10 @@ func (st *HelmState) appendSuppressOutputLineRegexFlags(flags []string, release
}
func (st *HelmState) appendWaitForJobsFlags(flags []string, release *ReleaseSpec, ops *SyncOpts) []string {
if st.shouldUseKubedog(release, ops) {
return flags
}
switch {
case release.WaitForJobs != nil && *release.WaitForJobs:
flags = append(flags, "--wait-for-jobs")
@ -173,10 +181,26 @@ func (st *HelmState) appendWaitForJobsFlags(flags []string, release *ReleaseSpec
case release.WaitForJobs == nil && st.HelmDefaults.WaitForJobs:
flags = append(flags, "--wait-for-jobs")
}
return flags
}
func (st *HelmState) shouldUseKubedog(release *ReleaseSpec, ops *SyncOpts) bool {
trackMode := release.TrackMode
if trackMode == "" && ops != nil && ops.TrackMode != "" {
trackMode = ops.TrackMode
}
if trackMode == "" {
trackMode = st.HelmDefaults.TrackMode
}
return trackMode == "kubedog"
}
func (st *HelmState) appendWaitFlags(flags []string, release *ReleaseSpec, ops *SyncOpts) []string {
if st.shouldUseKubedog(release, ops) {
return flags
}
switch {
case release.Wait != nil && *release.Wait:
flags = append(flags, "--wait")
@ -423,3 +447,181 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
return nil, clean, nil
}
func (st *HelmState) trackWithKubedog(ctx context.Context, release *ReleaseSpec, helm helmexec.Interface, ops *SyncOpts) error {
timeout := 5 * time.Minute
if release.TrackTimeout != nil && *release.TrackTimeout > 0 {
timeout = time.Duration(*release.TrackTimeout) * time.Second
} else if ops != nil && ops.TrackTimeout > 0 {
timeout = time.Duration(ops.TrackTimeout) * time.Second
}
trackLogs := release.TrackLogs != nil && *release.TrackLogs
if release.TrackLogs == nil && ops != nil {
trackLogs = ops.TrackLogs
}
filterConfig := &resource.FilterConfig{
TrackKinds: release.TrackKinds,
SkipKinds: release.SkipKinds,
TrackResources: convertTrackResources(release.TrackResources),
}
kubeContext := st.getKubeContext(release)
trackOpts := kubedog.NewTrackOptions().
WithTimeout(timeout).
WithLogs(trackLogs).
WithFilterConfig(filterConfig)
tracker, err := kubedog.NewTracker(&kubedog.TrackerConfig{
Logger: st.logger,
Namespace: release.Namespace,
KubeContext: kubeContext,
Kubeconfig: st.kubeconfig,
TrackOptions: trackOpts,
})
if err != nil {
return fmt.Errorf("failed to create kubedog tracker: %w", err)
}
resources, err := st.getReleaseResources(ctx, release, helm)
if err != nil {
return fmt.Errorf("failed to get release resources: %w", err)
}
if len(resources) == 0 {
st.logger.Infof("No trackable resources found for release %s", release.Name)
return nil
}
st.logger.Infof("Tracking %d resources from release %s with kubedog", len(resources), release.Name)
if err := tracker.TrackResources(ctx, resources); err != nil {
return fmt.Errorf("kubedog tracking failed for release %s: %w", release.Name, err)
}
return nil
}
func (st *HelmState) getReleaseResources(_ context.Context, release *ReleaseSpec, helm helmexec.Interface) ([]*resource.Resource, error) {
st.logger.Debugf("Getting resources for release %s", release.Name)
manifest, namespace, err := st.getReleaseManifest(release, helm)
if err != nil {
return nil, fmt.Errorf("failed to get release manifest: %w", err)
}
if len(manifest) == 0 {
st.logger.Infof("No manifest found for release %s", release.Name)
return nil, nil
}
defaultNs := namespace
if defaultNs == "" {
defaultNs = "default"
}
resources, err := resource.ParseManifest(manifest, defaultNs, st.logger)
if err != nil {
return nil, fmt.Errorf("failed to parse release resources from manifest: %w", err)
}
if len(resources) == 0 {
st.logger.Infof("No resources found in manifest for release %s", release.Name)
return nil, nil
}
st.logger.Infof("Found %d resources in manifest for release %s", len(resources), release.Name)
result := make([]*resource.Resource, len(resources))
for i := range resources {
result[i] = &resources[i]
}
return result, nil
}
func (st *HelmState) getReleaseManifest(release *ReleaseSpec, helm helmexec.Interface) ([]byte, string, error) {
var tempDir string
var err error
if st.tempDir != nil {
tempDir, err = st.tempDir("", "helmfile-template-")
} else {
tempDir, err = os.MkdirTemp("", "helmfile-template-")
}
if err != nil {
return nil, "", fmt.Errorf("failed to create temp directory: %w", err)
}
defer func() {
if err := os.RemoveAll(tempDir); err != nil {
st.logger.Warnf("Failed to remove temp directory %s: %v", tempDir, err)
}
}()
releaseCopy := *release
st.ApplyOverrides(&releaseCopy)
flags, files, err := st.flagsForTemplate(helm, &releaseCopy, 0, &TemplateOpts{})
if err != nil {
return nil, "", fmt.Errorf("failed to generate template flags: %w", err)
}
defer st.removeFiles(files)
flags = append(flags, "--output-dir", tempDir)
if err := helm.TemplateRelease(releaseCopy.Name, releaseCopy.ChartPathOrName(), flags...); err != nil {
return nil, "", fmt.Errorf("failed to run helm template: %w", err)
}
var manifest []byte
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(info.Name(), ".yaml") && !strings.HasSuffix(info.Name(), ".yml") {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}
if len(manifest) > 0 {
manifest = append(manifest, []byte("\n---\n")...)
}
manifest = append(manifest, content...)
return nil
})
if err != nil {
return nil, "", fmt.Errorf("failed to walk template output directory: %w", err)
}
return manifest, releaseCopy.Namespace, nil
}
func convertTrackResources(resources []TrackResourceSpec) []resource.Resource {
if len(resources) == 0 {
return nil
}
result := make([]resource.Resource, len(resources))
for i, r := range resources {
result[i] = resource.Resource{
Kind: r.Kind,
Name: r.Name,
Namespace: r.Namespace,
}
}
return result
}

View File

@ -2,7 +2,7 @@ package state
import (
"bytes"
"context"
gocontext "context"
"crypto/sha1"
"encoding/hex"
"errors"
@ -129,12 +129,18 @@ type HelmState struct {
valsRuntime vals.Evaluator
kubeconfig string
// RenderedValues is the helmfile-wide values that is `.Values`
// which is accessible from within the whole helmfile go template.
// Note that this is usually computed by DesiredStateLoader from ReleaseSetSpec.Env
RenderedValues map[string]any
}
func (st *HelmState) SetKubeconfig(kubeconfig string) {
st.kubeconfig = kubeconfig
}
// SubHelmfileSpec defines the subhelmfile path and options
type SubHelmfileSpec struct {
//path or glob pattern for the sub helmfiles
@ -226,6 +232,8 @@ type HelmSpec struct {
SyncReleaseLabels *bool `yaml:"syncReleaseLabels,omitempty"`
// TakeOwnership is true if the helmfile should take ownership of the release
TakeOwnership *bool `yaml:"takeOwnership,omitempty"`
// TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources
TrackMode string `yaml:"trackMode,omitempty"`
}
// RepositorySpec that defines values for a helm repo
@ -254,7 +262,7 @@ type Inherit struct {
type Inherits []Inherit
// ReleaseSpec defines the structure of a helm release
// ReleaseSpec defines the configuration for a Helm release managed by helmfile.
type ReleaseSpec struct {
// Chart is the name of the chart being installed to create this release
Chart string `yaml:"chart,omitempty"`
@ -444,8 +452,27 @@ type ReleaseSpec struct {
DeleteTimeout *int `yaml:"deleteTimeout,omitempty"`
// SyncReleaseLabels is true if the release labels should be synced with the helmfile labels
SyncReleaseLabels *bool `yaml:"syncReleaseLabels,omitempty"`
// TakeOwnership is true if the release should take ownership of the resources
// TakeOwnership is true if release should take ownership of resources
TakeOwnership *bool `yaml:"takeOwnership,omitempty"`
// TrackMode specifies whether to use 'helm' or 'kubedog' for tracking resources
TrackMode string `yaml:"trackMode,omitempty"`
// TrackTimeout specifies timeout for kubedog tracking (in seconds)
TrackTimeout *int `yaml:"trackTimeout,omitempty"`
// TrackLogs enables log streaming with kubedog
TrackLogs *bool `yaml:"trackLogs,omitempty"`
// TrackKinds is a whitelist of resource kinds to track
TrackKinds []string `yaml:"trackKinds,omitempty"`
// SkipKinds is a blacklist of resource kinds to skip tracking
SkipKinds []string `yaml:"skipKinds,omitempty"`
// TrackResources is a whitelist of specific resources to track
TrackResources []TrackResourceSpec `yaml:"trackResources,omitempty"`
}
// TrackResourceSpec specifies a resource to track
type TrackResourceSpec struct {
Kind string `yaml:"kind,omitempty"`
Name string `yaml:"name,omitempty"`
Namespace string `yaml:"namespace,omitempty"`
}
func (r *Inherits) UnmarshalYAML(unmarshal func(any) error) error {
@ -870,6 +897,9 @@ type SyncOpts struct {
SyncArgs string
HideNotes bool
TakeOwnership bool
TrackMode string
TrackTimeout int
TrackLogs bool
}
type SyncOpt interface{ Apply(*SyncOpts) }
@ -1095,6 +1125,11 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
}
} else if release.UpdateStrategy == UpdateStrategyReinstallIfForbidden {
relErr = st.performSyncOrReinstallOfRelease(affectedReleases, helm, context, release, chart, m, flags...)
if relErr == nil && st.shouldUseKubedog(release, opts) {
if trackErr := st.trackWithKubedog(gocontext.Background(), release, helm, opts); trackErr != nil {
st.logger.Warnf("kubedog tracking failed for release %s: %v", release.Name, trackErr)
}
}
} else {
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
m.Lock()
@ -1111,6 +1146,12 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
} else {
release.installedVersion = installedVersion
}
if st.shouldUseKubedog(release, opts) {
if trackErr := st.trackWithKubedog(gocontext.Background(), release, helm, opts); trackErr != nil {
st.logger.Warnf("kubedog tracking failed for release %s: %v", release.Name, trackErr)
}
}
}
}
@ -4910,7 +4951,7 @@ func (st *HelmState) acquireSharedLock(result *chartLockResult, chartPath string
result.inProcessMutex = st.getNamedRWMutex(chartPath)
result.inProcessMutex.RLock()
ctx, cancel := context.WithTimeout(context.Background(), lockTimeout)
ctx, cancel := gocontext.WithTimeout(gocontext.Background(), lockTimeout)
defer cancel()
locked, err := result.fileLock.TryRLockContext(ctx, 500*time.Millisecond)
@ -4945,7 +4986,7 @@ func (st *HelmState) acquireExclusiveLock(result *chartLockResult, chartPath str
var lockErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), lockTimeout)
ctx, cancel := gocontext.WithTimeout(gocontext.Background(), lockTimeout)
locked, lockErr = result.fileLock.TryLockContext(ctx, 500*time.Millisecond)
cancel()

View File

@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-6884949b8b",
want: "foo-values-dd88b94b8",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-58f57b794f",
want: "foo-values-6fb7bbb95f",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-6b6b884cc9",
want: "foo-values-56d84c9897",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-85494c4677",
want: "foo-values-6644fc9d47",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-9d65c65f",
want: "bar-values-859cd849bf",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-84b69bb989",
want: "myns-foo-values-86d544f7f9",
})
for id, n := range ids {

View File

@ -0,0 +1,90 @@
---
# Source: __workingdir__/testdata/snapshot/kubedog_tracking/input.yaml.gotmpl
filepath: input.yaml.gotmpl
helmBinary: helm
kustomizeBinary: kustomize
releases:
- chart: ../../charts/raw-0.1.0
name: kubedog-baseline
labels:
chart: raw-0.1.0
name: kubedog-baseline
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-baseline
namespace: {{ .Release.Namespace }}
data:
baseline: value
trackMode: kubedog
- chart: ../../charts/raw-0.1.0
name: kubedog-with-timeout
labels:
chart: raw-0.1.0
name: kubedog-with-timeout
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-timeout
namespace: {{ .Release.Namespace }}
data:
timeout: value
trackMode: kubedog
trackTimeout: 300
- chart: ../../charts/raw-0.1.0
name: kubedog-with-logs
labels:
chart: raw-0.1.0
name: kubedog-with-logs
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-logs
namespace: {{ .Release.Namespace }}
data:
logs: value
trackMode: kubedog
trackLogs: true
- chart: ../../charts/raw-0.1.0
name: kubedog-with-whitelist
labels:
chart: raw-0.1.0
name: kubedog-with-whitelist
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-tracked
namespace: {{ .Release.Namespace }}
data:
tracked: value
- |
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-secret
namespace: {{ .Release.Namespace }}
type: Opaque
data:
secret: dmFsdWU=
trackMode: kubedog
trackKinds:
- ConfigMap
templates: {}
renderedvalues: {}

View File

@ -0,0 +1,90 @@
---
# Source: __workingdir__/testdata/snapshot/kubedog_tracking/input.yaml.gotmpl
filepath: input.yaml.gotmpl
helmBinary: helm
kustomizeBinary: kustomize
releases:
- chart: ../../charts/raw-0.1.0
name: kubedog-baseline
labels:
chart: raw-0.1.0
name: kubedog-baseline
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-baseline
namespace: {{ .Release.Namespace }}
data:
baseline: value
trackMode: kubedog
- chart: ../../charts/raw-0.1.0
name: kubedog-with-timeout
labels:
chart: raw-0.1.0
name: kubedog-with-timeout
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-timeout
namespace: {{ .Release.Namespace }}
data:
timeout: value
trackMode: kubedog
trackTimeout: 300
- chart: ../../charts/raw-0.1.0
name: kubedog-with-logs
labels:
chart: raw-0.1.0
name: kubedog-with-logs
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-logs
namespace: {{ .Release.Namespace }}
data:
logs: value
trackMode: kubedog
trackLogs: true
- chart: ../../charts/raw-0.1.0
name: kubedog-with-whitelist
labels:
chart: raw-0.1.0
name: kubedog-with-whitelist
namespace: ""
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-tracked
namespace: {{ .Release.Namespace }}
data:
tracked: value
- |
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-secret
namespace: {{ .Release.Namespace }}
type: Opaque
data:
secret: dmFsdWU=
trackMode: kubedog
trackKinds:
- ConfigMap
templates: {}
renderedvalues: {}

View File

@ -0,0 +1,69 @@
releases:
- name: kubedog-baseline
chart: ../../charts/raw-0.1.0
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}-baseline
namespace: {{`{{ .Release.Namespace }}`}}
data:
baseline: value
trackMode: kubedog
- name: kubedog-with-timeout
chart: ../../charts/raw-0.1.0
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}-timeout
namespace: {{`{{ .Release.Namespace }}`}}
data:
timeout: value
trackMode: kubedog
trackTimeout: 300
- name: kubedog-with-logs
chart: ../../charts/raw-0.1.0
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}-logs
namespace: {{`{{ .Release.Namespace }}`}}
data:
logs: value
trackMode: kubedog
trackLogs: true
- name: kubedog-with-whitelist
chart: ../../charts/raw-0.1.0
values:
- templates:
- |
apiVersion: v1
kind: ConfigMap
metadata:
name: {{`{{ .Release.Name }}`}}-tracked
namespace: {{`{{ .Release.Namespace }}`}}
data:
tracked: value
- |
apiVersion: v1
kind: Secret
metadata:
name: {{`{{ .Release.Name }}`}}-secret
namespace: {{`{{ .Release.Namespace }}`}}
type: Opaque
data:
secret: dmFsdWU=
trackMode: kubedog
trackKinds:
- ConfigMap

View File

@ -135,6 +135,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
. ${dir}/test-cases/issue-2418.sh
. ${dir}/test-cases/issue-2424-sequential-values-paths.sh
. ${dir}/test-cases/issue-2431.sh
. ${dir}/test-cases/kubedog-tracking.sh
# ALL DONE -----------------------------------------------------------------------------------------------------------

View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
test_start "kubedog-tracking - resource tracking with kubedog integration"
kubedog_case_dir="${cases_dir}/kubedog-tracking"
config_file="helmfile.yaml.gotmpl"
info "Testing kubedog integration with httpbin chart"
# Test 1: Basic sync with kubedog tracking
info "Syncing release with basic kubedog tracking"
${helmfile} -f "${kubedog_case_dir}/${config_file}" -l name=httpbin-basic sync
code=$?
[ "${code}" -eq 0 ] || fail "unexpected exit code returned by helmfile sync: ${code}"
wait_deploy_ready httpbin-basic-httpbin
info "Verifying httpbin-basic deployment is running"
${kubectl} get deployment httpbin-basic-httpbin -n "${test_ns}" || fail "httpbin-basic deployment not found"
# Test 2: Sync with whitelist filtering
info "Syncing release with whitelist filtering"
${helmfile} -f "${kubedog_case_dir}/${config_file}" -l name=httpbin-with-whitelist sync
code=$?
[ "${code}" -eq 0 ] || fail "unexpected exit code returned by helmfile sync with whitelist: ${code}"
wait_deploy_ready httpbin-with-whitelist-httpbin
info "Verifying httpbin-with-whitelist deployment is running"
${kubectl} get deployment httpbin-with-whitelist-httpbin -n "${test_ns}" || fail "httpbin-with-whitelist deployment not found"
# Test 3: Sync with specific resource tracking
info "Syncing release with specific resource tracking"
${helmfile} -f "${kubedog_case_dir}/${config_file}" -l name=httpbin-with-resources sync
code=$?
[ "${code}" -eq 0 ] || fail "unexpected exit code returned by helmfile sync with resource tracking: ${code}"
wait_deploy_ready httpbin-with-resources-httpbin
info "Verifying httpbin-with-resources deployment is running"
${kubectl} get deployment httpbin-with-resources-httpbin -n "${test_ns}" || fail "httpbin-with-resources deployment not found"
# Test 4: Apply all releases with kubedog via CLI flag
info "Testing apply with kubedog CLI flag"
${helmfile} -f "${kubedog_case_dir}/${config_file}" apply --track-mode kubedog --track-timeout 60
code=$?
[ "${code}" -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}"
# Test 5: Cleanup
info "Destroying all releases"
${helmfile} -f "${kubedog_case_dir}/${config_file}" destroy
code=$?
[ "${code}" -eq 0 ] || fail "unexpected exit code returned by helmfile destroy: ${code}"
info "kubedog integration test completed successfully"
test_pass "kubedog-tracking"

View File

@ -0,0 +1,52 @@
# Kubedog Integration Test
This test validates the kubedog resource tracking integration with Helmfile.
## What it tests
1. **Basic kubedog tracking**: Deploys httpbin with `trackMode: kubedog` enabled
2. **Whitelist filtering**: Uses `trackKinds` to only track Deployment resources
3. **Specific resource tracking**: Uses `trackResources` to track specific resources by name
4. **CLI flags usage**: Tests `--track-mode` and `--track-timeout` flags
5. **Cleanup**: Ensures all releases are properly deleted
## Prerequisites
- Kubernetes cluster (minikube for local testing)
- Helm 3.x installed
- kubedog library integrated (built into Helmfile)
- kubectl configured to access the cluster
## Test Cases
### httpbin-basic
- Simple deployment with kubedog tracking enabled
- `trackMode: kubedog`
- `trackTimeout: 60` seconds
- `trackLogs: false`
### httpbin-with-whitelist
- Deployment with resource kind whitelist
- Only tracks `Deployment` resources
- Skips `ConfigMap` and `Secret` resources
### httpbin-with-resources
- Deployment with specific resource tracking
- Tracks only the deployment by name and kind
## Running the test
```bash
# Run all integration tests including kubedog
./test/integration/run.sh
# Run only kubedog test (if supported by your test framework)
# Note: Currently all tests run together via run.sh
```
## Expected behavior
1. All three httpbin deployments should be created successfully
2. Kubedog should track the resources during deployment
3. Deployments should reach ready state
4. All releases should be cleaned up after tests

View File

@ -0,0 +1,34 @@
releases:
- name: httpbin-basic
namespace: {{ .Namespace }}
chart: ../../charts/httpbin
set:
- name: ingress.enabled
value: false
trackMode: kubedog
trackTimeout: 60
trackLogs: false
- name: httpbin-with-whitelist
namespace: {{ .Namespace }}
chart: ../../charts/httpbin
set:
- name: ingress.enabled
value: false
trackMode: kubedog
trackKinds:
- Deployment
skipKinds:
- ConfigMap
- Secret
- name: httpbin-with-resources
namespace: {{ .Namespace }}
chart: ../../charts/httpbin
set:
- name: ingress.enabled
value: false
trackMode: kubedog
trackResources:
- kind: Deployment
name: httpbin-with-resources-httpbin