~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
351 lines · 8.9 KB Raw History
package apigen

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"

	"gopkg.in/yaml.v3"

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

const (
	kcpSchemaAPIVersion = "apis.kcp.io/v1alpha1"
	kcpExportAPIVersion = "apis.kcp.io/v1alpha2"
	scopeCluster        = "Cluster"
)

func scalar(tag, value string) *yaml.Node {
	return &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: value}
}

func str(value string) *yaml.Node {
	return scalar("!!str", value)
}

func boolean(value bool) *yaml.Node {
	if value {
		return scalar("!!bool", "true")
	}
	return scalar("!!bool", "false")
}

func mapping(pairs ...any) *yaml.Node {
	n := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
	for i := 0; i < len(pairs); i += 2 {
		n.Content = append(n.Content, str(pairs[i].(string)), pairs[i+1].(*yaml.Node))
	}
	if len(n.Content) == 0 {
		n.Style = yaml.FlowStyle
	}
	return n
}

func sequence(items ...*yaml.Node) *yaml.Node {
	return &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq", Content: items}
}

func strings2nodes(values []string) []*yaml.Node {
	out := make([]*yaml.Node, len(values))
	for i, v := range values {
		out[i] = str(v)
	}
	return out
}

func schemaNode(s *Schema) *yaml.Node {
	pairs := []any{"type", str(s.Type)}
	if s.Format != "" {
		pairs = append(pairs, "format", str(s.Format))
	}
	if len(s.Enum) > 0 {
		pairs = append(pairs, "enum", sequence(strings2nodes(s.Enum)...))
	}
	if s.Default != nil {
		pairs = append(pairs, "default", valueNode(s.Default))
	}
	if s.Minimum != nil {
		pairs = append(pairs, "minimum", integer(*s.Minimum))
	}
	if s.Maximum != nil {
		pairs = append(pairs, "maximum", integer(*s.Maximum))
	}
	if s.MaxLength != nil {
		pairs = append(pairs, "maxLength", integer(*s.MaxLength))
	}
	if s.Pattern != "" {
		pairs = append(pairs, "pattern", str(s.Pattern))
	}
	if len(s.Properties) > 0 {
		props := mapping()
		props.Style = 0
		for _, p := range s.Properties {
			props.Content = append(props.Content, str(p.Name), schemaNode(p.Schema))
		}
		pairs = append(pairs, "properties", props)
	}
	if req := s.Required(); len(req) > 0 {
		pairs = append(pairs, "required", sequence(strings2nodes(req)...))
	}
	if s.Items != nil {
		pairs = append(pairs, "items", schemaNode(s.Items))
	}
	if s.AdditionalProperties != nil {
		pairs = append(pairs, "additionalProperties", schemaNode(s.AdditionalProperties))
	}
	if s.ListType != "" {
		pairs = append(pairs, "x-kubernetes-list-type", str(s.ListType))
	}
	if len(s.ListMapKeys) > 0 {
		pairs = append(pairs, "x-kubernetes-list-map-keys", sequence(strings2nodes(s.ListMapKeys)...))
	}
	if s.PreserveUnknown {
		pairs = append(pairs, "x-kubernetes-preserve-unknown-fields", boolean(true))
	}
	return mapping(pairs...)
}

func integer(n int64) *yaml.Node {
	return scalar("!!int", strconv.FormatInt(n, 10))
}

func valueNode(v any) *yaml.Node {
	switch x := v.(type) {
	case string:
		return str(x)
	case bool:
		return boolean(x)
	case float64:
		if x == math.Trunc(x) {
			return integer(int64(x))
		}
		return scalar("!!float", strconv.FormatFloat(x, 'g', -1, 64))
	case []any:
		items := make([]*yaml.Node, len(x))
		for i, e := range x {
			items[i] = valueNode(e)
		}
		n := sequence(items...)
		n.Style = yaml.FlowStyle
		return n
	case map[string]any:
		keys := make([]string, 0, len(x))
		for k := range x {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		pairs := make([]any, 0, 2*len(keys))
		for _, k := range keys {
			pairs = append(pairs, k, valueNode(x[k]))
		}
		n := mapping(pairs...)
		n.Style = yaml.FlowStyle
		return n
	default:
		return scalar("!!null", "null")
	}
}

func encodeYAML(doc *yaml.Node) ([]byte, error) {
	var buf bytes.Buffer
	enc := yaml.NewEncoder(&buf)
	enc.SetIndent(2)
	if err := enc.Encode(doc); err != nil {
		return nil, err
	}
	if err := enc.Close(); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func (m *Model) APIResourceSchema(r Resource) ([]byte, error) {
	version := []any{
		"name", str(m.Version),
		"served", boolean(true),
		"storage", boolean(true),
		"subresources", mapping("status", mapping()),
		"schema", schemaNode(r.Schema),
	}
	if len(r.PrinterColumns) > 0 {
		var columns []*yaml.Node
		for _, c := range r.PrinterColumns {
			columns = append(columns, mapping("name", str(c.Name), "type", str(c.Type), "jsonPath", str(c.JSONPath)))
		}
		version = append(version, "additionalPrinterColumns", sequence(columns...))
	}
	metadata := []any{"name", str(r.SchemaName(m.Group, m.Version))}
	if len(r.Contracts) > 0 {
		var labels []any
		for _, c := range r.Contracts {
			labels = append(labels, commonv1.ContractLabel(c), str(commonv1.ContractVersion))
		}
		metadata = append(metadata, "labels", mapping(labels...))
	}
	doc := mapping(
		"apiVersion", str(kcpSchemaAPIVersion),
		"kind", str("APIResourceSchema"),
		"metadata", mapping(metadata...),
		"spec", mapping(
			"group", str(m.Group),
			"names", mapping(
				"kind", str(r.Kind),
				"listKind", str(r.ListKind),
				"plural", str(r.Plural),
				"singular", str(r.Singular),
			),
			"scope", str(scopeCluster),
			"versions", sequence(mapping(version...)),
		),
	)
	return encodeYAML(doc)
}

type Export struct {
	Name      string
	Resources []Resource
}

func (m *Model) Exports() []Export {
	var out []Export
	index := map[string]int{}
	for _, r := range m.Resources {
		i, ok := index[r.Export]
		if !ok {
			i = len(out)
			index[r.Export] = i
			out = append(out, Export{Name: r.Export})
		}
		out[i].Resources = append(out[i].Resources, r)
	}
	return out
}

func (m *Model) APIExport(e Export) ([]byte, error) {
	var resources []*yaml.Node
	for _, r := range e.Resources {
		resources = append(resources, mapping(
			"group", str(m.Group),
			"name", str(r.Plural),
			"schema", str(r.SchemaName(m.Group, m.Version)),
			"storage", mapping("crd", mapping()),
		))
	}
	doc := mapping(
		"apiVersion", str(kcpExportAPIVersion),
		"kind", str("APIExport"),
		"metadata", mapping("name", str(e.Name)),
		"spec", mapping("resources", sequence(resources...)),
	)
	return encodeYAML(doc)
}

func SchemaFile(r Resource) string {
	return "deploy/" + r.Singular + "-apiresourceschema.yaml"
}

func ExportFile(e Export) string {
	return "deploy/" + e.Resources[0].Singular + "-apiexport.yaml"
}

func SpecHash(manifest []byte) (string, string, error) {
	var doc struct {
		Metadata struct {
			Name string `yaml:"name"`
		} `yaml:"metadata"`
		Spec any `yaml:"spec"`
	}
	if err := yaml.Unmarshal(manifest, &doc); err != nil {
		return "", "", err
	}
	if doc.Metadata.Name == "" || doc.Spec == nil {
		return "", "", fmt.Errorf("not an APIResourceSchema: no metadata.name or spec")
	}
	canonical, err := json.Marshal(doc.Spec)
	if err != nil {
		return "", "", err
	}
	sum := sha256.Sum256(canonical)
	return doc.Metadata.Name, "sha256:" + hex.EncodeToString(sum[:]), nil
}

type RevisionLock map[string]string

func ParseRevisionLock(data []byte) (RevisionLock, error) {
	lock := RevisionLock{}
	for i, line := range strings.Split(string(data), "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		name, hash, ok := strings.Cut(line, " ")
		if !ok || name == "" || !strings.HasPrefix(hash, "sha256:") || strings.Contains(hash, " ") {
			return nil, fmt.Errorf("line %d: %q is not \"<schema name> sha256:<hex>\"", i+1, line)
		}
		if prior, ok := lock[name]; ok && prior != hash {
			return nil, fmt.Errorf("line %d: %s is recorded twice, as %s and %s: two branches changed its spec under "+
				"the same revision (a union merge keeps both lines). Keep the line that is on origin/main, drop "+
				"the other, and bump Revision in api/v1alpha1/resources.go", i+1, name, prior, hash)
		}
		lock[name] = hash
	}
	return lock, nil
}

func (l RevisionLock) Admit(name, hash string) error {
	if prior, ok := l[name]; ok && prior != hash {
		plural := strings.SplitN(name, ".", 3)
		kind := name
		if len(plural) == 3 {
			kind = plural[1]
		}
		return fmt.Errorf("the spec of APIResourceSchema %s changed (%s, was %s), and kcp holds "+
			"APIResourceSchema.spec IMMUTABLE: applying it fails on every host that already has it. "+
			"Bump Revision for %s in api/v1alpha1/resources.go so the change ships as a new schema "+
			"the APIExport is re-pointed at", name, hash, prior, kind)
	}
	l[name] = hash
	return nil
}

func RevisionOrder(data []byte) []string {
	var order []string
	seen := map[string]bool{}
	for line := range strings.SplitSeq(string(data), "\n") {
		name, _, _ := strings.Cut(strings.TrimSpace(line), " ")
		if name != "" && !seen[name] {
			seen[name] = true
			order = append(order, name)
		}
	}
	return order
}

func (l RevisionLock) Marshal(order ...string) ([]byte, error) {
	var b bytes.Buffer
	written := map[string]bool{}
	for _, name := range order {
		if hash, ok := l[name]; ok && !written[name] {
			written[name] = true
			fmt.Fprintf(&b, "%s %s\n", name, hash)
		}
	}
	var rest []string
	for name := range l {
		if !written[name] {
			rest = append(rest, name)
		}
	}
	sort.Strings(rest)
	for _, name := range rest {
		fmt.Fprintf(&b, "%s %s\n", name, l[name])
	}
	return b.Bytes(), nil
}