blob: d8b3a7479bb2a5c2460767e0317216d6f99b6a5a [file] [log] [blame]
package installer
import (
"fmt"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/format"
)
type Kind int
const (
KindBoolean Kind = 0
KindInt = 7
KindString = 1
KindStruct = 2
KindNetwork = 3
KindMultiNetwork = 10
KindAuth = 5
KindSSHKey = 6
KindNumber = 4
KindArrayString = 8
KindPort = 9
KindVPNAuthKey = 11
KindCluster = 12
KindPassword = 13
KindSketchSessionId = 14
)
type Field struct {
Name string
Schema Schema
}
type Schema interface {
Name() string
Kind() Kind
Fields() []Field
Advanced() bool
Meta() map[string]string
}
var AuthSchema Schema = structSchema{
name: "Auth",
fields: []Field{
Field{"enabled", basicSchema{"Enabled", KindBoolean, false, nil}},
Field{"groups", basicSchema{"Groups", KindString, false, nil}},
Field{"noAuthPathPatterns", basicSchema{"Optional Auth Paths", KindArrayString, false, nil}},
},
advanced: false,
}
var SSHKeySchema Schema = structSchema{
name: "SSH Key",
fields: []Field{
Field{"public", basicSchema{"Public Key", KindString, false, nil}},
Field{"private", basicSchema{"Private Key", KindString, false, nil}},
},
advanced: true,
}
const clusterSchema = `
#Cluster: {
name: string
kubeconfig: string
ingressClassName: string
}
value: #Cluster
#Schema: %s
value: #Schema
`
func isCluster(v cue.Value) bool {
if v.Value().Kind() != cue.StructKind {
return false
}
vb, err := format.Node(v.Syntax(cue.All()), format.TabIndent(true))
if err != nil {
return false
}
s := fmt.Sprintf(clusterSchema, string(vb))
c := cuecontext.New()
u := c.CompileString(s)
if err := u.Err(); err != nil {
return false
}
if err := u.Validate(); err != nil {
return false
}
if err := u.Eval().Err(); err != nil {
return false
}
return true
}
const networkSchema = `
#Network: {
name: string
ingressClass: string
certificateIssuer: string | *""
domain: string
allocatePortAddr: string
reservePortAddr: string
deallocatePortAddr: string
}
value: #Network
#Schema: %s
value: #Schema
`
func isNetwork(v cue.Value) bool {
if v.Value().Kind() != cue.StructKind {
return false
}
vb, err := format.Node(v.Syntax(cue.All()), format.TabIndent(true))
if err != nil {
return false
}
s := fmt.Sprintf(networkSchema, string(vb))
c := cuecontext.New()
u := c.CompileString(s)
if err := u.Err(); err != nil {
return false
}
if err := u.Validate(); err != nil {
return false
}
if err := u.Eval().Err(); err != nil {
return false
}
return true
}
const multiNetworkSchema = `
#Network: {
name: string
ingressClass: string
certificateIssuer: string | *""
domain: string
allocatePortAddr: string
reservePortAddr: string
deallocatePortAddr: string
}
#Networks: [...#Network]
value: #Networks
#Schema: %s
value: #Schema
`
func isMultiNetwork(v cue.Value) bool {
if v.Value().IncompleteKind() != cue.ListKind {
return false
}
vb, err := format.Node(v.Syntax(cue.All()), format.TabIndent(true))
if err != nil {
return false
}
s := fmt.Sprintf(multiNetworkSchema, string(vb))
c := cuecontext.New()
u := c.CompileString(s)
if err := u.Err(); err != nil {
return false
}
if err := u.Validate(); err != nil {
return false
}
if err := u.Eval().Err(); err != nil {
return false
}
return true
}
const authSchema = `
#Auth: {
enabled: bool | false
groups: string | *""
noAuthPathPatterns: [...string] | *[]
}
value: #Auth
#Schema: %s
value: #Schema
`
func isAuth(v cue.Value) bool {
if v.Value().Kind() != cue.StructKind {
return false
}
vb, err := format.Node(v.Syntax(cue.All()), format.TabIndent(true))
if err != nil {
return false
}
s := fmt.Sprintf(authSchema, string(vb))
c := cuecontext.New()
u := c.CompileString(s)
if err := u.Err(); err != nil {
return false
}
if err := u.Validate(); err != nil {
return false
}
if err := u.Eval().Err(); err != nil {
return false
}
return true
}
const sshKeySchema = `
#SSHKey: {
public: string
private: string
}
value: #SSHKey
#Schema: %s
value: #Schema
`
func isSSHKey(v cue.Value) bool {
if v.Value().Kind() != cue.StructKind {
return false
}
vb, err := format.Node(v.Syntax(cue.All()), format.TabIndent(true))
if err != nil {
return false
}
s := fmt.Sprintf(sshKeySchema, string(vb))
c := cuecontext.New()
u := c.CompileString(s)
if err := u.Err(); err != nil {
return false
}
if err := u.Validate(); err != nil {
return false
}
if err := u.Eval().Err(); err != nil {
return false
}
return true
}
type basicSchema struct {
name string
kind Kind
advanced bool
meta map[string]string
}
func (s basicSchema) Name() string {
return s.name
}
func (s basicSchema) Kind() Kind {
return s.kind
}
func (s basicSchema) Fields() []Field {
return nil
}
func (s basicSchema) Advanced() bool {
return s.advanced
}
func (s basicSchema) Meta() map[string]string {
return s.meta
}
type structSchema struct {
name string
fields []Field
advanced bool
}
func (s structSchema) Name() string {
return s.name
}
func (s structSchema) Kind() Kind {
return KindStruct
}
func (s structSchema) Fields() []Field {
return s.fields
}
func (s structSchema) Advanced() bool {
return s.advanced
}
func (s structSchema) Meta() map[string]string {
return map[string]string{}
}
func NewCueSchema(name string, v cue.Value) (Schema, error) {
nameAttr := v.Attribute("name")
if nameAttr.Err() == nil {
name = nameAttr.Contents()
}
role := ""
roleAttr := v.Attribute("role")
if roleAttr.Err() == nil {
role = strings.ToLower(roleAttr.Contents())
}
switch v.IncompleteKind() {
case cue.StringKind:
if role == "password" {
// TODO(gio): implement configurable requirements such as min-length, ...
return basicSchema{name, KindPassword, false, nil}, nil
} else if role == "sketch-session-id" {
return basicSchema{name, KindSketchSessionId, false, nil}, nil
} else if role == "vpnauthkey" {
meta := map[string]string{}
usernameFieldAttr := v.Attribute("usernameField")
if usernameFieldAttr.Err() == nil {
meta["usernameField"] = usernameFieldAttr.Contents()
}
usernameAttr := v.Attribute("username")
if usernameAttr.Err() == nil {
meta["username"] = usernameAttr.Contents()
}
if len(meta) != 1 {
return nil, fmt.Errorf("invalid vpn auth key field meta: %+v", meta)
}
enabledFieldAttr := v.Attribute("enabledField")
if enabledFieldAttr.Err() == nil {
meta["enabledField"] = enabledFieldAttr.Contents()
}
return basicSchema{name, KindVPNAuthKey, true, meta}, nil
} else {
return basicSchema{name, KindString, false, nil}, nil
}
case cue.BoolKind:
return basicSchema{name, KindBoolean, false, nil}, nil
case cue.NumberKind:
return basicSchema{name, KindNumber, false, nil}, nil
case cue.IntKind:
if role == "port" {
return basicSchema{name, KindPort, true, nil}, nil
} else {
return basicSchema{name, KindInt, false, nil}, nil
}
case cue.ListKind:
if isMultiNetwork(v) {
return basicSchema{name, KindMultiNetwork, false, nil}, nil
}
return basicSchema{name, KindArrayString, false, nil}, nil
case cue.StructKind:
if isNetwork(v) {
return basicSchema{name, KindNetwork, false, nil}, nil
} else if isAuth(v) {
return basicSchema{name, KindAuth, false, nil}, nil
} else if isSSHKey(v) {
return basicSchema{name, KindSSHKey, true, nil}, nil
} else if isCluster(v) {
return basicSchema{name, KindCluster, false, nil}, nil
}
s := structSchema{name, make([]Field, 0), false}
f, err := v.Fields(cue.All())
if err != nil {
return nil, err
}
for f.Next() {
scm, err := NewCueSchema(f.Selector().String(), f.Value())
if err != nil {
return nil, err
}
s.fields = append(s.fields, Field{cleanFieldName(f.Selector().String()), scm})
}
return s, nil
default:
return nil, fmt.Errorf("SHOULD NOT REACH! field: %s, value: %s", name, v)
}
}
func ExtractDefaultValues(v cue.Value) (any, error) {
switch v.IncompleteKind() {
case cue.StringKind:
if d, ok := v.Default(); ok || d.IsConcrete() {
return d.String()
}
case cue.BoolKind:
if d, ok := v.Default(); ok {
return d.Bool()
}
case cue.NumberKind:
// TODO(gio): handle numbers
return nil, fmt.Errorf("implement: %s", v)
case cue.IntKind:
if d, ok := v.Default(); ok || d.IsConcrete() {
return d.Int64()
}
case cue.ListKind:
// TODO(gio): handle lists
return nil, nil
case cue.StructKind:
// TODO(gio): Such fields might have default values as well?
if isNetwork(v) {
return nil, nil
} else if isAuth(v) {
return nil, nil
} else if isCluster(v) {
return nil, nil
}
ret := map[string]any{}
f, err := v.Fields(cue.All())
if err != nil {
return nil, err
}
for f.Next() {
fv, err := ExtractDefaultValues(f.Value())
if err != nil {
return nil, err
}
if fv != nil {
ret[f.Selector().String()] = fv
}
}
if len(ret) == 0 {
return nil, nil
}
return ret, nil
default:
return nil, fmt.Errorf("SHOULD NOT REACH! value: %s", v)
}
return nil, nil
}
func cleanFieldName(name string) string {
return strings.ReplaceAll(strings.ReplaceAll(name, "?", ""), "!", "")
}