blob: eebb8ad1fd682538de75d219b327e3d57bd1eede [file] [log] [blame]
package installer
import (
"fmt"
"html/template"
"math/rand/v2"
"strings"
"github.com/richardlehane/crock32"
"github.com/sethvargo/go-password/password"
)
const defaultClusterName = "default"
type Release struct {
AppInstanceId string `json:"appInstanceId"`
Namespace string `json:"namespace"`
RepoAddr string `json:"repoAddr"`
AppDir string `json:"appDir"`
ImageRegistry string `json:"imageRegistry,omitempty"`
}
type Network struct {
Name string `json:"name,omitempty"`
IngressClass string `json:"ingressClass,omitempty"`
CertificateIssuer string `json:"certificateIssuer,omitempty"`
Domain string `json:"domain,omitempty"`
AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
ReservePortAddr string `json:"reservePortAddr,omitempty"`
DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
}
type InfraAppInstanceConfig struct {
Id string `json:"id"`
AppId string `json:"appId"`
Infra InfraConfig `json:"infra"`
Release Release `json:"release"`
Values map[string]any `json:"values"`
Input map[string]any `json:"input"`
URL string `json:"url"`
Help []HelpDocument `json:"help"`
Icon template.HTML `json:"icon"`
}
type AppInstanceConfig struct {
Id string `json:"id"`
AppId string `json:"appId"`
Env EnvConfig `json:"env"`
Release Release `json:"release"`
Values map[string]any `json:"values"`
Input map[string]any `json:"input"`
URL string `json:"url"`
Help []HelpDocument `json:"help"`
Icon string `json:"icon"`
}
func (a AppInstanceConfig) InputToValues(schema Schema) map[string]any {
ret, err := derivedToConfig(a.Input, schema)
if err != nil {
panic(err)
}
return ret
}
func getField(v any, f string) any {
for _, i := range strings.Split(f, ".") {
vm := v.(map[string]any)
v = vm[i]
}
return v
}
func deriveValues(
root any,
values any,
schema Schema,
networks []Network,
clusters []Cluster,
vpnKeyGen VPNAPIClient,
) (map[string]any, error) {
ret := make(map[string]any)
for _, f := range schema.Fields() {
k := f.Name
def := f.Schema
// TODO(gio): validate that it is map
v, ok := values.(map[string]any)[k]
// TODO(gio): if missing use default value
if !ok || v == nil {
if def.Kind() == KindSSHKey {
key, err := NewECDSASSHKeyPair("tmp")
if err != nil {
return nil, err
}
ret[k] = map[string]string{
"public": string(key.RawAuthorizedKey()),
"private": string(key.RawPrivateKey()),
}
}
if def.Kind() == KindPassword {
psswd, err := GeneratePassword()
if err != nil {
return nil, err
}
ret[k] = psswd
}
if def.Kind() == KindSketchSessionId {
ret[k] = GenerateSketchSessionId()
}
if def.Kind() == KindVPNAuthKey {
enabled := true
if v, ok := def.Meta()["enabledField"]; ok {
// TODO(gio): Improve getField
enabled, ok = getField(root, v).(bool)
if !ok {
enabled = false
// TODO(gio): validate that enabled field exists in the schema
// return nil, fmt.Errorf("could not resolve enabled: %+v %s %+v", def.Meta(), v, root)
}
}
if !enabled {
continue
}
var username string
if v, ok := def.Meta()["username"]; ok {
username = v
} else if v, ok := def.Meta()["usernameField"]; ok {
// TODO(gio): Improve getField
username, ok = getField(root, v).(string)
if !ok {
return nil, fmt.Errorf("could not resolve username: %+v %s %+v", def.Meta(), v, root)
}
}
authKey, err := vpnKeyGen.GenerateAuthKey(username)
if err != nil {
return nil, err
}
ret[k] = authKey
}
continue
}
switch def.Kind() {
case KindBoolean:
ret[k] = v
case KindString:
ret[k] = v
case KindInt:
ret[k] = v
case KindPort:
ret[k] = v
case KindVPNAuthKey:
ret[k] = v
case KindPassword:
ret[k] = v
case KindSketchSessionId:
ret[k] = v
case KindArrayString:
a, ok := v.([]string)
if !ok {
return nil, fmt.Errorf("expected string array")
}
ret[k] = a
case KindNetwork:
name, ok := v.(string)
if !ok {
return nil, fmt.Errorf("not a string")
}
n, err := findNetwork(networks, name)
if err != nil {
return nil, err
}
ret[k] = n
case KindMultiNetwork:
vv, ok := v.([]any)
if !ok {
return nil, fmt.Errorf("not an array")
}
picked := []Network{}
for _, nn := range vv {
name, ok := nn.(string)
if !ok {
return nil, fmt.Errorf("not a string")
}
n, err := findNetwork(networks, name)
if err != nil {
return nil, err
}
picked = append(picked, n)
}
ret[k] = picked
case KindCluster:
name, ok := v.(string)
if !ok {
// TODO(gio): validate that value has cluster schema
ret[k] = v
} else {
c, err := findCluster(clusters, name)
if err != nil {
return nil, err
}
if c == nil {
delete(ret, k)
} else {
ret[k] = c
}
}
case KindAuth:
r, err := deriveValues(root, v, AuthSchema, networks, clusters, vpnKeyGen)
if err != nil {
return nil, err
}
ret[k] = r
case KindSSHKey:
r, err := deriveValues(root, v, SSHKeySchema, networks, clusters, vpnKeyGen)
if err != nil {
return nil, err
}
ret[k] = r
case KindStruct:
r, err := deriveValues(root, v, def, networks, clusters, vpnKeyGen)
if err != nil {
return nil, err
}
ret[k] = r
default:
return nil, fmt.Errorf("Should not reach!")
}
}
return ret, nil
}
func derivedToConfig(derived map[string]any, schema Schema) (map[string]any, error) {
ret := make(map[string]any)
for _, f := range schema.Fields() {
k := f.Name
def := f.Schema
v, ok := derived[k]
// TODO(gio): if missing use default value
if !ok {
if def.Kind() == KindCluster {
ret[k] = "default"
}
continue
}
switch def.Kind() {
case KindBoolean:
ret[k] = v
case KindString:
ret[k] = v
case KindInt:
ret[k] = v
case KindPort:
ret[k] = v
case KindVPNAuthKey:
ret[k] = v
case KindPassword:
ret[k] = v
case KindArrayString:
a, ok := v.([]string)
if !ok {
return nil, fmt.Errorf("expected string array")
}
ret[k] = a
case KindNetwork:
vm, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map")
}
name, ok := vm["name"]
if !ok {
return nil, fmt.Errorf("expected network name")
}
ret[k] = name
case KindMultiNetwork:
nl, ok := v.([]any)
if !ok {
return nil, fmt.Errorf("expected map")
}
names := []string{}
for _, n := range nl {
i, ok := n.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map")
}
name, ok := i["name"]
if !ok {
return nil, fmt.Errorf("expected network name")
}
names = append(names, name.(string))
}
ret[k] = names
case KindAuth:
vm, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map")
}
r, err := derivedToConfig(vm, AuthSchema)
if err != nil {
return nil, err
}
ret[k] = r
case KindSSHKey:
vm, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map")
}
r, err := derivedToConfig(vm, SSHKeySchema)
if err != nil {
return nil, err
}
ret[k] = r
case KindStruct:
vm, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map")
}
r, err := derivedToConfig(vm, def)
if err != nil {
return nil, err
}
ret[k] = r
case KindCluster:
vm, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map")
}
name, ok := vm["name"]
if !ok {
return nil, fmt.Errorf("expected cluster name")
}
ret[k] = name
default:
return nil, fmt.Errorf("Should not reach!")
}
}
return ret, nil
}
func findNetwork(networks []Network, name string) (Network, error) {
for _, n := range networks {
if n.Name == name {
return n, nil
}
}
return Network{}, fmt.Errorf("Network not found: %s", name)
}
func findCluster(clusters []Cluster, name string) (*Cluster, error) {
if name == defaultClusterName {
return nil, nil
}
for _, c := range clusters {
if c.Name == name {
return &c, nil
}
}
return nil, fmt.Errorf("Cluster not found: %s", name)
}
func GeneratePassword() (string, error) {
return password.Generate(20, 5, 0, false, true)
}
func GenerateSketchSessionId() string {
u1, u2 := rand.Uint64(), rand.Uint64N(1<<16)
s := crock32.Encode(u1) + crock32.Encode(uint64(u2))
if len(s) < 16 {
s += strings.Repeat("0", 16-len(s))
}
return s[0:4] + "-" + s[4:8] + "-" + s[8:12] + "-" + s[12:16]
}