~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
262 lines · 6.6 KB Raw History
package compose

import (
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
)

const (
	ReasonInvalidKey       = "InvalidKey"
	ReasonAmbiguousKey     = "AmbiguousKey"
	ReasonSettingsMismatch = "SettingsMismatch"
	ReasonInvalidValue     = "InvalidValue"
)

type OverrideError struct {
	Key     string
	Reason  string
	Message string
}

func (e *OverrideError) Error() string {
	if e.Key == "" {
		return "components: " + e.Message
	}
	return "components." + e.Key + ": " + e.Message
}

func overrideErr(key, reason, format string, args ...any) error {
	return &OverrideError{Key: key, Reason: reason, Message: fmt.Sprintf(format, args...)}
}

func OverrideErrorReason(err error) string {
	if typed, ok := err.(*OverrideError); ok && typed.Reason != "" {
		return typed.Reason
	}
	return ReasonInvalidValue
}

func resolveOverrides(overrides v1alpha1.ComponentOverrides, desired []Desired) (map[string]v1alpha1.ComponentOverride, error) {
	out := make(map[string]v1alpha1.ComponentOverride, len(overrides))
	keys := make([]string, 0, len(overrides))
	for key := range overrides {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		kind, name, explicit := strings.Cut(key, "/")
		if strings.TrimSpace(key) == "" || strings.TrimSpace(kind) == "" || (explicit && strings.TrimSpace(name) == "") {
			return nil, overrideErr(key, ReasonInvalidKey, "a key names a component kind or a kind and a name")
		}
		var matches []Desired
		for _, d := range desired {
			if IsExposureKind(d.Kind) || d.Kind != kind || (explicit && d.Name != name) {
				continue
			}
			matches = append(matches, d)
		}
		if len(matches) == 0 {
			return nil, overrideErr(key, ReasonInvalidKey, "no %s component is in this root's desired set", key)
		}
		if !explicit && len(matches) > 1 {
			return nil, overrideErr(key, ReasonAmbiguousKey, "%s names %d components; use %s/%s", kind, len(matches), kind, matches[0].Name)
		}
		ov := overrides[key]
		if err := settingsFor(key, matches[0].Kind, ov.Settings); err != nil {
			return nil, err
		}
		out[ComponentKey(matches[0].Kind, matches[0].Name)] = ov
	}
	return out, nil
}

func settingsBlocks(settings *v1alpha1.ComponentSettings) (map[string]any, error) {
	if settings == nil {
		return nil, nil
	}
	var out map[string]any
	if err := jsonCopy(settings, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func settingsFor(key, kind string, settings *v1alpha1.ComponentSettings) error {
	blocks, err := settingsBlocks(settings)
	if err != nil {
		return overrideErr(key, ReasonInvalidValue, "the settings do not encode: %v", err)
	}
	want := lowerFirst(kind)
	for name := range blocks {
		if name != want {
			return overrideErr(key, ReasonSettingsMismatch, "settings.%s does not configure a %s component", name, kind)
		}
	}
	return nil
}

func ApplyOverrides(overrides v1alpha1.ComponentOverrides, desired []Desired) ([]Desired, error) {
	resolved, err := resolveOverrides(overrides, desired)
	if err != nil {
		return nil, err
	}
	if len(resolved) == 0 {
		return desired, nil
	}
	out := make([]Desired, len(desired))
	copy(out, desired)
	for i := range out {
		d := &out[i]
		if IsExposureKind(d.Kind) {
			continue
		}
		ov, ok := resolved[ComponentKey(d.Kind, d.Name)]
		if !ok {
			continue
		}
		spec, err := overrideSpec(d.Kind, d.Spec, ov)
		if err != nil {
			return nil, err
		}
		d.Spec = spec
	}
	return out, nil
}

func overrideSpec(kind string, spec any, ov v1alpha1.ComponentOverride) (map[string]any, error) {
	base, err := SpecMap(spec)
	if err != nil {
		return nil, err
	}
	out := map[string]any{}
	if base != nil {
		if err := jsonCopy(base, &out); err != nil {
			return nil, err
		}
	}
	if ov.Suspend != nil && *ov.Suspend {
		out[suspendField(kind)] = true
	}
	if ov.RestartPolicy != "" {
		out["restartPolicy"] = string(ov.RestartPolicy)
	}
	if err := setJSON(out, "limits", ov.Limits); err != nil {
		return nil, err
	}
	if err := setJSON(out, "serviceAuth", ov.ServiceAuth); err != nil {
		return nil, err
	}
	if err := setJSON(out, "comms", ov.Comms); err != nil {
		return nil, err
	}
	if ov.Ingress != nil {
		if err := ingressOverride(kind, out, ov.Ingress); err != nil {
			return nil, err
		}
	}
	blocks, err := settingsBlocks(ov.Settings)
	if err != nil {
		return nil, err
	}
	if block, ok := blocks[lowerFirst(kind)].(map[string]any); ok {
		deepMerge(out, block)
	}
	return out, nil
}

func suspendField(kind string) string {
	switch kind {
	case ComponentKindBidder, ComponentKindRequester:
		return "suspendParticipation"
	}
	return "suspend"
}

func OverrideComms(overrides v1alpha1.ComponentOverrides, kind, name string, base *v1alpha1.MarketComms) *v1alpha1.MarketComms {
	for _, key := range []string{ComponentKey(kind, name), kind} {
		if ov, ok := overrides[key]; ok && ov.Comms != nil {
			return ov.Comms
		}
	}
	return base
}

func ingressOverride(kind string, out map[string]any, ingress *v1alpha1.IngressConfig) error {
	switch kind {
	case ComponentKindRequester:
		return setJSON(out, "ingress", ingress)
	case ComponentKindPds:
		if ingress.IngressRef != nil {
			return setJSON(out, "ingressRef", ingress.IngressRef)
		}
	case ComponentKindIngressProxy:
		if p := strings.TrimSpace(ingress.Provider); p != "" {
			out["provider"] = p
		}
		if host := strings.TrimSpace(ingress.PublicHost); host != "" {
			out["publicHost"] = host
			out["hostnames"] = []any{IngressHostname(host)}
			identity, _ := out["identity"].(map[string]any)
			if identity == nil {
				identity = map[string]any{}
			}
			identity["handle"] = IngressHostname(host)
			out["identity"] = identity
		}
		if x := ingress.Xrpc; x != nil {
			if host := strings.TrimSpace(x.IngressProxyHost); host != "" {
				out["ingressProxyHost"] = host
			}
			if relay := strings.TrimSpace(x.RelayURL); relay != "" {
				out["relayURL"] = relay
			}
			if err := setJSON(out, "relayRef", x.RelayRef); err != nil {
				return err
			}
			if err := setJSON(out, "identityRef", x.IdentityRef); err != nil {
				return err
			}
		}
		return setJSON(out, "opentunnel", ingress.OpenTunnel)
	}
	return nil
}

func setJSON(out map[string]any, key string, value any) error {
	if isNil(value) {
		return nil
	}
	var v any
	if err := jsonCopy(value, &v); err != nil {
		return err
	}
	out[key] = v
	return nil
}

func isNil(v any) bool {
	if v == nil {
		return true
	}
	rv := reflect.ValueOf(v)
	return rv.Kind() == reflect.Pointer && rv.IsNil()
}

func deepMerge(dst, src map[string]any) {
	for k, v := range src {
		if v == nil {
			continue
		}
		if sm, ok := v.(map[string]any); ok {
			if dm, ok := dst[k].(map[string]any); ok {
				deepMerge(dm, sm)
				continue
			}
		}
		dst[k] = v
	}
}