diff --git a/cmd/apply.go b/cmd/apply.go index c47ce83d..93213b2a 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -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 } diff --git a/cmd/sync.go b/cmd/sync.go index 6cc31975..0e7b795b 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -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 } diff --git a/docs/advanced-features.md b/docs/advanced-features.md index dba1123f..1c05c9d7 100644 --- a/docs/advanced-features.md +++ b/docs/advanced-features.md @@ -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: diff --git a/go.mod b/go.mod index 01629db0..1c4567c9 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 591a8625..98666cb4 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/app/app.go b/pkg/app/app.go index 459d95a3..021eab95 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -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) })) diff --git a/pkg/app/app_sync_test.go b/pkg/app/app_sync_test.go index 948e8c06..200e8f5c 100644 --- a/pkg/app/app_sync_test.go +++ b/pkg/app/app_sync_test.go @@ -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, diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 07e9e6f1..ffc500fb 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -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 diff --git a/pkg/app/config.go b/pkg/app/config.go index 8465f1dd..03bdc627 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -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 diff --git a/pkg/cluster/release.go b/pkg/cluster/release.go new file mode 100644 index 00000000..de0d8c59 --- /dev/null +++ b/pkg/cluster/release.go @@ -0,0 +1,11 @@ +package cluster + +import ( + "github.com/helmfile/helmfile/pkg/resource" +) + +type ( + Resource = resource.Resource + FilterConfig = resource.FilterConfig + ResourceFilter = resource.ResourceFilter +) diff --git a/pkg/config/apply.go b/pkg/config/apply.go index cfdd3451..dbf36ce3 100644 --- a/pkg/config/apply.go +++ b/pkg/config/apply.go @@ -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() +} diff --git a/pkg/config/sync.go b/pkg/config/sync.go index b59701f1..87a005b6 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -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() +} diff --git a/pkg/kubedog/options.go b/pkg/kubedog/options.go new file mode 100644 index 00000000..36c16ad2 --- /dev/null +++ b/pkg/kubedog/options.go @@ -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 +} diff --git a/pkg/kubedog/tracker.go b/pkg/kubedog/tracker.go new file mode 100644 index 00000000..752038b8 --- /dev/null +++ b/pkg/kubedog/tracker.go @@ -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 +} diff --git a/pkg/kubedog/tracker_test.go b/pkg/kubedog/tracker_test.go new file mode 100644 index 00000000..b09a96c2 --- /dev/null +++ b/pkg/kubedog/tracker_test.go @@ -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) +} diff --git a/pkg/resource/filter.go b/pkg/resource/filter.go new file mode 100644 index 00000000..bf2cc812 --- /dev/null +++ b/pkg/resource/filter.go @@ -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 +} diff --git a/pkg/resource/filter_test.go b/pkg/resource/filter_test.go new file mode 100644 index 00000000..16da811a --- /dev/null +++ b/pkg/resource/filter_test.go @@ -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) + }) + } +} diff --git a/pkg/resource/parser.go b/pkg/resource/parser.go new file mode 100644 index 00000000..19d9820f --- /dev/null +++ b/pkg/resource/parser.go @@ -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 +} diff --git a/pkg/resource/types.go b/pkg/resource/types.go new file mode 100644 index 00000000..4782e63c --- /dev/null +++ b/pkg/resource/types.go @@ -0,0 +1,13 @@ +package resource + +type Resource struct { + Kind string + Name string + Namespace string +} + +type FilterConfig struct { + TrackKinds []string + SkipKinds []string + TrackResources []Resource +} diff --git a/pkg/state/helmx.go b/pkg/state/helmx.go index 1513c617..6d82327c 100644 --- a/pkg/state/helmx.go +++ b/pkg/state/helmx.go @@ -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 +} diff --git a/pkg/state/state.go b/pkg/state/state.go index b3a60c9b..49b5d80b 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -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() diff --git a/pkg/state/temp_test.go b/pkg/state/temp_test.go index f8a35579..14aff96a 100644 --- a/pkg/state/temp_test.go +++ b/pkg/state/temp_test.go @@ -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 { diff --git a/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/gopkg.in-yaml.v2-output.yaml b/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/gopkg.in-yaml.v2-output.yaml new file mode 100644 index 00000000..af4d3dd0 --- /dev/null +++ b/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/gopkg.in-yaml.v2-output.yaml @@ -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: {} diff --git a/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/gopkg.in-yaml.v3-output.yaml b/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/gopkg.in-yaml.v3-output.yaml new file mode 100644 index 00000000..d95a0e25 --- /dev/null +++ b/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/gopkg.in-yaml.v3-output.yaml @@ -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: {} diff --git a/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/input.yaml.gotmpl b/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/input.yaml.gotmpl new file mode 100644 index 00000000..37ba69e1 --- /dev/null +++ b/test/e2e/template/helmfile/testdata/snapshot/kubedog_tracking/input.yaml.gotmpl @@ -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 diff --git a/test/integration/run.sh b/test/integration/run.sh index aca275b9..8b4767e9 100755 --- a/test/integration/run.sh +++ b/test/integration/run.sh @@ -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 ----------------------------------------------------------------------------------------------------------- diff --git a/test/integration/test-cases/kubedog-tracking.sh b/test/integration/test-cases/kubedog-tracking.sh new file mode 100755 index 00000000..695c0572 --- /dev/null +++ b/test/integration/test-cases/kubedog-tracking.sh @@ -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" diff --git a/test/integration/test-cases/kubedog-tracking/README.md b/test/integration/test-cases/kubedog-tracking/README.md new file mode 100644 index 00000000..2d540932 --- /dev/null +++ b/test/integration/test-cases/kubedog-tracking/README.md @@ -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 diff --git a/test/integration/test-cases/kubedog-tracking/helmfile.yaml.gotmpl b/test/integration/test-cases/kubedog-tracking/helmfile.yaml.gotmpl new file mode 100644 index 00000000..c311ca8a --- /dev/null +++ b/test/integration/test-cases/kubedog-tracking/helmfile.yaml.gotmpl @@ -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