blob: d31d9ff5f91b06e92b2318f83a989a06f29400db [file] [log] [blame]
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +04001package installer
2
3import (
gio3af43942024-04-16 08:13:50 +04004 "bytes"
5 "encoding/json"
6 "errors"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04007 "fmt"
gioefa0ed42024-06-13 12:31:43 +04008 "io"
gio3af43942024-04-16 08:13:50 +04009 "io/fs"
gio3af43942024-04-16 08:13:50 +040010 "net/http"
11 "path"
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +040012 "path/filepath"
giof8843412024-05-22 16:38:05 +040013 "strings"
gio69731e82024-08-01 14:15:55 +040014 "sync"
gioe72b54f2024-04-22 10:44:41 +040015
gioefa0ed42024-06-13 12:31:43 +040016 gio "github.com/giolekva/pcloud/core/installer/io"
gioe72b54f2024-04-22 10:44:41 +040017 "github.com/giolekva/pcloud/core/installer/soft"
gio778577f2024-04-29 09:44:38 +040018
giof8843412024-05-22 16:38:05 +040019 helmv2 "github.com/fluxcd/helm-controller/api/v2"
gio778577f2024-04-29 09:44:38 +040020 "sigs.k8s.io/yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040021)
22
gio5e49bb62024-07-20 10:43:19 +040023const (
24 configFileName = "config.yaml"
25 kustomizationFileName = "kustomization.yaml"
26 gitIgnoreFileName = ".gitignore"
27 includeEverything = "!*"
28)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040029
gio778577f2024-04-29 09:44:38 +040030var ErrorNotFound = errors.New("not found")
31
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040032type AppManager struct {
gio864b4332024-09-05 13:56:47 +040033 l sync.Locker
34 repoIO soft.RepoIO
35 nsc NamespaceCreator
36 jc JobCreator
37 hf HelmFetcher
38 vpnAPIClient VPNAPIClient
39 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040040}
41
giof8843412024-05-22 16:38:05 +040042func NewAppManager(
43 repoIO soft.RepoIO,
44 nsc NamespaceCreator,
45 jc JobCreator,
46 hf HelmFetcher,
gio864b4332024-09-05 13:56:47 +040047 vpnKeyGen VPNAPIClient,
giof8843412024-05-22 16:38:05 +040048 appDirRoot string,
49) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040050 return &AppManager{
gio69731e82024-08-01 14:15:55 +040051 &sync.Mutex{},
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040052 repoIO,
giof8843412024-05-22 16:38:05 +040053 nsc,
54 jc,
55 hf,
gio36b23b32024-08-25 12:20:54 +040056 vpnKeyGen,
gio308105e2024-04-19 13:12:13 +040057 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040058 }, nil
59}
60
gioe72b54f2024-04-22 10:44:41 +040061func (m *AppManager) Config() (EnvConfig, error) {
62 var cfg EnvConfig
63 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
64 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040065 } else {
66 return cfg, nil
67 }
68}
69
gio3cdee592024-04-17 10:15:56 +040070func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
71 var cfg AppInstanceConfig
gioe72b54f2024-04-22 10:44:41 +040072 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040073 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040074 } else {
75 return cfg, nil
76 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040077}
78
gio7fbd4ad2024-08-27 10:06:39 +040079func (m *AppManager) GetAllInstances() ([]AppInstanceConfig, error) {
gio09a3e5b2024-04-26 14:11:06 +040080 m.repoIO.Pull()
gioe72b54f2024-04-22 10:44:41 +040081 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040082 if err != nil {
83 return nil, err
84 }
gio3cdee592024-04-17 10:15:56 +040085 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040086 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040087 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
88 if err != nil {
89 return nil, err
90 }
91 cfg.Id = app
92 ret = append(ret, cfg)
93 }
94 return ret, nil
95}
96
gio7fbd4ad2024-08-27 10:06:39 +040097func (m *AppManager) GetAllAppInstances(name string) ([]AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040098 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio308105e2024-04-19 13:12:13 +040099 if err != nil {
giocb34ad22024-07-11 08:01:13 +0400100 if errors.Is(err, fs.ErrNotExist) {
101 return nil, nil
102 } else {
103 return nil, err
104 }
gio308105e2024-04-19 13:12:13 +0400105 }
106 ret := make([]AppInstanceConfig, 0)
107 for _, app := range kust.Resources {
108 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400109 if err != nil {
110 return nil, err
111 }
112 cfg.Id = app
113 if cfg.AppId == name {
114 ret = append(ret, cfg)
115 }
116 }
117 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400118}
119
gio7fbd4ad2024-08-27 10:06:39 +0400120func (m *AppManager) GetInstance(id string) (*AppInstanceConfig, error) {
121 appDir := filepath.Clean(filepath.Join(m.appDirRoot, id))
122 cfgPath := filepath.Join(appDir, "config.json")
123 // kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
124 // if err != nil {
125 // return nil, err
126 // }
127 // for _, app := range kust.Resources {
128 // if app == id {
129 // cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
130 cfg, err := m.appConfig(cfgPath)
gio3af43942024-04-16 08:13:50 +0400131 if err != nil {
gio778577f2024-04-29 09:44:38 +0400132 return nil, err
gio3af43942024-04-16 08:13:50 +0400133 }
gio7fbd4ad2024-08-27 10:06:39 +0400134 cfg.Id = id
135 return &cfg, err
136 // if err != nil {
137 // return nil, err
138 // }
139 // cfg.Id = id
140 // return &cfg, nil
141 // }
142 // }
143 // return nil, ErrorNotFound
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400144}
145
giof8843412024-05-22 16:38:05 +0400146func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
147 files, err := fs.ListDir(dir)
148 if err != nil {
149 return nil, err
150 }
151 cfg := CueAppData{}
152 for _, f := range files {
153 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
154 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
155 if err != nil {
156 return nil, err
157 }
158 cfg[f.Name()] = contents
159 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400160 }
gio308105e2024-04-19 13:12:13 +0400161 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400162}
163
giof8843412024-05-22 16:38:05 +0400164func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
165 cfg, err := GetCueAppData(m.repoIO, filepath.Join(m.appDirRoot, id))
166 if err != nil {
167 return nil, err
168 }
169 return NewCueEnvApp(cfg)
170}
171
gio3af43942024-04-16 08:13:50 +0400172type allocatePortReq struct {
173 Protocol string `json:"protocol"`
174 SourcePort int `json:"sourcePort"`
175 TargetService string `json:"targetService"`
176 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400177 Secret string `json:"secret,omitempty"`
178}
179
180type removePortReq struct {
181 Protocol string `json:"protocol"`
182 SourcePort int `json:"sourcePort"`
183 TargetService string `json:"targetService"`
184 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400185}
186
gioefa0ed42024-06-13 12:31:43 +0400187type reservePortResp struct {
188 Port int `json:"port"`
189 Secret string `json:"secret"`
190}
191
192func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
193 ret := map[string]reservePortResp{}
194 for p, reserveAddr := range ports {
195 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
196 if err != nil {
197 return nil, err
198 }
199 if resp.StatusCode != http.StatusOK {
200 var e bytes.Buffer
201 io.Copy(&e, resp.Body)
202 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
203 }
204 var r reservePortResp
205 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
206 return nil, err
207 }
208 ret[p] = r
209 }
210 return ret, nil
211}
212
213func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400214 for _, p := range ports {
215 var buf bytes.Buffer
216 req := allocatePortReq{
217 Protocol: p.Protocol,
218 SourcePort: p.SourcePort,
219 TargetService: p.TargetService,
220 TargetPort: p.TargetPort,
221 }
gioefa0ed42024-06-13 12:31:43 +0400222 allocator := ""
223 for n, r := range reservations {
224 if p.SourcePort == r.Port {
225 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400226 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400227 break
228 }
229 }
230 if allocator == "" {
231 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
232 }
giobd7ab0b2024-06-17 12:55:17 +0400233 if err := json.NewEncoder(&buf).Encode(req); err != nil {
234 return err
235 }
gioefa0ed42024-06-13 12:31:43 +0400236 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400237 if err != nil {
238 return err
239 }
240 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400241 var r bytes.Buffer
242 io.Copy(&r, resp.Body)
243 return fmt.Errorf("Could not allocate port %d, status code %d, message: %s", p.SourcePort, resp.StatusCode, r.String())
gio3af43942024-04-16 08:13:50 +0400244 }
245 }
246 return nil
247}
248
giocdfa3722024-06-13 20:10:14 +0400249func closePorts(ports []PortForward) error {
250 var retErr error
251 for _, p := range ports {
252 var buf bytes.Buffer
253 req := removePortReq{
254 Protocol: p.Protocol,
255 SourcePort: p.SourcePort,
256 TargetService: p.TargetService,
257 TargetPort: p.TargetPort,
258 }
259 if err := json.NewEncoder(&buf).Encode(req); err != nil {
260 retErr = err
261 continue
262 }
263 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
264 if err != nil {
265 retErr = err
266 continue
267 }
268 if resp.StatusCode != http.StatusOK {
269 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
270 continue
271 }
272 }
273 return retErr
274}
275
gioe72b54f2024-04-22 10:44:41 +0400276func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400277 for p := filepath.Clean(path); p != "/"; {
278 parent, child := filepath.Split(p)
279 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400280 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400281 if err != nil {
282 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400283 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400284 kust = &k
285 } else {
286 return err
287 }
288 }
289 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400290 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400291 return err
292 }
293 p = filepath.Clean(parent)
294 }
295 return nil
296}
297
gio778577f2024-04-29 09:44:38 +0400298type Resource struct {
giob4a3a192024-08-19 09:55:47 +0400299 Name string `json:"name"`
300 Namespace string `json:"namespace"`
301 Info string `json:"info"`
302 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400303}
304
305type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400306 Release Release
307 Helm []Resource
308 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400309}
310
gio3cdee592024-04-17 10:15:56 +0400311// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400312func installApp(
gioe72b54f2024-04-22 10:44:41 +0400313 repo soft.RepoIO,
314 appDir string,
315 name string,
316 config any,
gioe72b54f2024-04-22 10:44:41 +0400317 resources CueAppData,
318 data CueAppData,
giof8843412024-05-22 16:38:05 +0400319 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400320) error {
giof8843412024-05-22 16:38:05 +0400321 var o installOptions
322 for _, i := range opts {
323 i(&o)
324 }
325 dopts := []soft.DoOption{}
326 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400327 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
328 }
gio94904702024-07-26 16:58:34 +0400329 if o.NoPull {
330 dopts = append(dopts, soft.WithNoPull())
331 }
giof8843412024-05-22 16:38:05 +0400332 if o.NoPublish {
333 dopts = append(dopts, soft.WithNoCommit())
334 }
giof71a0832024-06-27 14:45:45 +0400335 if o.Force {
336 dopts = append(dopts, soft.WithForce())
337 }
gio9d66f322024-07-06 13:45:10 +0400338 if o.NoLock {
339 dopts = append(dopts, soft.WithNoLock())
340 }
giob4a3a192024-08-19 09:55:47 +0400341 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400342 if err := r.RemoveDir(appDir); err != nil {
343 return "", err
344 }
345 resourcesDir := path.Join(appDir, "resources")
346 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400347 return "", err
348 }
gio94904702024-07-26 16:58:34 +0400349 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400350 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400351 return err
gio5e49bb62024-07-20 10:43:19 +0400352 }
gioe72b54f2024-04-22 10:44:41 +0400353 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400354 return err
gio3af43942024-04-16 08:13:50 +0400355 }
gioe72b54f2024-04-22 10:44:41 +0400356 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400357 return err
gio308105e2024-04-19 13:12:13 +0400358 }
gioe72b54f2024-04-22 10:44:41 +0400359 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400360 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
gio94904702024-07-26 16:58:34 +0400361 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400362 }
363 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400364 if err != nil {
gio94904702024-07-26 16:58:34 +0400365 return err
gio3af43942024-04-16 08:13:50 +0400366 }
gio308105e2024-04-19 13:12:13 +0400367 defer w.Close()
368 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400369 return err
gio3af43942024-04-16 08:13:50 +0400370 }
371 }
gio94904702024-07-26 16:58:34 +0400372 return nil
373 }(); err != nil {
374 return "", err
gio308105e2024-04-19 13:12:13 +0400375 }
gio94904702024-07-26 16:58:34 +0400376 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400377 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400378 return err
gio308105e2024-04-19 13:12:13 +0400379 }
gioefa0ed42024-06-13 12:31:43 +0400380 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400381 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400382 appKust.AddResources(name)
383 w, err := r.Writer(path.Join(resourcesDir, name))
384 if err != nil {
gio94904702024-07-26 16:58:34 +0400385 return err
gio308105e2024-04-19 13:12:13 +0400386 }
387 defer w.Close()
388 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400389 return err
gio308105e2024-04-19 13:12:13 +0400390 }
391 }
gioe72b54f2024-04-22 10:44:41 +0400392 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400393 return err
gio3af43942024-04-16 08:13:50 +0400394 }
gio94904702024-07-26 16:58:34 +0400395 return nil
396 }(); err != nil {
397 return "", err
gio3af43942024-04-16 08:13:50 +0400398 }
gioe72b54f2024-04-22 10:44:41 +0400399 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400400 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400401 return err
gio3af43942024-04-16 08:13:50 +0400402}
403
gio3cdee592024-04-17 10:15:56 +0400404// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400405func (m *AppManager) Install(
406 app EnvApp,
407 instanceId string,
408 appDir string,
409 namespace string,
410 values map[string]any,
411 opts ...InstallOption,
412) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400413 o := &installOptions{}
414 for _, i := range opts {
415 i(o)
416 }
417 if !o.NoLock {
418 m.l.Lock()
419 defer m.l.Unlock()
420 }
gioefa0ed42024-06-13 12:31:43 +0400421 portFields := findPortFields(app.Schema())
422 fakeReservations := map[string]reservePortResp{}
423 for i, f := range portFields {
424 fakeReservations[f] = reservePortResp{Port: i}
425 }
426 if err := setPortFields(values, fakeReservations); err != nil {
427 return ReleaseResources{}, err
428 }
gio3af43942024-04-16 08:13:50 +0400429 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400430 if !o.NoPull {
431 if err := m.repoIO.Pull(); err != nil {
432 return ReleaseResources{}, err
433 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400434 }
gio94904702024-07-26 16:58:34 +0400435 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400436 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400437 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400438 }
gio0eaf2712024-04-14 13:08:46 +0400439 var env EnvConfig
440 if o.Env != nil {
441 env = *o.Env
442 } else {
443 var err error
444 env, err = m.Config()
445 if err != nil {
446 return ReleaseResources{}, err
447 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400448 }
giocb34ad22024-07-11 08:01:13 +0400449 var networks []Network
450 if o.Networks != nil {
451 networks = o.Networks
452 } else {
453 var err error
454 networks, err = m.CreateNetworks(env)
455 if err != nil {
456 return ReleaseResources{}, err
457 }
458 }
giof8843412024-05-22 16:38:05 +0400459 var lg LocalChartGenerator
460 if o.LG != nil {
461 lg = o.LG
462 } else {
463 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
464 }
gio3cdee592024-04-17 10:15:56 +0400465 release := Release{
466 AppInstanceId: instanceId,
467 Namespace: namespace,
468 RepoAddr: m.repoIO.FullAddress(),
469 AppDir: appDir,
470 }
gio864b4332024-09-05 13:56:47 +0400471 rendered, err := app.Render(release, env, networks, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400472 if err != nil {
gio778577f2024-04-29 09:44:38 +0400473 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400474 }
gioefa0ed42024-06-13 12:31:43 +0400475 reservators := map[string]string{}
476 allocators := map[string]string{}
477 for _, pf := range rendered.Ports {
478 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
479 allocators[portFields[pf.SourcePort]] = pf.Allocator
480 }
481 portReservations, err := reservePorts(reservators)
482 if err != nil {
483 return ReleaseResources{}, err
484 }
485 if err := setPortFields(values, portReservations); err != nil {
486 return ReleaseResources{}, err
487 }
gio7841f4f2024-07-26 19:53:49 +0400488 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400489 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
490 if o.FetchContainerImages {
491 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
492 return ReleaseResources{}, err
493 }
gio0eaf2712024-04-14 13:08:46 +0400494 }
giof71a0832024-06-27 14:45:45 +0400495 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
496 if err != nil {
giof8843412024-05-22 16:38:05 +0400497 return ReleaseResources{}, err
498 }
giof71a0832024-06-27 14:45:45 +0400499 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400500 if o.FetchContainerImages {
501 release.ImageRegistry = imageRegistry
502 }
gio864b4332024-09-05 13:56:47 +0400503 rendered, err = app.Render(release, env, networks, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400504 if err != nil {
505 return ReleaseResources{}, err
506 }
gio94904702024-07-26 16:58:34 +0400507 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400508 return ReleaseResources{}, err
509 }
gioff2a29a2024-05-01 17:06:42 +0400510 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400511 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400512 return ReleaseResources{}, err
513 }
gio778577f2024-04-29 09:44:38 +0400514 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400515 Release: rendered.Config.Release,
516 RenderedRaw: rendered.Raw,
517 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400518 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400519}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400520
gio778577f2024-04-29 09:44:38 +0400521type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400522 Metadata struct {
523 Name string `json:"name"`
524 Namespace string `json:"namespace"`
525 Annotations map[string]string `json:"annotations"`
526 } `json:"metadata"`
527 Kind string `json:"kind"`
528 Status struct {
gio778577f2024-04-29 09:44:38 +0400529 Conditions []struct {
530 Type string `json:"type"`
531 Status string `json:"status"`
532 } `json:"conditions"`
533 } `json:"status,omitempty"`
534}
535
536func extractHelm(resources CueAppData) []Resource {
537 ret := make([]Resource, 0, len(resources))
538 for _, contents := range resources {
539 var h helmRelease
540 if err := yaml.Unmarshal(contents, &h); err != nil {
541 panic(err) // TODO(gio): handle
542 }
gio0eaf2712024-04-14 13:08:46 +0400543 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400544 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400545 Name: h.Metadata.Name,
546 Namespace: h.Metadata.Namespace,
547 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
548 Annotations: nil,
giof9f0bee2024-06-11 20:10:05 +0400549 }
550 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400551 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400552 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
553 if ok && len(info) != 0 {
554 res.Info = info
555 }
556 }
557 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400558 }
gio778577f2024-04-29 09:44:38 +0400559 }
560 return ret
561}
562
giof8843412024-05-22 16:38:05 +0400563// TODO(gio): take app configuration from the repo
564func (m *AppManager) Update(
565 instanceId string,
566 values map[string]any,
567 opts ...InstallOption,
568) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400569 m.l.Lock()
570 defer m.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400571 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400572 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400573 }
gio3cdee592024-04-17 10:15:56 +0400574 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400575 if err != nil {
gio778577f2024-04-29 09:44:38 +0400576 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400577 }
gio308105e2024-04-19 13:12:13 +0400578 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400579 app, err := m.GetInstanceApp(instanceId)
580 if err != nil {
581 return ReleaseResources{}, err
582 }
gio308105e2024-04-19 13:12:13 +0400583 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400584 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400585 if err != nil {
gio778577f2024-04-29 09:44:38 +0400586 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400587 }
giocdfa3722024-06-13 20:10:14 +0400588 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400589 if err != nil {
590 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400591 }
giocb34ad22024-07-11 08:01:13 +0400592 networks, err := m.CreateNetworks(env)
593 if err != nil {
594 return ReleaseResources{}, err
595 }
gio864b4332024-09-05 13:56:47 +0400596 rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts, m.vpnAPIClient)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400597 if err != nil {
gio778577f2024-04-29 09:44:38 +0400598 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400599 }
gio94904702024-07-26 16:58:34 +0400600 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
601 return ReleaseResources{}, err
602 }
603 return ReleaseResources{
604 Release: rendered.Config.Release,
605 RenderedRaw: rendered.Raw,
606 Helm: extractHelm(rendered.Resources),
607 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400608}
609
610func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400611 m.l.Lock()
612 defer m.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400613 if err := m.repoIO.Pull(); err != nil {
614 return err
615 }
gio864b4332024-09-05 13:56:47 +0400616 var cfg renderedInstance
giob4a3a192024-08-19 09:55:47 +0400617 if _, err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400618 instanceDir := filepath.Join(m.appDirRoot, instanceId)
619 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
620 if err != nil {
621 return "", err
622 }
gio864b4332024-09-05 13:56:47 +0400623 cfg = renderedCfg
giocdfa3722024-06-13 20:10:14 +0400624 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400625 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400626 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400627 if err != nil {
628 return "", err
629 }
630 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400631 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400632 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400633 }); err != nil {
634 return err
635 }
gio864b4332024-09-05 13:56:47 +0400636 if err := closePorts(cfg.PortForward); err != nil {
giocdfa3722024-06-13 20:10:14 +0400637 return err
638 }
gio864b4332024-09-05 13:56:47 +0400639 for vmName, vmCfg := range cfg.Out.VM {
640 if vmCfg.VPN.Enabled {
641 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil {
642 return err
643 }
644 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
645 return err
646 }
647 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil {
648 return err
649 }
650 }
651 }
giocdfa3722024-06-13 20:10:14 +0400652 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400653}
654
giocb34ad22024-07-11 08:01:13 +0400655func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
656 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400657 {
giocdfa3722024-06-13 20:10:14 +0400658 Name: "Public",
659 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
660 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
661 Domain: env.Domain,
662 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
663 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
664 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400665 },
gio7841f4f2024-07-26 19:53:49 +0400666 }
667 if env.PrivateDomain != "" {
668 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400669 Name: "Private",
670 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
671 Domain: env.PrivateDomain,
672 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
673 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
674 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400675 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400676 }
gio7fbd4ad2024-08-27 10:06:39 +0400677 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400678 if err != nil {
679 return nil, err
680 }
681 for _, a := range n {
682 ret = append(ret, Network{
683 Name: a.Input["name"].(string),
684 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
685 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
686 Domain: a.Input["domain"].(string),
687 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
688 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
689 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
690 })
691 }
692 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400693}
gio3cdee592024-04-17 10:15:56 +0400694
gio0eaf2712024-04-14 13:08:46 +0400695type installOptions struct {
gio94904702024-07-26 16:58:34 +0400696 NoPull bool
giof8843412024-05-22 16:38:05 +0400697 NoPublish bool
698 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400699 Networks []Network
giof8843412024-05-22 16:38:05 +0400700 Branch string
701 LG LocalChartGenerator
702 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400703 Force bool
gio9d66f322024-07-06 13:45:10 +0400704 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400705}
706
707type InstallOption func(*installOptions)
708
709func WithConfig(env *EnvConfig) InstallOption {
710 return func(o *installOptions) {
711 o.Env = env
712 }
713}
714
giocb34ad22024-07-11 08:01:13 +0400715func WithNetworks(networks []Network) InstallOption {
716 return func(o *installOptions) {
717 o.Networks = networks
718 }
719}
720
gio23bdc1b2024-07-11 16:07:47 +0400721func WithNoNetworks() InstallOption {
722 return WithNetworks([]Network{})
723}
724
gio0eaf2712024-04-14 13:08:46 +0400725func WithBranch(branch string) InstallOption {
726 return func(o *installOptions) {
727 o.Branch = branch
728 }
729}
730
giof71a0832024-06-27 14:45:45 +0400731func WithForce() InstallOption {
732 return func(o *installOptions) {
733 o.Force = true
734 }
735}
736
giof8843412024-05-22 16:38:05 +0400737func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
738 return func(o *installOptions) {
739 o.LG = lg
740 }
741}
742
743func WithFetchContainerImages() InstallOption {
744 return func(o *installOptions) {
745 o.FetchContainerImages = true
746 }
747}
748
749func WithNoPublish() InstallOption {
750 return func(o *installOptions) {
751 o.NoPublish = true
752 }
753}
754
gio94904702024-07-26 16:58:34 +0400755func WithNoPull() InstallOption {
756 return func(o *installOptions) {
757 o.NoPull = true
758 }
759}
760
gio9d66f322024-07-06 13:45:10 +0400761func WithNoLock() InstallOption {
762 return func(o *installOptions) {
763 o.NoLock = true
764 }
765}
766
giof8843412024-05-22 16:38:05 +0400767// InfraAppmanager
768
769type InfraAppManager struct {
770 repoIO soft.RepoIO
771 nsc NamespaceCreator
772 hf HelmFetcher
773 lg LocalChartGenerator
774}
775
776func NewInfraAppManager(
777 repoIO soft.RepoIO,
778 nsc NamespaceCreator,
779 hf HelmFetcher,
780 lg LocalChartGenerator,
781) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400782 return &InfraAppManager{
783 repoIO,
giof8843412024-05-22 16:38:05 +0400784 nsc,
785 hf,
786 lg,
gio3cdee592024-04-17 10:15:56 +0400787 }, nil
788}
789
790func (m *InfraAppManager) Config() (InfraConfig, error) {
791 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400792 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400793 return InfraConfig{}, err
794 } else {
795 return cfg, nil
796 }
797}
798
gioe72b54f2024-04-22 10:44:41 +0400799func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
800 var cfg InfraAppInstanceConfig
801 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
802 return InfraAppInstanceConfig{}, err
803 } else {
804 return cfg, nil
805 }
806}
807
808func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
809 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
810 if err != nil {
811 return InfraAppInstanceConfig{}, err
812 }
813 for _, app := range kust.Resources {
814 if app == id {
815 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
816 if err != nil {
817 return InfraAppInstanceConfig{}, err
818 }
819 cfg.Id = id
820 return cfg, nil
821 }
822 }
823 return InfraAppInstanceConfig{}, nil
824}
825
gio778577f2024-04-29 09:44:38 +0400826func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400827 appDir = filepath.Clean(appDir)
828 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400829 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400830 }
giof8843412024-05-22 16:38:05 +0400831 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400832 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400833 }
834 infra, err := m.Config()
835 if err != nil {
gio778577f2024-04-29 09:44:38 +0400836 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400837 }
838 release := Release{
839 Namespace: namespace,
840 RepoAddr: m.repoIO.FullAddress(),
841 AppDir: appDir,
842 }
gio7841f4f2024-07-26 19:53:49 +0400843 networks := m.CreateNetworks(infra)
844 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +0400845 if err != nil {
846 return ReleaseResources{}, err
847 }
giof71a0832024-06-27 14:45:45 +0400848 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
849 if err != nil {
giof8843412024-05-22 16:38:05 +0400850 return ReleaseResources{}, err
851 }
giof71a0832024-06-27 14:45:45 +0400852 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +0400853 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400854 if err != nil {
gio778577f2024-04-29 09:44:38 +0400855 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400856 }
gio94904702024-07-26 16:58:34 +0400857 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
858 return ReleaseResources{}, err
859 }
860 return ReleaseResources{
861 Release: rendered.Config.Release,
862 RenderedRaw: rendered.Raw,
863 Helm: extractHelm(rendered.Resources),
864 }, nil
gioe72b54f2024-04-22 10:44:41 +0400865}
866
giof8843412024-05-22 16:38:05 +0400867// TODO(gio): take app configuration from the repo
868func (m *InfraAppManager) Update(
869 instanceId string,
870 values map[string]any,
871 opts ...InstallOption,
872) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400873 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400874 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400875 }
gio7841f4f2024-07-26 19:53:49 +0400876 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +0400877 if err != nil {
gio778577f2024-04-29 09:44:38 +0400878 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400879 }
880 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400881 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
882 if err != nil {
883 return ReleaseResources{}, err
884 }
885 app, err := NewCueInfraApp(appCfg)
886 if err != nil {
887 return ReleaseResources{}, err
888 }
gioe72b54f2024-04-22 10:44:41 +0400889 instanceConfigPath := filepath.Join(instanceDir, "config.json")
890 config, err := m.appConfig(instanceConfigPath)
891 if err != nil {
gio778577f2024-04-29 09:44:38 +0400892 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400893 }
giocdfa3722024-06-13 20:10:14 +0400894 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400895 if err != nil {
896 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400897 }
gio7841f4f2024-07-26 19:53:49 +0400898 networks := m.CreateNetworks(infra)
899 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400900 if err != nil {
gio778577f2024-04-29 09:44:38 +0400901 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400902 }
gio94904702024-07-26 16:58:34 +0400903 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
904 return ReleaseResources{}, err
905 }
906 return ReleaseResources{
907 Release: rendered.Config.Release,
908 RenderedRaw: rendered.Raw,
909 Helm: extractHelm(rendered.Resources),
910 }, nil
gio3cdee592024-04-17 10:15:56 +0400911}
giof8843412024-05-22 16:38:05 +0400912
gio7841f4f2024-07-26 19:53:49 +0400913func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
914 return []InfraNetwork{
915 {
916 Name: "Public",
917 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
918 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
919 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
920 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
921 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
922 },
923 }
924}
925
giof8843412024-05-22 16:38:05 +0400926func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
927 ret := make(map[string]string)
928 for name, chart := range charts.Git {
929 chartRoot := filepath.Join(root, name)
930 ret[name] = chartRoot
931 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
932 return nil, err
933 }
934 }
935 return ret, nil
936}
937
938func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
939 ret := make(map[string]helmv2.HelmChartTemplateSpec)
940 for name, path := range charts {
941 ret[name] = g.Generate(path)
942 }
943 return ret
944}
945
946func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
947 for _, img := range imgs {
948 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
949 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
950 "skopeo",
951 "--insecure-policy",
952 "copy",
953 "--dest-tls-verify=false", // TODO(gio): enable
954 "--multi-arch=all",
955 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
956 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
957 }); err != nil {
958 return err
959 }
960 }
961 return nil
962}
963
964type renderedInstance struct {
965 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400966 PortForward []PortForward `json:"portForward"`
gio864b4332024-09-05 13:56:47 +0400967 Out outRendered `json:"out"`
968}
969
970type outRendered struct {
971 VM map[string]vmRendered `json:"vm"`
972}
973
974type vmRendered struct {
975 Username string `json:"username"`
976 VPN struct {
977 Enabled bool `json:"enabled"`
978 AuthKey string `json:"authKey"`
979 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +0400980}
981
giocdfa3722024-06-13 20:10:14 +0400982func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400983 r, err := fs.Reader(path)
984 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400985 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400986 }
987 defer r.Close()
988 var cfg renderedInstance
989 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400990 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400991 }
giocdfa3722024-06-13 20:10:14 +0400992 return cfg, nil
giof8843412024-05-22 16:38:05 +0400993}
gioefa0ed42024-06-13 12:31:43 +0400994
995func findPortFields(scm Schema) []string {
996 switch scm.Kind() {
997 case KindBoolean:
998 return []string{}
999 case KindInt:
1000 return []string{}
1001 case KindString:
1002 return []string{}
1003 case KindStruct:
1004 ret := []string{}
1005 for _, f := range scm.Fields() {
1006 for _, p := range findPortFields(f.Schema) {
1007 if p == "" {
1008 ret = append(ret, f.Name)
1009 } else {
1010 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1011 }
1012 }
1013 }
1014 return ret
1015 case KindNetwork:
1016 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001017 case KindMultiNetwork:
1018 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001019 case KindAuth:
1020 return []string{}
1021 case KindSSHKey:
1022 return []string{}
1023 case KindNumber:
1024 return []string{}
1025 case KindArrayString:
1026 return []string{}
1027 case KindPort:
1028 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001029 case KindVPNAuthKey:
1030 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001031 default:
1032 panic("MUST NOT REACH!")
1033 }
1034}
1035
1036func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1037 for p, r := range ports {
1038 if err := setPortField(values, p, r.Port); err != nil {
1039 return err
1040 }
1041 }
1042 return nil
1043}
1044
1045func setPortField(values map[string]any, field string, port int) error {
1046 f := strings.SplitN(field, ".", 2)
1047 if len(f) == 2 {
1048 var sub map[string]any
1049 if s, ok := values[f[0]]; ok {
1050 sub, ok = s.(map[string]any)
1051 if !ok {
1052 return fmt.Errorf("expected map")
1053 }
1054 } else {
1055 sub = map[string]any{}
1056 values[f[0]] = sub
1057 }
1058 if err := setPortField(sub, f[1], port); err != nil {
1059 return err
1060 }
1061 } else {
1062 values[f[0]] = port
1063 }
1064 return nil
1065}