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:
parent
9cad1ab490
commit
bc8ed87397
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue