118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
)
|
|
|
|
func intstrFromInt(i int) intstr.IntOrString { return intstr.FromInt(i) }
|
|
|
|
const (
|
|
requeueShort = 15 * time.Second
|
|
requeueLong = 2 * time.Minute
|
|
|
|
managedByLabel = "app.kubernetes.io/managed-by"
|
|
managedByValue = "kea-operator"
|
|
clusterLabel = "kea.unkin.net/cluster"
|
|
roleLabel = "kea.unkin.net/role"
|
|
|
|
// statefulSetPodNameLabel is stamped on every StatefulSet pod by Kubernetes;
|
|
// the per-pod ClusterIP Service selects a single pod through it.
|
|
statefulSetPodNameLabel = "statefulset.kubernetes.io/pod-name"
|
|
|
|
finalizer = "kea.unkin.net/finalizer"
|
|
|
|
defaultAPIImage = "artifactapi.k8s.syd1.au.unkin.net/docker-internal/kea-api:latest"
|
|
)
|
|
|
|
func headlessName(cluster string) string { return cluster + "-headless" }
|
|
func serviceName(cluster string) string { return cluster }
|
|
func configMapName(cluster string) string { return cluster + "-config" }
|
|
func stsName(cluster string) string { return cluster }
|
|
|
|
// podName is the StatefulSet pod name for an ordinal.
|
|
func podName(cluster string, ordinal int) string { return fmt.Sprintf("%s-%d", cluster, ordinal) }
|
|
|
|
// peerServiceName is the per-pod ClusterIP Service fronting one HA peer's
|
|
// ctrl-agent. Its stable ClusterIP is what the HA hook peer URL points at.
|
|
func peerServiceName(cluster string, ordinal int) string {
|
|
return fmt.Sprintf("%s-peer-%d", cluster, ordinal)
|
|
}
|
|
|
|
// peerURL builds an HA peer control-channel URL. Kea 2.6's HA hook parses the
|
|
// host as an IP literal (no DNS), so this must be a stable IP address.
|
|
func peerURL(ip string) string { return fmt.Sprintf("http://%s:%d/", ip, 8000) }
|
|
|
|
func commonLabels(cluster string) map[string]string {
|
|
return map[string]string{
|
|
managedByLabel: managedByValue,
|
|
clusterLabel: cluster,
|
|
}
|
|
}
|
|
|
|
// setReady sets the single "Ready" condition, mirroring bind-operator.
|
|
func setReady(conds *[]metav1.Condition, gen int64, ok bool, reason, msg string) {
|
|
status := metav1.ConditionFalse
|
|
if ok {
|
|
status = metav1.ConditionTrue
|
|
}
|
|
meta.SetStatusCondition(conds, metav1.Condition{
|
|
Type: "Ready",
|
|
Status: status,
|
|
ObservedGeneration: gen,
|
|
Reason: reason,
|
|
Message: msg,
|
|
})
|
|
}
|
|
|
|
// configHash returns a stable hash of the named ConfigMap's data. Pods copy
|
|
// config out of a projected volume at startup, so a ConfigMap change alone
|
|
// never reaches a running pod; stamping this hash on the pod template is what
|
|
// rolls the StatefulSet. The input must contain no pod IPs or the roll loops.
|
|
func configHash(ctx context.Context, c client.Client, ns, name string) (string, error) {
|
|
var cm corev1.ConfigMap
|
|
if err := c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, &cm); err != nil {
|
|
return "", err
|
|
}
|
|
keys := make([]string, 0, len(cm.Data))
|
|
for k := range cm.Data {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
var buf bytes.Buffer
|
|
for _, k := range keys {
|
|
buf.WriteString(k)
|
|
buf.WriteByte(0)
|
|
buf.WriteString(cm.Data[k])
|
|
buf.WriteByte(0)
|
|
}
|
|
sum := sha256.Sum256(buf.Bytes())
|
|
return hex.EncodeToString(sum[:]), nil
|
|
}
|
|
|
|
func int32ptr(i int32) *int32 { return &i }
|
|
func ptr[T any](v T) *T { return &v }
|
|
|
|
// splitPool splits a "start - end" (any spacing) or single-address pool into
|
|
// its address fields.
|
|
func splitPool(p string) []string {
|
|
if !strings.Contains(p, "-") {
|
|
return []string{strings.TrimSpace(p)}
|
|
}
|
|
parts := strings.SplitN(p, "-", 2)
|
|
return []string{strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])}
|
|
}
|