blob: eebb8ad1fd682538de75d219b327e3d57bd1eede [file] [log] [blame]
gio3af43942024-04-16 08:13:50 +04001package installer
2
3import (
4 "fmt"
gio09a3e5b2024-04-26 14:11:06 +04005 "html/template"
gioe65d9a92025-06-19 09:02:32 +04006 "math/rand/v2"
gio36b23b32024-08-25 12:20:54 +04007 "strings"
gio6481c902025-05-20 16:16:30 +04008
gioe65d9a92025-06-19 09:02:32 +04009 "github.com/richardlehane/crock32"
gio6481c902025-05-20 16:16:30 +040010 "github.com/sethvargo/go-password/password"
gio3af43942024-04-16 08:13:50 +040011)
12
giof6ad2982024-08-23 17:42:49 +040013const defaultClusterName = "default"
14
gio3af43942024-04-16 08:13:50 +040015type Release struct {
gio3cdee592024-04-17 10:15:56 +040016 AppInstanceId string `json:"appInstanceId"`
17 Namespace string `json:"namespace"`
18 RepoAddr string `json:"repoAddr"`
19 AppDir string `json:"appDir"`
giof8843412024-05-22 16:38:05 +040020 ImageRegistry string `json:"imageRegistry,omitempty"`
gio3af43942024-04-16 08:13:50 +040021}
22
23type Network struct {
giocdfa3722024-06-13 20:10:14 +040024 Name string `json:"name,omitempty"`
25 IngressClass string `json:"ingressClass,omitempty"`
26 CertificateIssuer string `json:"certificateIssuer,omitempty"`
27 Domain string `json:"domain,omitempty"`
28 AllocatePortAddr string `json:"allocatePortAddr,omitempty"`
29 ReservePortAddr string `json:"reservePortAddr,omitempty"`
30 DeallocatePortAddr string `json:"deallocatePortAddr,omitempty"`
gio3af43942024-04-16 08:13:50 +040031}
32
gioe72b54f2024-04-22 10:44:41 +040033type InfraAppInstanceConfig struct {
34 Id string `json:"id"`
35 AppId string `json:"appId"`
36 Infra InfraConfig `json:"infra"`
37 Release Release `json:"release"`
38 Values map[string]any `json:"values"`
39 Input map[string]any `json:"input"`
gio09a3e5b2024-04-26 14:11:06 +040040 URL string `json:"url"`
41 Help []HelpDocument `json:"help"`
42 Icon template.HTML `json:"icon"`
gioe72b54f2024-04-22 10:44:41 +040043}
44
gio3cdee592024-04-17 10:15:56 +040045type AppInstanceConfig struct {
gio3af43942024-04-16 08:13:50 +040046 Id string `json:"id"`
47 AppId string `json:"appId"`
gioe72b54f2024-04-22 10:44:41 +040048 Env EnvConfig `json:"env"`
gio3cdee592024-04-17 10:15:56 +040049 Release Release `json:"release"`
50 Values map[string]any `json:"values"`
51 Input map[string]any `json:"input"`
gio09a3e5b2024-04-26 14:11:06 +040052 URL string `json:"url"`
Davit Tabidze56f86a42024-04-09 19:15:25 +040053 Help []HelpDocument `json:"help"`
gio09a3e5b2024-04-26 14:11:06 +040054 Icon string `json:"icon"`
gio3af43942024-04-16 08:13:50 +040055}
56
gio3cdee592024-04-17 10:15:56 +040057func (a AppInstanceConfig) InputToValues(schema Schema) map[string]any {
58 ret, err := derivedToConfig(a.Input, schema)
gio3af43942024-04-16 08:13:50 +040059 if err != nil {
gio3cdee592024-04-17 10:15:56 +040060 panic(err)
gio3af43942024-04-16 08:13:50 +040061 }
62 return ret
63}
64
gio36b23b32024-08-25 12:20:54 +040065func getField(v any, f string) any {
66 for _, i := range strings.Split(f, ".") {
67 vm := v.(map[string]any)
68 v = vm[i]
69 }
70 return v
71}
72
73func deriveValues(
74 root any,
75 values any,
76 schema Schema,
77 networks []Network,
giof6ad2982024-08-23 17:42:49 +040078 clusters []Cluster,
gio864b4332024-09-05 13:56:47 +040079 vpnKeyGen VPNAPIClient,
gio36b23b32024-08-25 12:20:54 +040080) (map[string]any, error) {
gio3af43942024-04-16 08:13:50 +040081 ret := make(map[string]any)
gio44f621b2024-04-29 09:44:38 +040082 for _, f := range schema.Fields() {
83 k := f.Name
84 def := f.Schema
gio3af43942024-04-16 08:13:50 +040085 // TODO(gio): validate that it is map
86 v, ok := values.(map[string]any)[k]
87 // TODO(gio): if missing use default value
gio842db3f2025-05-30 11:57:20 +040088 if !ok || v == nil {
gio3af43942024-04-16 08:13:50 +040089 if def.Kind() == KindSSHKey {
90 key, err := NewECDSASSHKeyPair("tmp")
91 if err != nil {
92 return nil, err
93 }
94 ret[k] = map[string]string{
95 "public": string(key.RawAuthorizedKey()),
96 "private": string(key.RawPrivateKey()),
97 }
98 }
gio6481c902025-05-20 16:16:30 +040099 if def.Kind() == KindPassword {
100 psswd, err := GeneratePassword()
101 if err != nil {
102 return nil, err
103 }
104 ret[k] = psswd
105 }
gioe65d9a92025-06-19 09:02:32 +0400106 if def.Kind() == KindSketchSessionId {
107 ret[k] = GenerateSketchSessionId()
108 }
gio36b23b32024-08-25 12:20:54 +0400109 if def.Kind() == KindVPNAuthKey {
gio29f6b872024-09-08 16:14:58 +0400110 enabled := true
111 if v, ok := def.Meta()["enabledField"]; ok {
112 // TODO(gio): Improve getField
113 enabled, ok = getField(root, v).(bool)
114 if !ok {
giof6ad2982024-08-23 17:42:49 +0400115 enabled = false
116 // TODO(gio): validate that enabled field exists in the schema
117 // return nil, fmt.Errorf("could not resolve enabled: %+v %s %+v", def.Meta(), v, root)
gio29f6b872024-09-08 16:14:58 +0400118 }
119 }
120 if !enabled {
121 continue
122 }
gio7fbd4ad2024-08-27 10:06:39 +0400123 var username string
124 if v, ok := def.Meta()["username"]; ok {
125 username = v
126 } else if v, ok := def.Meta()["usernameField"]; ok {
127 // TODO(gio): Improve getField
128 username, ok = getField(root, v).(string)
129 if !ok {
130 return nil, fmt.Errorf("could not resolve username: %+v %s %+v", def.Meta(), v, root)
131 }
132 }
gio864b4332024-09-05 13:56:47 +0400133 authKey, err := vpnKeyGen.GenerateAuthKey(username)
gio36b23b32024-08-25 12:20:54 +0400134 if err != nil {
135 return nil, err
136 }
137 ret[k] = authKey
138 }
gio3af43942024-04-16 08:13:50 +0400139 continue
140 }
141 switch def.Kind() {
142 case KindBoolean:
143 ret[k] = v
144 case KindString:
145 ret[k] = v
146 case KindInt:
147 ret[k] = v
gioefa0ed42024-06-13 12:31:43 +0400148 case KindPort:
149 ret[k] = v
gio36b23b32024-08-25 12:20:54 +0400150 case KindVPNAuthKey:
151 ret[k] = v
gio6481c902025-05-20 16:16:30 +0400152 case KindPassword:
153 ret[k] = v
gioe65d9a92025-06-19 09:02:32 +0400154 case KindSketchSessionId:
155 ret[k] = v
gioe72b54f2024-04-22 10:44:41 +0400156 case KindArrayString:
157 a, ok := v.([]string)
158 if !ok {
159 return nil, fmt.Errorf("expected string array")
160 }
161 ret[k] = a
gio3af43942024-04-16 08:13:50 +0400162 case KindNetwork:
gio4ece99c2024-07-18 11:05:50 +0400163 name, ok := v.(string)
164 if !ok {
165 return nil, fmt.Errorf("not a string")
166 }
167 n, err := findNetwork(networks, name)
gio3af43942024-04-16 08:13:50 +0400168 if err != nil {
169 return nil, err
170 }
171 ret[k] = n
gio4ece99c2024-07-18 11:05:50 +0400172 case KindMultiNetwork:
173 vv, ok := v.([]any)
174 if !ok {
175 return nil, fmt.Errorf("not an array")
176 }
177 picked := []Network{}
178 for _, nn := range vv {
179 name, ok := nn.(string)
180 if !ok {
181 return nil, fmt.Errorf("not a string")
182 }
183 n, err := findNetwork(networks, name)
184 if err != nil {
185 return nil, err
186 }
187 picked = append(picked, n)
188 }
189 ret[k] = picked
giof6ad2982024-08-23 17:42:49 +0400190 case KindCluster:
191 name, ok := v.(string)
192 if !ok {
193 // TODO(gio): validate that value has cluster schema
194 ret[k] = v
195 } else {
196 c, err := findCluster(clusters, name)
197 if err != nil {
198 return nil, err
199 }
200 if c == nil {
201 delete(ret, k)
202 } else {
203 ret[k] = c
204 }
205 }
gio3af43942024-04-16 08:13:50 +0400206 case KindAuth:
giof6ad2982024-08-23 17:42:49 +0400207 r, err := deriveValues(root, v, AuthSchema, networks, clusters, vpnKeyGen)
gio3af43942024-04-16 08:13:50 +0400208 if err != nil {
209 return nil, err
210 }
211 ret[k] = r
212 case KindSSHKey:
giof6ad2982024-08-23 17:42:49 +0400213 r, err := deriveValues(root, v, SSHKeySchema, networks, clusters, vpnKeyGen)
gio3af43942024-04-16 08:13:50 +0400214 if err != nil {
215 return nil, err
216 }
217 ret[k] = r
218 case KindStruct:
giof6ad2982024-08-23 17:42:49 +0400219 r, err := deriveValues(root, v, def, networks, clusters, vpnKeyGen)
gio3af43942024-04-16 08:13:50 +0400220 if err != nil {
221 return nil, err
222 }
223 ret[k] = r
224 default:
225 return nil, fmt.Errorf("Should not reach!")
226 }
227 }
228 return ret, nil
229}
230
231func derivedToConfig(derived map[string]any, schema Schema) (map[string]any, error) {
232 ret := make(map[string]any)
gio44f621b2024-04-29 09:44:38 +0400233 for _, f := range schema.Fields() {
234 k := f.Name
235 def := f.Schema
gio3af43942024-04-16 08:13:50 +0400236 v, ok := derived[k]
237 // TODO(gio): if missing use default value
238 if !ok {
gio488554f2024-10-02 16:41:26 +0400239 if def.Kind() == KindCluster {
240 ret[k] = "default"
241 }
gio3af43942024-04-16 08:13:50 +0400242 continue
243 }
244 switch def.Kind() {
245 case KindBoolean:
246 ret[k] = v
247 case KindString:
248 ret[k] = v
249 case KindInt:
250 ret[k] = v
gioefa0ed42024-06-13 12:31:43 +0400251 case KindPort:
252 ret[k] = v
gio36b23b32024-08-25 12:20:54 +0400253 case KindVPNAuthKey:
254 ret[k] = v
gio6481c902025-05-20 16:16:30 +0400255 case KindPassword:
256 ret[k] = v
gioe72b54f2024-04-22 10:44:41 +0400257 case KindArrayString:
258 a, ok := v.([]string)
259 if !ok {
260 return nil, fmt.Errorf("expected string array")
261 }
262 ret[k] = a
gio3af43942024-04-16 08:13:50 +0400263 case KindNetwork:
264 vm, ok := v.(map[string]any)
265 if !ok {
266 return nil, fmt.Errorf("expected map")
267 }
268 name, ok := vm["name"]
269 if !ok {
270 return nil, fmt.Errorf("expected network name")
271 }
272 ret[k] = name
gio4ece99c2024-07-18 11:05:50 +0400273 case KindMultiNetwork:
274 nl, ok := v.([]any)
275 if !ok {
276 return nil, fmt.Errorf("expected map")
277 }
278 names := []string{}
279 for _, n := range nl {
280 i, ok := n.(map[string]any)
281 if !ok {
282 return nil, fmt.Errorf("expected map")
283 }
284 name, ok := i["name"]
285 if !ok {
286 return nil, fmt.Errorf("expected network name")
287 }
288 names = append(names, name.(string))
289 }
290 ret[k] = names
gio3af43942024-04-16 08:13:50 +0400291 case KindAuth:
292 vm, ok := v.(map[string]any)
293 if !ok {
294 return nil, fmt.Errorf("expected map")
295 }
296 r, err := derivedToConfig(vm, AuthSchema)
297 if err != nil {
298 return nil, err
299 }
300 ret[k] = r
301 case KindSSHKey:
302 vm, ok := v.(map[string]any)
303 if !ok {
304 return nil, fmt.Errorf("expected map")
305 }
306 r, err := derivedToConfig(vm, SSHKeySchema)
307 if err != nil {
308 return nil, err
309 }
310 ret[k] = r
311 case KindStruct:
312 vm, ok := v.(map[string]any)
313 if !ok {
314 return nil, fmt.Errorf("expected map")
315 }
316 r, err := derivedToConfig(vm, def)
317 if err != nil {
318 return nil, err
319 }
320 ret[k] = r
giof6ad2982024-08-23 17:42:49 +0400321 case KindCluster:
322 vm, ok := v.(map[string]any)
323 if !ok {
324 return nil, fmt.Errorf("expected map")
325 }
326 name, ok := vm["name"]
327 if !ok {
328 return nil, fmt.Errorf("expected cluster name")
329 }
330 ret[k] = name
gio3af43942024-04-16 08:13:50 +0400331 default:
332 return nil, fmt.Errorf("Should not reach!")
333 }
334 }
335 return ret, nil
336}
337
338func findNetwork(networks []Network, name string) (Network, error) {
339 for _, n := range networks {
340 if n.Name == name {
341 return n, nil
342 }
343 }
344 return Network{}, fmt.Errorf("Network not found: %s", name)
345}
giof6ad2982024-08-23 17:42:49 +0400346
347func findCluster(clusters []Cluster, name string) (*Cluster, error) {
348 if name == defaultClusterName {
349 return nil, nil
350 }
351 for _, c := range clusters {
352 if c.Name == name {
353 return &c, nil
354 }
355 }
356 return nil, fmt.Errorf("Cluster not found: %s", name)
357}
gio6481c902025-05-20 16:16:30 +0400358
359func GeneratePassword() (string, error) {
360 return password.Generate(20, 5, 0, false, true)
361}
gioe65d9a92025-06-19 09:02:32 +0400362
363func GenerateSketchSessionId() string {
364 u1, u2 := rand.Uint64(), rand.Uint64N(1<<16)
365 s := crock32.Encode(u1) + crock32.Encode(uint64(u2))
366 if len(s) < 16 {
367 s += strings.Repeat("0", 16-len(s))
368 }
369 return s[0:4] + "-" + s[4:8] + "-" + s[8:12] + "-" + s[12:16]
370}