helmfile/pkg/cluster/release.go

276 lines
6.6 KiB
Go

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,
}
}