292 lines
8.3 KiB
Go
292 lines
8.3 KiB
Go
package sdk
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/segmentio/encoding/json"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
)
|
|
|
|
const (
|
|
Unknown = "Unknown"
|
|
Healthy = "Healthy"
|
|
Unhealthy = "Unhealthy"
|
|
Progressing = "Progressing"
|
|
)
|
|
|
|
// ResourceStatus represents a generic status for any Kubernetes resource.
|
|
type ResourceStatus struct {
|
|
// Phase is a simple, high-level summary of where the resource is in its lifecycle.
|
|
Phase string `json:"phase,omitempty"`
|
|
|
|
// HealthSummary represents the summarized health status of the resource
|
|
HealthSummary *HealthCondition `json:"healthSummary,omitempty"`
|
|
|
|
// Reason is a brief CamelCase string containing the reason for the resource's current status.
|
|
Reason string `json:"reason,omitempty"`
|
|
|
|
// Message is a human-readable description of the current status.
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// HealthCondition represents a summarized health condition for a resource
|
|
type HealthCondition struct {
|
|
Status string `json:"status,omitempty"`
|
|
Reason string `json:"reason,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// parseResources returns a list of resources with additional status information, in a consistent format.
|
|
func parseResources(resourceTypesLists map[string][]runtime.Object) ([]*unstructured.Unstructured, error) {
|
|
flattenedResources := flattenResources(resourceTypesLists)
|
|
|
|
resourcesInfo := []*unstructured.Unstructured{}
|
|
for _, resource := range flattenedResources {
|
|
info, err := getResourceInfo(resource)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resourcesInfo = append(resourcesInfo, info)
|
|
}
|
|
|
|
return resourcesInfo, nil
|
|
}
|
|
|
|
func getResourceInfo(obj runtime.Object) (*unstructured.Unstructured, error) {
|
|
data, err := json.Marshal(obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := &unstructured.Unstructured{}
|
|
err = json.Unmarshal(data, res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
status, conditions, err := extractStatus(res)
|
|
if err == nil {
|
|
summarizeStatus(status, conditions, res.GetName(), res.GetNamespace(), err)
|
|
applyStatusToResource(res, status)
|
|
}
|
|
|
|
// only keep metadata, kind and status (other fields are not needed)
|
|
res.Object = map[string]any{
|
|
"metadata": res.Object["metadata"],
|
|
"kind": res.Object["kind"],
|
|
"status": res.Object["status"],
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// extractStatus extracts the status from an unstructured resource
|
|
func extractStatus(res *unstructured.Unstructured) (*ResourceStatus, []metav1.Condition, error) {
|
|
statusMap, found, err := unstructured.NestedMap(res.Object, "status")
|
|
if !found || err != nil {
|
|
return &ResourceStatus{}, nil, nil
|
|
}
|
|
|
|
// Extract basic status fields
|
|
phase, _, _ := unstructured.NestedString(statusMap, "phase")
|
|
reason, _, _ := unstructured.NestedString(statusMap, "reason")
|
|
message, _, _ := unstructured.NestedString(statusMap, "message")
|
|
|
|
// Extract conditions for analysis
|
|
conditions := []metav1.Condition{}
|
|
conditionsData, found, _ := unstructured.NestedSlice(statusMap, "conditions")
|
|
if found {
|
|
for _, condData := range conditionsData {
|
|
condMap, ok := condData.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
cond := metav1.Condition{}
|
|
if typeStr, ok := condMap["type"].(string); ok {
|
|
cond.Type = typeStr
|
|
}
|
|
if statusStr, ok := condMap["status"].(string); ok {
|
|
cond.Status = metav1.ConditionStatus(statusStr)
|
|
}
|
|
if reasonStr, ok := condMap["reason"].(string); ok {
|
|
cond.Reason = reasonStr
|
|
}
|
|
if msgStr, ok := condMap["message"].(string); ok {
|
|
cond.Message = msgStr
|
|
}
|
|
if timeStr, ok := condMap["lastTransitionTime"].(string); ok {
|
|
t, _ := time.Parse(time.RFC3339, timeStr)
|
|
cond.LastTransitionTime = metav1.Time{Time: t}
|
|
}
|
|
|
|
conditions = append(conditions, cond)
|
|
}
|
|
}
|
|
|
|
return &ResourceStatus{
|
|
Phase: phase,
|
|
Reason: reason,
|
|
Message: message,
|
|
}, conditions, nil
|
|
}
|
|
|
|
// summarizeStatus creates a health summary based on resource status and conditions
|
|
func summarizeStatus(status *ResourceStatus, conditions []metav1.Condition, name string, namespace string, err error) *ResourceStatus {
|
|
healthSummary := &HealthCondition{
|
|
Status: Unknown,
|
|
Reason: status.Reason,
|
|
Message: status.Message,
|
|
}
|
|
|
|
// Handle error case first
|
|
if err != nil {
|
|
healthSummary.Reason = "ErrorGettingStatus"
|
|
healthSummary.Message = err.Error()
|
|
status.HealthSummary = healthSummary
|
|
return status
|
|
}
|
|
|
|
// Handle phase-based status
|
|
switch status.Phase {
|
|
case "Error":
|
|
healthSummary.Status = Unhealthy
|
|
healthSummary.Reason = status.Phase
|
|
case "Running":
|
|
healthSummary.Status = Healthy
|
|
healthSummary.Reason = status.Phase
|
|
case "Pending":
|
|
healthSummary.Status = Progressing
|
|
healthSummary.Reason = status.Phase
|
|
case "Failed":
|
|
healthSummary.Status = Unhealthy
|
|
healthSummary.Reason = status.Phase
|
|
case "Available", "Active", "Established", "Bound", "Ready", "Succeeded":
|
|
healthSummary.Status = Healthy
|
|
healthSummary.Reason = status.Phase
|
|
case "":
|
|
// Empty phase - check conditions or default to "Exists"
|
|
if len(conditions) > 0 {
|
|
analyzeConditions(conditions, healthSummary)
|
|
} else {
|
|
healthSummary.Status = Healthy
|
|
healthSummary.Reason = "Exists"
|
|
}
|
|
default:
|
|
log.Warn().
|
|
Str("context", "HelmClient").
|
|
Str("namespace", namespace).
|
|
Str("name", name).
|
|
Str("phase", status.Phase).
|
|
Msg("Unhandled status")
|
|
healthSummary.Reason = status.Phase
|
|
}
|
|
|
|
// Set message from first condition if available
|
|
if len(conditions) > 0 && healthSummary.Message == "" {
|
|
healthSummary.Message = conditions[0].Message
|
|
}
|
|
|
|
status.HealthSummary = healthSummary
|
|
return status
|
|
}
|
|
|
|
// analyzeConditions determines resource health based on standard condition types
|
|
func analyzeConditions(conditions []metav1.Condition, healthSummary *HealthCondition) {
|
|
for _, cond := range conditions {
|
|
switch cond.Type {
|
|
case "Progressing":
|
|
if cond.Status == "False" {
|
|
healthSummary.Status = Unhealthy
|
|
healthSummary.Reason = cond.Reason
|
|
} else if cond.Reason != "NewReplicaSetAvailable" {
|
|
healthSummary.Status = Unknown
|
|
healthSummary.Reason = cond.Reason
|
|
}
|
|
case "Available", "Ready", "DisruptionAllowed", "Established", "NamesAccepted":
|
|
if healthSummary.Status == Unknown ||
|
|
(cond.Type == "Established" && healthSummary.Status == Healthy) ||
|
|
(cond.Type == "NamesAccepted" && healthSummary.Status == Healthy) {
|
|
if cond.Status == "False" {
|
|
healthSummary.Status = Unhealthy
|
|
} else {
|
|
healthSummary.Status = Healthy
|
|
}
|
|
healthSummary.Reason = cond.Reason
|
|
}
|
|
case "ContainersReady":
|
|
if healthSummary.Status == Unknown && cond.Status == "False" {
|
|
healthSummary.Status = Unhealthy
|
|
healthSummary.Reason = cond.Reason
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// applyStatusToResource applies the typed ResourceStatus back to the unstructured resource
|
|
func applyStatusToResource(res *unstructured.Unstructured, status *ResourceStatus) {
|
|
statusMap := map[string]any{
|
|
"phase": status.Phase,
|
|
"reason": status.Reason,
|
|
"message": status.Message,
|
|
}
|
|
|
|
if status.HealthSummary != nil {
|
|
statusMap["healthSummary"] = map[string]any{
|
|
"status": status.HealthSummary.Status,
|
|
"reason": status.HealthSummary.Reason,
|
|
"message": status.HealthSummary.Message,
|
|
}
|
|
}
|
|
|
|
unstructured.SetNestedMap(res.Object, statusMap, "status")
|
|
}
|
|
|
|
// flattenResources extracts items from a list resource and convert them to runtime.Objects
|
|
func flattenResources(resourceTypesLists map[string][]runtime.Object) []runtime.Object {
|
|
flattenedResources := []runtime.Object{}
|
|
|
|
for _, resourceTypeList := range resourceTypesLists {
|
|
for _, resourceItem := range resourceTypeList {
|
|
// if the resource item is a list, we need to flatten it too e.g. PodList
|
|
items := extractItemsIfList(resourceItem)
|
|
if items != nil {
|
|
flattenedResources = append(flattenedResources, items...)
|
|
} else {
|
|
flattenedResources = append(flattenedResources, resourceItem)
|
|
}
|
|
}
|
|
}
|
|
|
|
return flattenedResources
|
|
}
|
|
|
|
// extractItemsIfList extracts items if the resource is a list, or returns nil if not a list
|
|
func extractItemsIfList(resource runtime.Object) []runtime.Object {
|
|
unstructuredObj, ok := resource.(runtime.Unstructured)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if !unstructuredObj.IsList() {
|
|
return nil
|
|
}
|
|
|
|
extractedItems := []runtime.Object{}
|
|
err := unstructuredObj.EachListItem(func(obj runtime.Object) error {
|
|
extractedItems = append(extractedItems, obj)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return extractedItems
|
|
}
|