feat: implement getReleaseResources for kubedog tracking

Implement the getReleaseResources method to detect Kubernetes resources
from Helm chart manifests for kubedog tracking.

This implementation:

1. Creates a new pkg/cluster package with:
   - Resource struct for resource metadata (kind, name, namespace)
   - ReleaseResources struct for holding all resources in a release
   - TrackConfig for custom tracking configuration
   - GetReleaseResourcesFromManifest() to parse helm template output
   - Helper functions for filtering trackable vs static resources
   - Helper functions for getting Helm release labels/annotations

2. Implements getReleaseResources() method that:
   - Runs helm template to generate Kubernetes manifests
   - Reads all YAML files from template output directory
   - Combines files into a single manifest with document separators
   - Parses manifest using k8s.io/apimachinery unstructured decoder
   - Converts cluster.Resource to kubedog.ResourceSpec for tracking
   - Cleans up temporary files after parsing

3. Removes unused functions from helmx.go:
   - parseResourceKindAndName() - replaced by manifest parsing
   - isTrackableResourceKind() - moved to cluster package

Benefits:
- Works offline without Kubernetes cluster connection
- Faster than API-based resource detection
- More reliable and deterministic
- Enables kubedog tracking with proper resource filtering

Fixes: #2383
Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2026-01-25 19:17:33 +08:00
parent 9cad1ab490
commit bc8ed87397
2 changed files with 358 additions and 24 deletions

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

@ -0,0 +1,275 @@
package cluster
import (
"bytes"
"fmt"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/yaml"
)
type Resource struct {
Kind string
Name string
Namespace string
Manifest string
}
type ReleaseResources struct {
ReleaseName string
Namespace string
Resources []Resource
}
type TrackConfig struct {
TrackKinds []string
SkipKinds []string
CustomTrackableKinds []string
CustomStaticKinds []string
}
func GetReleaseResourcesFromManifest(manifest []byte, releaseName, releaseNamespace string) (*ReleaseResources, error) {
return GetReleaseResourcesFromManifestWithLogger(nil, manifest, releaseName, releaseNamespace, nil)
}
func GetReleaseResourcesFromManifestWithConfig(manifest []byte, releaseName, releaseNamespace string, config *TrackConfig) (*ReleaseResources, error) {
return GetReleaseResourcesFromManifestWithLogger(nil, manifest, releaseName, releaseNamespace, config)
}
func GetReleaseResourcesFromManifestWithLogger(logger *zap.SugaredLogger, manifest []byte, releaseName, releaseNamespace string, config *TrackConfig) (*ReleaseResources, error) {
resources, err := parseManifest(manifest, logger)
if err != nil {
return nil, fmt.Errorf("failed to parse manifest: %w", err)
}
if len(resources) == 0 {
if logger != nil {
logger.Debugf("No resources found in manifest for release %s", releaseName)
}
return &ReleaseResources{
ReleaseName: releaseName,
Namespace: releaseNamespace,
Resources: resources,
}, nil
}
if config != nil {
filteredResources := filterResourcesByConfig(resources, config, logger)
if logger != nil {
logger.Infof("Found %d resources in manifest for release %s (filtered from %d total)", len(filteredResources), releaseName, len(resources))
for _, res := range filteredResources {
logger.Debugf(" - %s/%s in namespace %s", res.Kind, res.Name, res.Namespace)
}
}
return &ReleaseResources{
ReleaseName: releaseName,
Namespace: releaseNamespace,
Resources: filteredResources,
}, nil
}
if logger != nil {
logger.Infof("Found %d resources in manifest for release %s", len(resources), releaseName)
for _, res := range resources {
logger.Debugf(" - %s/%s in namespace %s", res.Kind, res.Name, res.Namespace)
}
}
return &ReleaseResources{
ReleaseName: releaseName,
Namespace: releaseNamespace,
Resources: resources,
}, nil
}
func filterResourcesByConfig(resources []Resource, config *TrackConfig, logger *zap.SugaredLogger) []Resource {
var filtered []Resource
for _, res := range resources {
if shouldSkipResource(res.Kind, config, logger) {
if logger != nil {
logger.Debugf("Skipping resource %s/%s (kind: %s) based on configuration", res.Kind, res.Name, res.Kind)
}
continue
}
filtered = append(filtered, res)
}
return filtered
}
func shouldSkipResource(kind string, config *TrackConfig, logger *zap.SugaredLogger) bool {
if len(config.TrackKinds) > 0 {
shouldTrack := false
for _, trackKind := range config.TrackKinds {
if kind == trackKind {
shouldTrack = true
break
}
}
if !shouldTrack {
if logger != nil {
logger.Debugf("Resource kind %s is not in TrackKinds list, skipping", kind)
}
return true
}
}
if len(config.SkipKinds) > 0 {
for _, skipKind := range config.SkipKinds {
if kind == skipKind {
if logger != nil {
logger.Debugf("Resource kind %s is in SkipKinds list, skipping", kind)
}
return true
}
}
}
return false
}
func parseManifest(manifest []byte, 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 err.Error() == "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 = "default"
}
manifestBytes, err := yaml.Marshal(obj.Object)
if err != nil {
if logger != nil {
logger.Debugf("Failed to marshal %s/%s: %v", kind, name, err)
}
continue
}
res := Resource{
Kind: kind,
Name: name,
Namespace: namespace,
Manifest: string(manifestBytes),
}
resources = append(resources, res)
}
return resources, nil
}
func IsTrackableKind(kind string) bool {
trackableKinds := map[string]bool{
"Deployment": true,
"StatefulSet": true,
"DaemonSet": true,
"Job": true,
"Pod": true,
"ReplicaSet": true,
}
return trackableKinds[kind]
}
func IsTrackableKindWithConfig(kind string, config *TrackConfig) bool {
if config == nil {
return IsTrackableKind(kind)
}
if len(config.CustomTrackableKinds) > 0 {
for _, customKind := range config.CustomTrackableKinds {
if kind == customKind {
return true
}
}
return false
}
return IsTrackableKind(kind)
}
func IsStaticKind(kind string) bool {
staticKinds := map[string]bool{
"Service": true,
"ConfigMap": true,
"Secret": true,
"PersistentVolumeClaim": true,
"PersistentVolume": true,
"StorageClass": true,
"Namespace": true,
"ResourceQuota": true,
"LimitRange": true,
"PriorityClass": true,
"ServiceAccount": true,
"Role": true,
"RoleBinding": true,
"ClusterRole": true,
"ClusterRoleBinding": true,
"NetworkPolicy": true,
"Ingress": true,
"CustomResourceDefinition": true,
}
return staticKinds[kind]
}
func IsStaticKindWithConfig(kind string, config *TrackConfig) bool {
if config == nil {
return IsStaticKind(kind)
}
if len(config.CustomStaticKinds) > 0 {
for _, customKind := range config.CustomStaticKinds {
if kind == customKind {
return true
}
}
return false
}
return IsStaticKind(kind)
}
func GetHelmReleaseLabels(releaseName, releaseNamespace string) map[string]string {
return map[string]string{
"meta.helm.sh/release-name": releaseName,
"meta.helm.sh/release-namespace": releaseNamespace,
}
}
func GetHelmReleaseAnnotations(releaseName string) map[string]string {
return map[string]string{
"meta.helm.sh/release-name": releaseName,
}
}

View File

@ -13,6 +13,7 @@ import (
"github.com/helmfile/chartify"
"helm.sh/helm/v4/pkg/storage/driver"
"github.com/helmfile/helmfile/pkg/cluster"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/kubedog"
"github.com/helmfile/helmfile/pkg/remote"
@ -503,32 +504,90 @@ func (st *HelmState) trackWithKubeDog(ctx context.Context, release *ReleaseSpec,
func (st *HelmState) getReleaseResources(ctx context.Context, release *ReleaseSpec, helm helmexec.Interface) ([]*kubedog.ResourceSpec, error) {
st.logger.Debugf("Getting resources for release %s", release.Name)
// TODO: Implement resource detection from manifest
// For now, return empty list to avoid compilation errors
return nil, nil
}
func (st *HelmState) parseResourceKindAndName(line string) (string, string, error) {
parts := strings.Fields(line)
if len(parts) < 3 {
return "", "", nil
manifest, err := st.getReleaseManifest(ctx, release, helm)
if err != nil {
return nil, fmt.Errorf("failed to get release manifest: %w", err)
}
kind := strings.TrimSpace(parts[0])
name := strings.TrimSpace(parts[1])
return kind, name, nil
}
func (st *HelmState) isTrackableResourceKind(kind string) bool {
trackableKinds := map[string]bool{
"Deployment": true,
"StatefulSet": true,
"DaemonSet": true,
"Job": true,
"Pod": true,
"ReplicaSet": true,
if len(manifest) == 0 {
st.logger.Infof("No manifest found for release %s", release.Name)
return nil, nil
}
return trackableKinds[kind]
releaseResources, err := cluster.GetReleaseResourcesFromManifest(manifest, release.Name, release.Namespace)
if err != nil {
return nil, fmt.Errorf("failed to parse release resources from manifest: %w", err)
}
var resources []*kubedog.ResourceSpec
for _, res := range releaseResources.Resources {
resources = append(resources, &kubedog.ResourceSpec{
Name: res.Name,
Namespace: res.Namespace,
Kind: res.Kind,
})
}
return resources, nil
}
func (st *HelmState) getReleaseManifest(ctx context.Context, release *ReleaseSpec, helm helmexec.Interface) ([]byte, error) {
tempDir, err := st.tempDir("", "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)
}
}()
st.ApplyOverrides(release)
flags, files, err := st.flagsForTemplate(helm, release, 0, &TemplateOpts{})
defer st.removeFiles(files)
if err != nil {
return nil, fmt.Errorf("failed to generate template flags: %w", err)
}
flags = append(flags, "--output-dir", tempDir)
if err := helm.TemplateRelease(release.Name, release.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, nil
}