blob: 6cf1da1cbf3049015169824f7605ec3ec6d79194 [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"
gioe72b54f2024-04-22 10:44:41 +040014
gioefa0ed42024-06-13 12:31:43 +040015 gio "github.com/giolekva/pcloud/core/installer/io"
gioe72b54f2024-04-22 10:44:41 +040016 "github.com/giolekva/pcloud/core/installer/soft"
gio778577f2024-04-29 09:44:38 +040017
giof8843412024-05-22 16:38:05 +040018 helmv2 "github.com/fluxcd/helm-controller/api/v2"
gio778577f2024-04-29 09:44:38 +040019 "sigs.k8s.io/yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040020)
21
gio5e49bb62024-07-20 10:43:19 +040022const (
23 configFileName = "config.yaml"
24 kustomizationFileName = "kustomization.yaml"
25 gitIgnoreFileName = ".gitignore"
26 includeEverything = "!*"
27)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040028
gio778577f2024-04-29 09:44:38 +040029var ErrorNotFound = errors.New("not found")
30
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040031type AppManager struct {
gioe72b54f2024-04-22 10:44:41 +040032 repoIO soft.RepoIO
giof8843412024-05-22 16:38:05 +040033 nsc NamespaceCreator
34 jc JobCreator
35 hf HelmFetcher
gio308105e2024-04-19 13:12:13 +040036 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040037}
38
giof8843412024-05-22 16:38:05 +040039func NewAppManager(
40 repoIO soft.RepoIO,
41 nsc NamespaceCreator,
42 jc JobCreator,
43 hf HelmFetcher,
44 appDirRoot string,
45) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040046 return &AppManager{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040047 repoIO,
giof8843412024-05-22 16:38:05 +040048 nsc,
49 jc,
50 hf,
gio308105e2024-04-19 13:12:13 +040051 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040052 }, nil
53}
54
gioe72b54f2024-04-22 10:44:41 +040055func (m *AppManager) Config() (EnvConfig, error) {
56 var cfg EnvConfig
57 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
58 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040059 } else {
60 return cfg, nil
61 }
62}
63
gio3cdee592024-04-17 10:15:56 +040064func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
65 var cfg AppInstanceConfig
gioe72b54f2024-04-22 10:44:41 +040066 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040067 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040068 } else {
69 return cfg, nil
70 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040071}
72
gio308105e2024-04-19 13:12:13 +040073func (m *AppManager) FindAllInstances() ([]AppInstanceConfig, error) {
gio09a3e5b2024-04-26 14:11:06 +040074 m.repoIO.Pull()
gioe72b54f2024-04-22 10:44:41 +040075 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040076 if err != nil {
77 return nil, err
78 }
gio3cdee592024-04-17 10:15:56 +040079 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040080 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040081 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
82 if err != nil {
83 return nil, err
84 }
85 cfg.Id = app
86 ret = append(ret, cfg)
87 }
88 return ret, nil
89}
90
91func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040092 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio308105e2024-04-19 13:12:13 +040093 if err != nil {
giocb34ad22024-07-11 08:01:13 +040094 if errors.Is(err, fs.ErrNotExist) {
95 return nil, nil
96 } else {
97 return nil, err
98 }
gio308105e2024-04-19 13:12:13 +040099 }
100 ret := make([]AppInstanceConfig, 0)
101 for _, app := range kust.Resources {
102 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400103 if err != nil {
104 return nil, err
105 }
106 cfg.Id = app
107 if cfg.AppId == name {
108 ret = append(ret, cfg)
109 }
110 }
111 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400112}
113
gio778577f2024-04-29 09:44:38 +0400114func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +0400115 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +0400116 if err != nil {
gio778577f2024-04-29 09:44:38 +0400117 return nil, err
gio3af43942024-04-16 08:13:50 +0400118 }
119 for _, app := range kust.Resources {
120 if app == id {
gio308105e2024-04-19 13:12:13 +0400121 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400122 if err != nil {
gio778577f2024-04-29 09:44:38 +0400123 return nil, err
gio3af43942024-04-16 08:13:50 +0400124 }
125 cfg.Id = id
gio778577f2024-04-29 09:44:38 +0400126 return &cfg, nil
gio3af43942024-04-16 08:13:50 +0400127 }
128 }
gio778577f2024-04-29 09:44:38 +0400129 return nil, ErrorNotFound
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400130}
131
giof8843412024-05-22 16:38:05 +0400132func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
133 files, err := fs.ListDir(dir)
134 if err != nil {
135 return nil, err
136 }
137 cfg := CueAppData{}
138 for _, f := range files {
139 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
140 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
141 if err != nil {
142 return nil, err
143 }
144 cfg[f.Name()] = contents
145 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400146 }
gio308105e2024-04-19 13:12:13 +0400147 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400148}
149
giof8843412024-05-22 16:38:05 +0400150func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
151 cfg, err := GetCueAppData(m.repoIO, filepath.Join(m.appDirRoot, id))
152 if err != nil {
153 return nil, err
154 }
155 return NewCueEnvApp(cfg)
156}
157
gio3af43942024-04-16 08:13:50 +0400158type allocatePortReq struct {
159 Protocol string `json:"protocol"`
160 SourcePort int `json:"sourcePort"`
161 TargetService string `json:"targetService"`
162 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400163 Secret string `json:"secret,omitempty"`
164}
165
166type removePortReq struct {
167 Protocol string `json:"protocol"`
168 SourcePort int `json:"sourcePort"`
169 TargetService string `json:"targetService"`
170 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400171}
172
gioefa0ed42024-06-13 12:31:43 +0400173type reservePortResp struct {
174 Port int `json:"port"`
175 Secret string `json:"secret"`
176}
177
178func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
179 ret := map[string]reservePortResp{}
180 for p, reserveAddr := range ports {
181 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
182 if err != nil {
183 return nil, err
184 }
185 if resp.StatusCode != http.StatusOK {
186 var e bytes.Buffer
187 io.Copy(&e, resp.Body)
188 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
189 }
190 var r reservePortResp
191 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
192 return nil, err
193 }
194 ret[p] = r
195 }
196 return ret, nil
197}
198
199func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400200 for _, p := range ports {
201 var buf bytes.Buffer
202 req := allocatePortReq{
203 Protocol: p.Protocol,
204 SourcePort: p.SourcePort,
205 TargetService: p.TargetService,
206 TargetPort: p.TargetPort,
207 }
gioefa0ed42024-06-13 12:31:43 +0400208 allocator := ""
209 for n, r := range reservations {
210 if p.SourcePort == r.Port {
211 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400212 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400213 break
214 }
215 }
216 if allocator == "" {
217 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
218 }
giobd7ab0b2024-06-17 12:55:17 +0400219 if err := json.NewEncoder(&buf).Encode(req); err != nil {
220 return err
221 }
gioefa0ed42024-06-13 12:31:43 +0400222 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400223 if err != nil {
224 return err
225 }
226 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400227 var r bytes.Buffer
228 io.Copy(&r, resp.Body)
229 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 +0400230 }
231 }
232 return nil
233}
234
giocdfa3722024-06-13 20:10:14 +0400235func closePorts(ports []PortForward) error {
236 var retErr error
237 for _, p := range ports {
238 var buf bytes.Buffer
239 req := removePortReq{
240 Protocol: p.Protocol,
241 SourcePort: p.SourcePort,
242 TargetService: p.TargetService,
243 TargetPort: p.TargetPort,
244 }
245 if err := json.NewEncoder(&buf).Encode(req); err != nil {
246 retErr = err
247 continue
248 }
249 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
250 if err != nil {
251 retErr = err
252 continue
253 }
254 if resp.StatusCode != http.StatusOK {
255 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
256 continue
257 }
258 }
259 return retErr
260}
261
gioe72b54f2024-04-22 10:44:41 +0400262func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400263 for p := filepath.Clean(path); p != "/"; {
264 parent, child := filepath.Split(p)
265 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400266 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400267 if err != nil {
268 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400269 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400270 kust = &k
271 } else {
272 return err
273 }
274 }
275 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400276 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400277 return err
278 }
279 p = filepath.Clean(parent)
280 }
281 return nil
282}
283
gio778577f2024-04-29 09:44:38 +0400284type Resource struct {
285 Name string `json:"name"`
286 Namespace string `json:"namespace"`
giof9f0bee2024-06-11 20:10:05 +0400287 Info string `json:"info"`
gio778577f2024-04-29 09:44:38 +0400288}
289
290type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400291 Release Release
292 Helm []Resource
293 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400294}
295
gio3cdee592024-04-17 10:15:56 +0400296// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400297func installApp(
gioe72b54f2024-04-22 10:44:41 +0400298 repo soft.RepoIO,
299 appDir string,
300 name string,
301 config any,
gioe72b54f2024-04-22 10:44:41 +0400302 resources CueAppData,
303 data CueAppData,
giof8843412024-05-22 16:38:05 +0400304 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400305) error {
giof8843412024-05-22 16:38:05 +0400306 var o installOptions
307 for _, i := range opts {
308 i(&o)
309 }
310 dopts := []soft.DoOption{}
311 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400312 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
313 }
gio94904702024-07-26 16:58:34 +0400314 if o.NoPull {
315 dopts = append(dopts, soft.WithNoPull())
316 }
giof8843412024-05-22 16:38:05 +0400317 if o.NoPublish {
318 dopts = append(dopts, soft.WithNoCommit())
319 }
giof71a0832024-06-27 14:45:45 +0400320 if o.Force {
321 dopts = append(dopts, soft.WithForce())
322 }
gio9d66f322024-07-06 13:45:10 +0400323 if o.NoLock {
324 dopts = append(dopts, soft.WithNoLock())
325 }
gio94904702024-07-26 16:58:34 +0400326 return repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400327 if err := r.RemoveDir(appDir); err != nil {
328 return "", err
329 }
330 resourcesDir := path.Join(appDir, "resources")
331 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400332 return "", err
333 }
gio94904702024-07-26 16:58:34 +0400334 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400335 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400336 return err
gio5e49bb62024-07-20 10:43:19 +0400337 }
gioe72b54f2024-04-22 10:44:41 +0400338 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400339 return err
gio3af43942024-04-16 08:13:50 +0400340 }
gioe72b54f2024-04-22 10:44:41 +0400341 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400342 return err
gio308105e2024-04-19 13:12:13 +0400343 }
gioe72b54f2024-04-22 10:44:41 +0400344 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400345 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
gio94904702024-07-26 16:58:34 +0400346 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400347 }
348 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400349 if err != nil {
gio94904702024-07-26 16:58:34 +0400350 return err
gio3af43942024-04-16 08:13:50 +0400351 }
gio308105e2024-04-19 13:12:13 +0400352 defer w.Close()
353 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400354 return err
gio3af43942024-04-16 08:13:50 +0400355 }
356 }
gio94904702024-07-26 16:58:34 +0400357 return nil
358 }(); err != nil {
359 return "", err
gio308105e2024-04-19 13:12:13 +0400360 }
gio94904702024-07-26 16:58:34 +0400361 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400362 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400363 return err
gio308105e2024-04-19 13:12:13 +0400364 }
gioefa0ed42024-06-13 12:31:43 +0400365 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400366 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400367 appKust.AddResources(name)
368 w, err := r.Writer(path.Join(resourcesDir, name))
369 if err != nil {
gio94904702024-07-26 16:58:34 +0400370 return err
gio308105e2024-04-19 13:12:13 +0400371 }
372 defer w.Close()
373 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400374 return err
gio308105e2024-04-19 13:12:13 +0400375 }
376 }
gioe72b54f2024-04-22 10:44:41 +0400377 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400378 return err
gio3af43942024-04-16 08:13:50 +0400379 }
gio94904702024-07-26 16:58:34 +0400380 return nil
381 }(); err != nil {
382 return "", err
gio3af43942024-04-16 08:13:50 +0400383 }
gioe72b54f2024-04-22 10:44:41 +0400384 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400385 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400386}
387
gio3cdee592024-04-17 10:15:56 +0400388// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400389func (m *AppManager) Install(
390 app EnvApp,
391 instanceId string,
392 appDir string,
393 namespace string,
394 values map[string]any,
395 opts ...InstallOption,
396) (ReleaseResources, error) {
gioefa0ed42024-06-13 12:31:43 +0400397 portFields := findPortFields(app.Schema())
398 fakeReservations := map[string]reservePortResp{}
399 for i, f := range portFields {
400 fakeReservations[f] = reservePortResp{Port: i}
401 }
402 if err := setPortFields(values, fakeReservations); err != nil {
403 return ReleaseResources{}, err
404 }
gio0eaf2712024-04-14 13:08:46 +0400405 o := &installOptions{}
406 for _, i := range opts {
407 i(o)
408 }
gio3af43942024-04-16 08:13:50 +0400409 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400410 if !o.NoPull {
411 if err := m.repoIO.Pull(); err != nil {
412 return ReleaseResources{}, err
413 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400414 }
gio94904702024-07-26 16:58:34 +0400415 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400416 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400417 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400418 }
gio0eaf2712024-04-14 13:08:46 +0400419 var env EnvConfig
420 if o.Env != nil {
421 env = *o.Env
422 } else {
423 var err error
424 env, err = m.Config()
425 if err != nil {
426 return ReleaseResources{}, err
427 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400428 }
giocb34ad22024-07-11 08:01:13 +0400429 var networks []Network
430 if o.Networks != nil {
431 networks = o.Networks
432 } else {
433 var err error
434 networks, err = m.CreateNetworks(env)
435 if err != nil {
436 return ReleaseResources{}, err
437 }
438 }
giof8843412024-05-22 16:38:05 +0400439 var lg LocalChartGenerator
440 if o.LG != nil {
441 lg = o.LG
442 } else {
443 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
444 }
gio3cdee592024-04-17 10:15:56 +0400445 release := Release{
446 AppInstanceId: instanceId,
447 Namespace: namespace,
448 RepoAddr: m.repoIO.FullAddress(),
449 AppDir: appDir,
450 }
giocb34ad22024-07-11 08:01:13 +0400451 rendered, err := app.Render(release, env, networks, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400452 if err != nil {
gio778577f2024-04-29 09:44:38 +0400453 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400454 }
gioefa0ed42024-06-13 12:31:43 +0400455 reservators := map[string]string{}
456 allocators := map[string]string{}
457 for _, pf := range rendered.Ports {
458 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
459 allocators[portFields[pf.SourcePort]] = pf.Allocator
460 }
461 portReservations, err := reservePorts(reservators)
462 if err != nil {
463 return ReleaseResources{}, err
464 }
465 if err := setPortFields(values, portReservations); err != nil {
466 return ReleaseResources{}, err
467 }
gio7841f4f2024-07-26 19:53:49 +0400468 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400469 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
470 if o.FetchContainerImages {
471 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
472 return ReleaseResources{}, err
473 }
gio0eaf2712024-04-14 13:08:46 +0400474 }
giof71a0832024-06-27 14:45:45 +0400475 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
476 if err != nil {
giof8843412024-05-22 16:38:05 +0400477 return ReleaseResources{}, err
478 }
giof71a0832024-06-27 14:45:45 +0400479 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400480 if o.FetchContainerImages {
481 release.ImageRegistry = imageRegistry
482 }
giocb34ad22024-07-11 08:01:13 +0400483 rendered, err = app.Render(release, env, networks, values, localCharts)
giof8843412024-05-22 16:38:05 +0400484 if err != nil {
485 return ReleaseResources{}, err
486 }
gio94904702024-07-26 16:58:34 +0400487 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400488 return ReleaseResources{}, err
489 }
gioff2a29a2024-05-01 17:06:42 +0400490 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400491 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400492 return ReleaseResources{}, err
493 }
gio778577f2024-04-29 09:44:38 +0400494 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400495 Release: rendered.Config.Release,
496 RenderedRaw: rendered.Raw,
497 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400498 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400499}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400500
gio778577f2024-04-29 09:44:38 +0400501type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400502 Metadata struct {
503 Name string `json:"name"`
504 Namespace string `json:"namespace"`
505 Annotations map[string]string `json:"annotations"`
506 } `json:"metadata"`
507 Kind string `json:"kind"`
508 Status struct {
gio778577f2024-04-29 09:44:38 +0400509 Conditions []struct {
510 Type string `json:"type"`
511 Status string `json:"status"`
512 } `json:"conditions"`
513 } `json:"status,omitempty"`
514}
515
516func extractHelm(resources CueAppData) []Resource {
517 ret := make([]Resource, 0, len(resources))
518 for _, contents := range resources {
519 var h helmRelease
520 if err := yaml.Unmarshal(contents, &h); err != nil {
521 panic(err) // TODO(gio): handle
522 }
gio0eaf2712024-04-14 13:08:46 +0400523 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400524 res := Resource{
525 Name: h.Metadata.Name,
526 Namespace: h.Metadata.Namespace,
527 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
528 }
529 if h.Metadata.Annotations != nil {
530 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
531 if ok && len(info) != 0 {
532 res.Info = info
533 }
534 }
535 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400536 }
gio778577f2024-04-29 09:44:38 +0400537 }
538 return ret
539}
540
giof8843412024-05-22 16:38:05 +0400541// TODO(gio): take app configuration from the repo
542func (m *AppManager) Update(
543 instanceId string,
544 values map[string]any,
545 opts ...InstallOption,
546) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400547 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400548 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400549 }
gio3cdee592024-04-17 10:15:56 +0400550 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400551 if err != nil {
gio778577f2024-04-29 09:44:38 +0400552 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400553 }
gio308105e2024-04-19 13:12:13 +0400554 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400555 app, err := m.GetInstanceApp(instanceId)
556 if err != nil {
557 return ReleaseResources{}, err
558 }
gio308105e2024-04-19 13:12:13 +0400559 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400560 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400561 if err != nil {
gio778577f2024-04-29 09:44:38 +0400562 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400563 }
giocdfa3722024-06-13 20:10:14 +0400564 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400565 if err != nil {
566 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400567 }
giocb34ad22024-07-11 08:01:13 +0400568 networks, err := m.CreateNetworks(env)
569 if err != nil {
570 return ReleaseResources{}, err
571 }
572 rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400573 if err != nil {
gio778577f2024-04-29 09:44:38 +0400574 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400575 }
gio94904702024-07-26 16:58:34 +0400576 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
577 return ReleaseResources{}, err
578 }
579 return ReleaseResources{
580 Release: rendered.Config.Release,
581 RenderedRaw: rendered.Raw,
582 Helm: extractHelm(rendered.Resources),
583 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400584}
585
586func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400587 if err := m.repoIO.Pull(); err != nil {
588 return err
589 }
giocdfa3722024-06-13 20:10:14 +0400590 var portForward []PortForward
591 if err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
592 instanceDir := filepath.Join(m.appDirRoot, instanceId)
593 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
594 if err != nil {
595 return "", err
596 }
597 portForward = renderedCfg.PortForward
598 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400599 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400600 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400601 if err != nil {
602 return "", err
603 }
604 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400605 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400606 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400607 }); err != nil {
608 return err
609 }
610 if err := closePorts(portForward); err != nil {
giocdfa3722024-06-13 20:10:14 +0400611 return err
612 }
613 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400614}
615
giocb34ad22024-07-11 08:01:13 +0400616func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
617 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400618 {
giocdfa3722024-06-13 20:10:14 +0400619 Name: "Public",
620 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
621 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
622 Domain: env.Domain,
623 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
624 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
625 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400626 },
gio7841f4f2024-07-26 19:53:49 +0400627 }
628 if env.PrivateDomain != "" {
629 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400630 Name: "Private",
631 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
632 Domain: env.PrivateDomain,
633 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
634 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
635 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400636 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400637 }
giocb34ad22024-07-11 08:01:13 +0400638 n, err := m.FindAllAppInstances("network")
639 if err != nil {
640 return nil, err
641 }
642 for _, a := range n {
643 ret = append(ret, Network{
644 Name: a.Input["name"].(string),
645 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
646 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
647 Domain: a.Input["domain"].(string),
648 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
649 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
650 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
651 })
652 }
653 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400654}
gio3cdee592024-04-17 10:15:56 +0400655
gio0eaf2712024-04-14 13:08:46 +0400656type installOptions struct {
gio94904702024-07-26 16:58:34 +0400657 NoPull bool
giof8843412024-05-22 16:38:05 +0400658 NoPublish bool
659 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400660 Networks []Network
giof8843412024-05-22 16:38:05 +0400661 Branch string
662 LG LocalChartGenerator
663 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400664 Force bool
gio9d66f322024-07-06 13:45:10 +0400665 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400666}
667
668type InstallOption func(*installOptions)
669
670func WithConfig(env *EnvConfig) InstallOption {
671 return func(o *installOptions) {
672 o.Env = env
673 }
674}
675
giocb34ad22024-07-11 08:01:13 +0400676func WithNetworks(networks []Network) InstallOption {
677 return func(o *installOptions) {
678 o.Networks = networks
679 }
680}
681
gio23bdc1b2024-07-11 16:07:47 +0400682func WithNoNetworks() InstallOption {
683 return WithNetworks([]Network{})
684}
685
gio0eaf2712024-04-14 13:08:46 +0400686func WithBranch(branch string) InstallOption {
687 return func(o *installOptions) {
688 o.Branch = branch
689 }
690}
691
giof71a0832024-06-27 14:45:45 +0400692func WithForce() InstallOption {
693 return func(o *installOptions) {
694 o.Force = true
695 }
696}
697
giof8843412024-05-22 16:38:05 +0400698func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
699 return func(o *installOptions) {
700 o.LG = lg
701 }
702}
703
704func WithFetchContainerImages() InstallOption {
705 return func(o *installOptions) {
706 o.FetchContainerImages = true
707 }
708}
709
710func WithNoPublish() InstallOption {
711 return func(o *installOptions) {
712 o.NoPublish = true
713 }
714}
715
gio94904702024-07-26 16:58:34 +0400716func WithNoPull() InstallOption {
717 return func(o *installOptions) {
718 o.NoPull = true
719 }
720}
721
gio9d66f322024-07-06 13:45:10 +0400722func WithNoLock() InstallOption {
723 return func(o *installOptions) {
724 o.NoLock = true
725 }
726}
727
giof8843412024-05-22 16:38:05 +0400728// InfraAppmanager
729
730type InfraAppManager struct {
731 repoIO soft.RepoIO
732 nsc NamespaceCreator
733 hf HelmFetcher
734 lg LocalChartGenerator
735}
736
737func NewInfraAppManager(
738 repoIO soft.RepoIO,
739 nsc NamespaceCreator,
740 hf HelmFetcher,
741 lg LocalChartGenerator,
742) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400743 return &InfraAppManager{
744 repoIO,
giof8843412024-05-22 16:38:05 +0400745 nsc,
746 hf,
747 lg,
gio3cdee592024-04-17 10:15:56 +0400748 }, nil
749}
750
751func (m *InfraAppManager) Config() (InfraConfig, error) {
752 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400753 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400754 return InfraConfig{}, err
755 } else {
756 return cfg, nil
757 }
758}
759
gioe72b54f2024-04-22 10:44:41 +0400760func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
761 var cfg InfraAppInstanceConfig
762 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
763 return InfraAppInstanceConfig{}, err
764 } else {
765 return cfg, nil
766 }
767}
768
769func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
770 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
771 if err != nil {
772 return InfraAppInstanceConfig{}, err
773 }
774 for _, app := range kust.Resources {
775 if app == id {
776 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
777 if err != nil {
778 return InfraAppInstanceConfig{}, err
779 }
780 cfg.Id = id
781 return cfg, nil
782 }
783 }
784 return InfraAppInstanceConfig{}, nil
785}
786
gio778577f2024-04-29 09:44:38 +0400787func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400788 appDir = filepath.Clean(appDir)
789 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400790 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400791 }
giof8843412024-05-22 16:38:05 +0400792 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400793 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400794 }
795 infra, err := m.Config()
796 if err != nil {
gio778577f2024-04-29 09:44:38 +0400797 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400798 }
799 release := Release{
800 Namespace: namespace,
801 RepoAddr: m.repoIO.FullAddress(),
802 AppDir: appDir,
803 }
gio7841f4f2024-07-26 19:53:49 +0400804 networks := m.CreateNetworks(infra)
805 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +0400806 if err != nil {
807 return ReleaseResources{}, err
808 }
giof71a0832024-06-27 14:45:45 +0400809 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
810 if err != nil {
giof8843412024-05-22 16:38:05 +0400811 return ReleaseResources{}, err
812 }
giof71a0832024-06-27 14:45:45 +0400813 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +0400814 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400815 if err != nil {
gio778577f2024-04-29 09:44:38 +0400816 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400817 }
gio94904702024-07-26 16:58:34 +0400818 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
819 return ReleaseResources{}, err
820 }
821 return ReleaseResources{
822 Release: rendered.Config.Release,
823 RenderedRaw: rendered.Raw,
824 Helm: extractHelm(rendered.Resources),
825 }, nil
gioe72b54f2024-04-22 10:44:41 +0400826}
827
giof8843412024-05-22 16:38:05 +0400828// TODO(gio): take app configuration from the repo
829func (m *InfraAppManager) Update(
830 instanceId string,
831 values map[string]any,
832 opts ...InstallOption,
833) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400834 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400835 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400836 }
gio7841f4f2024-07-26 19:53:49 +0400837 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +0400838 if err != nil {
gio778577f2024-04-29 09:44:38 +0400839 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400840 }
841 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400842 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
843 if err != nil {
844 return ReleaseResources{}, err
845 }
846 app, err := NewCueInfraApp(appCfg)
847 if err != nil {
848 return ReleaseResources{}, err
849 }
gioe72b54f2024-04-22 10:44:41 +0400850 instanceConfigPath := filepath.Join(instanceDir, "config.json")
851 config, err := m.appConfig(instanceConfigPath)
852 if err != nil {
gio778577f2024-04-29 09:44:38 +0400853 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400854 }
giocdfa3722024-06-13 20:10:14 +0400855 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400856 if err != nil {
857 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400858 }
gio7841f4f2024-07-26 19:53:49 +0400859 networks := m.CreateNetworks(infra)
860 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400861 if err != nil {
gio778577f2024-04-29 09:44:38 +0400862 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400863 }
gio94904702024-07-26 16:58:34 +0400864 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
865 return ReleaseResources{}, err
866 }
867 return ReleaseResources{
868 Release: rendered.Config.Release,
869 RenderedRaw: rendered.Raw,
870 Helm: extractHelm(rendered.Resources),
871 }, nil
gio3cdee592024-04-17 10:15:56 +0400872}
giof8843412024-05-22 16:38:05 +0400873
gio7841f4f2024-07-26 19:53:49 +0400874func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
875 return []InfraNetwork{
876 {
877 Name: "Public",
878 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
879 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
880 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
881 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
882 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
883 },
884 }
885}
886
giof8843412024-05-22 16:38:05 +0400887func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
888 ret := make(map[string]string)
889 for name, chart := range charts.Git {
890 chartRoot := filepath.Join(root, name)
891 ret[name] = chartRoot
892 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
893 return nil, err
894 }
895 }
896 return ret, nil
897}
898
899func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
900 ret := make(map[string]helmv2.HelmChartTemplateSpec)
901 for name, path := range charts {
902 ret[name] = g.Generate(path)
903 }
904 return ret
905}
906
907func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
908 for _, img := range imgs {
909 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
910 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
911 "skopeo",
912 "--insecure-policy",
913 "copy",
914 "--dest-tls-verify=false", // TODO(gio): enable
915 "--multi-arch=all",
916 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
917 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
918 }); err != nil {
919 return err
920 }
921 }
922 return nil
923}
924
925type renderedInstance struct {
926 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400927 PortForward []PortForward `json:"portForward"`
giof8843412024-05-22 16:38:05 +0400928}
929
giocdfa3722024-06-13 20:10:14 +0400930func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400931 r, err := fs.Reader(path)
932 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400933 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400934 }
935 defer r.Close()
936 var cfg renderedInstance
937 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400938 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400939 }
giocdfa3722024-06-13 20:10:14 +0400940 return cfg, nil
giof8843412024-05-22 16:38:05 +0400941}
gioefa0ed42024-06-13 12:31:43 +0400942
943func findPortFields(scm Schema) []string {
944 switch scm.Kind() {
945 case KindBoolean:
946 return []string{}
947 case KindInt:
948 return []string{}
949 case KindString:
950 return []string{}
951 case KindStruct:
952 ret := []string{}
953 for _, f := range scm.Fields() {
954 for _, p := range findPortFields(f.Schema) {
955 if p == "" {
956 ret = append(ret, f.Name)
957 } else {
958 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
959 }
960 }
961 }
962 return ret
963 case KindNetwork:
964 return []string{}
gio4ece99c2024-07-18 11:05:50 +0400965 case KindMultiNetwork:
966 return []string{}
gioefa0ed42024-06-13 12:31:43 +0400967 case KindAuth:
968 return []string{}
969 case KindSSHKey:
970 return []string{}
971 case KindNumber:
972 return []string{}
973 case KindArrayString:
974 return []string{}
975 case KindPort:
976 return []string{""}
977 default:
978 panic("MUST NOT REACH!")
979 }
980}
981
982func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
983 for p, r := range ports {
984 if err := setPortField(values, p, r.Port); err != nil {
985 return err
986 }
987 }
988 return nil
989}
990
991func setPortField(values map[string]any, field string, port int) error {
992 f := strings.SplitN(field, ".", 2)
993 if len(f) == 2 {
994 var sub map[string]any
995 if s, ok := values[f[0]]; ok {
996 sub, ok = s.(map[string]any)
997 if !ok {
998 return fmt.Errorf("expected map")
999 }
1000 } else {
1001 sub = map[string]any{}
1002 values[f[0]] = sub
1003 }
1004 if err := setPortField(sub, f[1], port); err != nil {
1005 return err
1006 }
1007 } else {
1008 values[f[0]] = port
1009 }
1010 return nil
1011}