internal/rollout/rollout.go
Rama main · réplica de lectura del repositorio original.
package rollout
import (
"cmp"
"context"
"errors"
"fmt"
"slices"
"strconv"
"strings"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"github.com/sophotechlabs/spinoza/internal/api"
"github.com/sophotechlabs/spinoza/internal/unstr"
)
const (
appsGroup = "apps"
replicaSets = "replicasets"
controllerRevisions = "controllerrevisions"
deploymentKind = "Deployment"
statefulSetKind = "StatefulSet"
daemonSetKind = "DaemonSet"
revisionAnnotation = "deployment.kubernetes.io/revision"
causeAnnotation = "kubernetes.io/change-cause"
templateHashLabel = "pod-template-hash"
revisionHashLabel = "controller-revision-hash"
specField = "spec"
templateField = "template"
)
type Source interface {
Get(ctx context.Context, ref api.ObjectRef) (*unstructured.Unstructured, error)
List(ctx context.Context, ref api.ObjectRef) ([]*unstructured.Unstructured, error)
}
func FromDynamic(dyn dynamic.Interface) Source {
return clusterSource{dyn: dyn}
}
type clusterSource struct {
dyn dynamic.Interface
}
func (source clusterSource) Get(ctx context.Context, ref api.ObjectRef) (*unstructured.Unstructured, error) {
return source.objects(ref).Get(ctx, ref.Name, metav1.GetOptions{})
}
func (source clusterSource) List(ctx context.Context, ref api.ObjectRef) ([]*unstructured.Unstructured, error) {
found, err := source.objects(ref).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
out := make([]*unstructured.Unstructured, 0, len(found.Items))
for i := range found.Items {
out = append(out, &found.Items[i])
}
return out, nil
}
func (source clusterSource) objects(ref api.ObjectRef) dynamic.ResourceInterface {
gvr := schema.GroupVersionResource{Group: ref.Group, Version: ref.Version, Resource: ref.Resource}
if ref.Namespace == "" {
return source.dyn.Resource(gvr)
}
return source.dyn.Resource(gvr).Namespace(ref.Namespace)
}
type entry struct {
api.Revision
template map[string]any
}
func List(ctx context.Context, src Source, ref api.ObjectRef) (api.Revisions, error) {
found, reason, err := history(ctx, src, ref)
if err != nil {
return api.Revisions{}, err
}
if reason != "" {
return api.Revisions{Supported: false, Reason: reason}, nil
}
out := make([]api.Revision, 0, len(found))
for _, one := range found {
out = append(out, one.Revision)
}
return api.Revisions{Revisions: out, Supported: true}, nil
}
func Template(ctx context.Context, src Source, ref api.ObjectRef, number int64) (map[string]any, error) {
found, reason, err := history(ctx, src, ref)
if err != nil {
return nil, err
}
if reason != "" {
return nil, errors.New(reason)
}
return pick(found, ref, number)
}
func pick(found []entry, ref api.ObjectRef, number int64) (map[string]any, error) {
for _, one := range found {
if one.Number != number {
continue
}
if one.template == nil {
return nil, fmt.Errorf("revision %d of %s holds no pod template", number, ref.Name)
}
return one.template, nil
}
return nil, fmt.Errorf("%s has no revision %d", ref.Name, number)
}
func history(ctx context.Context, src Source, ref api.ObjectRef) ([]entry, string, error) {
kind := kindOf(ref)
switch kind {
case deploymentKind:
found, err := replicaSetHistory(ctx, src, ref)
return found, "", err
case statefulSetKind, daemonSetKind:
found, err := controllerRevisionHistory(ctx, src, ref, kind)
return found, "", err
default:
return nil, describe(ref) + " keep no rollout revisions", nil
}
}
var kinds = map[string]string{
"deployments": deploymentKind,
"statefulsets": statefulSetKind,
"daemonsets": daemonSetKind,
}
func kindOf(ref api.ObjectRef) string {
if ref.Group != appsGroup {
return ""
}
return kinds[ref.Resource]
}
func describe(ref api.ObjectRef) string {
if ref.Group == "" {
return ref.Resource
}
return ref.Group + "/" + ref.Resource
}
func replicaSetHistory(ctx context.Context, src Source, ref api.ObjectRef) ([]entry, error) {
if _, err := src.Get(ctx, ref); err != nil {
return nil, err
}
sets, err := src.List(ctx, beside(ref, replicaSets))
if err != nil {
return nil, err
}
found := make([]entry, 0, len(sets))
for _, set := range sets {
if !ownedBy(set, ref.Name, deploymentKind) {
continue
}
number, numbered := annotatedRevision(set)
if !numbered {
continue
}
one := entryOf(set, number, templateOf(set, specField, templateField))
one.Replicas = int(unstr.Int(set, specField, "replicas"))
one.Ready = int(unstr.Int(set, "status", "readyReplicas"))
found = append(found, one)
}
return ordered(found), nil
}
func controllerRevisionHistory(ctx context.Context, src Source, ref api.ObjectRef, kind string) ([]entry, error) {
if _, err := src.Get(ctx, ref); err != nil {
return nil, err
}
held, err := src.List(ctx, beside(ref, controllerRevisions))
if err != nil {
return nil, err
}
found := make([]entry, 0, len(held))
for _, revision := range held {
if !ownedBy(revision, ref.Name, kind) {
continue
}
number := unstr.Int(revision, "revision")
if number == 0 {
continue
}
one := entryOf(revision, number, templateOf(revision, "data", specField, templateField))
one.Replicas = int(unstr.Int(revision, "data", specField, "replicas"))
found = append(found, one)
}
return ordered(found), nil
}
func entryOf(item *unstructured.Unstructured, number int64, template map[string]any) entry {
return entry{
Number: number,
Name: item.GetName(),
CreatedAt: createdAt(item),
Cause: item.GetAnnotations()[causeAnnotation],
Images: imagesOf(template),
template: template,
}
}
func beside(ref api.ObjectRef, resource string) api.ObjectRef {
return api.ObjectRef{
Group: appsGroup,
Version: ref.Version,
Resource: resource,
Namespace: ref.Namespace,
}
}
func ownedBy(item *unstructured.Unstructured, name, kind string) bool {
for _, owner := range item.GetOwnerReferences() {
if owner.Kind == kind && owner.Name == name {
return true
}
}
return false
}
func annotatedRevision(item *unstructured.Unstructured) (int64, bool) {
raw := item.GetAnnotations()[revisionAnnotation]
if raw == "" {
return 0, false
}
number, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return 0, false
}
return number, true
}
func templateOf(item *unstructured.Unstructured, fields ...string) map[string]any {
template, found := unstr.Map(item, fields...)
if !found {
return nil
}
return withoutHashes(template)
}
func withoutHashes(template map[string]any) map[string]any {
labels, found, err := unstructured.NestedStringMap(template, "metadata", "labels")
if !found || err != nil {
return template
}
delete(labels, templateHashLabel)
delete(labels, revisionHashLabel)
if len(labels) == 0 {
unstructured.RemoveNestedField(template, "metadata", "labels")
return template
}
setErr := unstructured.SetNestedStringMap(template, labels, "metadata", "labels")
if setErr != nil {
unstructured.RemoveNestedField(template, "metadata", "labels")
}
return template
}
func createdAt(item *unstructured.Unstructured) string {
stamp := item.GetCreationTimestamp()
if stamp.IsZero() {
return ""
}
return stamp.Time.UTC().Format(time.RFC3339)
}
func imagesOf(template map[string]any) []string {
containers, found, err := unstructured.NestedSlice(template, specField, "containers")
if !found || err != nil {
return nil
}
var out []string
for _, raw := range containers {
container, ok := raw.(map[string]any)
if !ok {
continue
}
image := unstr.At(container, "image")
if image == "" {
continue
}
out = append(out, image)
}
return out
}
func ordered(found []entry) []entry {
slices.SortFunc(found, func(left, right entry) int {
if left.Number != right.Number {
return cmp.Compare(right.Number, left.Number)
}
return strings.Compare(left.Name, right.Name)
})
return onlyOneCurrent(found)
}
func onlyOneCurrent(found []entry) []entry {
for at := range found {
found[at].Current = at == 0
}
return found
}