blob: 51a6757ff2f19ce9066e13202679366fba3fd9cc [file]
package watcher
import (
"fmt"
"log"
"os"
"reflect"
"sort"
"sync"
"github.com/apache/trafficserver-ingress-controller/endpoint"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// AtsSniHandler handles Atssnipolicy CR events
type AtsSniHandler struct {
ResourceName string
Ep *endpoint.Endpoint
FilePath string
mu sync.Mutex
}
// Constructor
func NewAtsSniHandler(resource string, ep *endpoint.Endpoint, path string) *AtsSniHandler {
log.Println("Ats SNI Handler initialized")
return &AtsSniHandler{ResourceName: resource, Ep: ep, FilePath: path}
}
// SniEntry represents one fqdn entry in sni.yaml (flexible, dynamic)
type SniEntry map[string]interface{}
// nodeFromInterface creates a yaml.Node from any arbitrary Go value, preserving
// sequences/maps/numbers/booleans instead of converting to a plain string.
func nodeFromInterface(v interface{}) (*yaml.Node, error) {
var node yaml.Node
// First marshal the value to YAML bytes, then unmarshal into a Node to
// obtain a properly-typed node.
b, err := yaml.Marshal(v)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(b, &node); err != nil {
return nil, err
}
// When unmarshalling into a node the top-level is a DocumentNode; return its content.
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
return node.Content[0], nil
}
return &node, nil
}
// Custom YAML marshaller to ensure fqdn appears first and to preserve native types
func (s SniEntry) MarshalYAML() (interface{}, error) {
node := &yaml.Node{
Kind: yaml.MappingNode,
Tag: "!!map",
}
// Write fqdn first if present
if fqdn, ok := s["fqdn"]; ok {
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "fqdn"}
valNode, err := nodeFromInterface(fqdn)
if err != nil {
// fallback to string scalar
valNode = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: fmt.Sprintf("%v", fqdn)}
}
node.Content = append(node.Content, keyNode, valNode)
}
// Collect remaining keys (excluding fqdn)
keys := make([]string, 0, len(s))
for k := range s {
if k != "fqdn" {
keys = append(keys, k)
}
}
// Sort the other keys alphabetically for consistent output
sort.Strings(keys)
// Append other keys with proper YAML nodes (preserving arrays/maps/etc)
for _, k := range keys {
v := s[k]
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: k}
valNode, err := nodeFromInterface(v)
if err != nil {
// fallback to string representation if marshalling fails
valNode = &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: fmt.Sprintf("%v", v)}
}
node.Content = append(node.Content, keyNode, valNode)
}
return node, nil
}
// SniFile represents the full sni.yaml structure
type SniFile struct {
Sni []SniEntry `yaml:"sni,omitempty"`
}
// Add handles creation of Atssnipolicy
func (h *AtsSniHandler) Add(obj interface{}) {
h.mu.Lock()
defer h.mu.Unlock()
u := obj.(*unstructured.Unstructured)
log.Printf("[ADD] Ats Sni Policy: #%v", u)
newSni, found, err := unstructured.NestedSlice(u.Object, "spec", "sni")
if err != nil || !found {
log.Printf("Add: sni not found or error: %v", err)
return
}
sniFile := h.loadSniFile()
for _, entry := range newSni {
entryMap, ok := entry.(map[string]interface{})
if !ok {
continue
}
fqdn, ok := entryMap["fqdn"].(string)
if !ok || fqdn == "" {
continue
}
updated := false
for i, existing := range sniFile.Sni {
if existingFqdn, _ := existing["fqdn"].(string); existingFqdn == fqdn {
if !reflect.DeepEqual(existing, entryMap) {
sniFile.Sni[i] = entryMap
}
updated = true
break
}
}
if !updated {
sniFile.Sni = append(sniFile.Sni, entryMap)
}
}
h.writeSniFile(sniFile)
h.reloadSni()
}
// Update handles updates of Atssnipolicy
func (h *AtsSniHandler) Update(oldObj, newObj interface{}) {
h.mu.Lock()
defer h.mu.Unlock()
newU := newObj.(*unstructured.Unstructured)
log.Printf("[UPDATE] Atssnipolicy: #%v", newU)
newSni, found, err := unstructured.NestedSlice(newU.Object, "spec", "sni")
if err != nil || !found {
log.Printf("Update: sni not found or error: %v", err)
return
}
sniFile := h.loadSniFile()
newMap := make(map[string]SniEntry)
for _, entry := range newSni {
if m, ok := entry.(map[string]interface{}); ok {
if fqdn, ok := m["fqdn"].(string); ok && fqdn != "" {
newMap[fqdn] = m
}
}
}
log.Println("New Updated map in Update function ", newMap)
var updatedSni []SniEntry
seen := make(map[string]struct{})
for _, existing := range sniFile.Sni {
fqdn, _ := existing["fqdn"].(string)
if newEntry, ok := newMap[fqdn]; ok {
updatedSni = append(updatedSni, newEntry)
seen[fqdn] = struct{}{}
} else {
updatedSni = append(updatedSni, existing)
}
}
for fqdn, newEntry := range newMap {
if _, already := seen[fqdn]; !already {
updatedSni = append(updatedSni, newEntry)
}
}
sniFile.Sni = updatedSni
h.writeSniFile(sniFile)
h.reloadSni()
}
// Delete handles deletion of Atssnipolicy
func (h *AtsSniHandler) Delete(obj interface{}) {
h.mu.Lock()
defer h.mu.Unlock()
u := obj.(*unstructured.Unstructured)
log.Printf("[DELETE] Atssnipolicy: #%v", u)
sniFile := h.loadSniFile()
sniList, found, err := unstructured.NestedSlice(u.Object, "spec", "sni")
if err != nil || !found {
log.Printf("Delete: sni not found or error: %v", err)
return
}
delMap := make(map[string]struct{})
for _, entry := range sniList {
if m, ok := entry.(map[string]interface{}); ok {
if fqdn, ok := m["fqdn"].(string); ok && fqdn != "" {
delMap[fqdn] = struct{}{}
}
}
}
var updatedSni []SniEntry
for _, existing := range sniFile.Sni {
fqdn, _ := existing["fqdn"].(string)
if _, toDelete := delMap[fqdn]; !toDelete {
updatedSni = append(updatedSni, existing)
}
}
sniFile.Sni = updatedSni
h.writeSniFile(sniFile)
h.reloadSni()
}
// loadSniFile reads existing sni.yaml
func (h *AtsSniHandler) loadSniFile() SniFile {
var sniFile SniFile
data, err := os.ReadFile(h.FilePath)
if err != nil {
if os.IsNotExist(err) {
return sniFile
}
log.Printf("Failed to read sni.yaml: %v", err)
return sniFile
}
if err := yaml.Unmarshal(data, &sniFile); err != nil {
log.Printf("Failed to unmarshal sni.yaml: %v", err)
}
return sniFile
}
// writeSniFile writes sni.yaml atomically
func (h *AtsSniHandler) writeSniFile(sniFile SniFile) {
if len(sniFile.Sni) == 0 {
if err := os.WriteFile(h.FilePath, []byte{}, 0644); err != nil {
log.Printf("Failed to clear sni.yaml: %v", err)
}
return
}
data, err := yaml.Marshal(&sniFile)
if err != nil {
log.Printf("Failed to marshal sni.yaml: %v", err)
return
}
tmp := h.FilePath + ".tmp"
if err := os.WriteFile(tmp, data, 0644); err != nil {
log.Printf("Failed to write temp sni.yaml: %v", err)
return
}
_ = os.Rename(tmp, h.FilePath)
}
// reloadSni triggers ATS reload
func (h *AtsSniHandler) reloadSni() {
if h.Ep != nil && h.Ep.ATSManager != nil {
if msg, err := h.Ep.ATSManager.SniSet(); err != nil {
log.Printf("Failed to reload ATS SNI: %v", err)
} else {
log.Printf("ATS SNI reloaded: %s", msg)
}
}
}