blob: c27b64163b2e98595b7d9572ab46c6bbb9e39816 [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 {
gio69731e82024-08-01 14:15:55 +040033 l sync.Locker
gioe72b54f2024-04-22 10:44:41 +040034 repoIO soft.RepoIO
giof8843412024-05-22 16:38:05 +040035 nsc NamespaceCreator
36 jc JobCreator
37 hf HelmFetcher
gio308105e2024-04-19 13:12:13 +040038 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040039}
40
giof8843412024-05-22 16:38:05 +040041func NewAppManager(
42 repoIO soft.RepoIO,
43 nsc NamespaceCreator,
44 jc JobCreator,
45 hf HelmFetcher,
46 appDirRoot string,
47) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040048 return &AppManager{
gio69731e82024-08-01 14:15:55 +040049 &sync.Mutex{},
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040050 repoIO,
giof8843412024-05-22 16:38:05 +040051 nsc,
52 jc,
53 hf,
gio308105e2024-04-19 13:12:13 +040054 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040055 }, nil
56}
57
gioe72b54f2024-04-22 10:44:41 +040058func (m *AppManager) Config() (EnvConfig, error) {
59 var cfg EnvConfig
60 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
61 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040062 } else {
63 return cfg, nil
64 }
65}
66
gio3cdee592024-04-17 10:15:56 +040067func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
68 var cfg AppInstanceConfig
gioe72b54f2024-04-22 10:44:41 +040069 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040070 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040071 } else {
72 return cfg, nil
73 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040074}
75
gio308105e2024-04-19 13:12:13 +040076func (m *AppManager) FindAllInstances() ([]AppInstanceConfig, error) {
gio09a3e5b2024-04-26 14:11:06 +040077 m.repoIO.Pull()
gioe72b54f2024-04-22 10:44:41 +040078 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040079 if err != nil {
80 return nil, err
81 }
gio3cdee592024-04-17 10:15:56 +040082 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040083 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040084 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
85 if err != nil {
86 return nil, err
87 }
88 cfg.Id = app
89 ret = append(ret, cfg)
90 }
91 return ret, nil
92}
93
94func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040095 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio308105e2024-04-19 13:12:13 +040096 if err != nil {
giocb34ad22024-07-11 08:01:13 +040097 if errors.Is(err, fs.ErrNotExist) {
98 return nil, nil
99 } else {
100 return nil, err
101 }
gio308105e2024-04-19 13:12:13 +0400102 }
103 ret := make([]AppInstanceConfig, 0)
104 for _, app := range kust.Resources {
105 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400106 if err != nil {
107 return nil, err
108 }
109 cfg.Id = app
110 if cfg.AppId == name {
111 ret = append(ret, cfg)
112 }
113 }
114 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400115}
116
gio778577f2024-04-29 09:44:38 +0400117func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +0400118 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +0400119 if err != nil {
gio778577f2024-04-29 09:44:38 +0400120 return nil, err
gio3af43942024-04-16 08:13:50 +0400121 }
122 for _, app := range kust.Resources {
123 if app == id {
gio308105e2024-04-19 13:12:13 +0400124 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400125 if err != nil {
gio778577f2024-04-29 09:44:38 +0400126 return nil, err
gio3af43942024-04-16 08:13:50 +0400127 }
128 cfg.Id = id
gio778577f2024-04-29 09:44:38 +0400129 return &cfg, nil
gio3af43942024-04-16 08:13:50 +0400130 }
131 }
gio778577f2024-04-29 09:44:38 +0400132 return nil, ErrorNotFound
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400133}
134
giof8843412024-05-22 16:38:05 +0400135func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
136 files, err := fs.ListDir(dir)
137 if err != nil {
138 return nil, err
139 }
140 cfg := CueAppData{}
141 for _, f := range files {
142 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
143 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
144 if err != nil {
145 return nil, err
146 }
147 cfg[f.Name()] = contents
148 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400149 }
gio308105e2024-04-19 13:12:13 +0400150 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400151}
152
giof8843412024-05-22 16:38:05 +0400153func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
154 cfg, err := GetCueAppData(m.repoIO, filepath.Join(m.appDirRoot, id))
155 if err != nil {
156 return nil, err
157 }
158 return NewCueEnvApp(cfg)
159}
160
gio3af43942024-04-16 08:13:50 +0400161type allocatePortReq struct {
162 Protocol string `json:"protocol"`
163 SourcePort int `json:"sourcePort"`
164 TargetService string `json:"targetService"`
165 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400166 Secret string `json:"secret,omitempty"`
167}
168
169type removePortReq struct {
170 Protocol string `json:"protocol"`
171 SourcePort int `json:"sourcePort"`
172 TargetService string `json:"targetService"`
173 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400174}
175
gioefa0ed42024-06-13 12:31:43 +0400176type reservePortResp struct {
177 Port int `json:"port"`
178 Secret string `json:"secret"`
179}
180
181func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
182 ret := map[string]reservePortResp{}
183 for p, reserveAddr := range ports {
184 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
185 if err != nil {
186 return nil, err
187 }
188 if resp.StatusCode != http.StatusOK {
189 var e bytes.Buffer
190 io.Copy(&e, resp.Body)
191 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
192 }
193 var r reservePortResp
194 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
195 return nil, err
196 }
197 ret[p] = r
198 }
199 return ret, nil
200}
201
202func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400203 for _, p := range ports {
204 var buf bytes.Buffer
205 req := allocatePortReq{
206 Protocol: p.Protocol,
207 SourcePort: p.SourcePort,
208 TargetService: p.TargetService,
209 TargetPort: p.TargetPort,
210 }
gioefa0ed42024-06-13 12:31:43 +0400211 allocator := ""
212 for n, r := range reservations {
213 if p.SourcePort == r.Port {
214 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400215 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400216 break
217 }
218 }
219 if allocator == "" {
220 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
221 }
giobd7ab0b2024-06-17 12:55:17 +0400222 if err := json.NewEncoder(&buf).Encode(req); err != nil {
223 return err
224 }
gioefa0ed42024-06-13 12:31:43 +0400225 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400226 if err != nil {
227 return err
228 }
229 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400230 var r bytes.Buffer
231 io.Copy(&r, resp.Body)
232 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 +0400233 }
234 }
235 return nil
236}
237
giocdfa3722024-06-13 20:10:14 +0400238func closePorts(ports []PortForward) error {
239 var retErr error
240 for _, p := range ports {
241 var buf bytes.Buffer
242 req := removePortReq{
243 Protocol: p.Protocol,
244 SourcePort: p.SourcePort,
245 TargetService: p.TargetService,
246 TargetPort: p.TargetPort,
247 }
248 if err := json.NewEncoder(&buf).Encode(req); err != nil {
249 retErr = err
250 continue
251 }
252 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
253 if err != nil {
254 retErr = err
255 continue
256 }
257 if resp.StatusCode != http.StatusOK {
258 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
259 continue
260 }
261 }
262 return retErr
263}
264
gioe72b54f2024-04-22 10:44:41 +0400265func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400266 for p := filepath.Clean(path); p != "/"; {
267 parent, child := filepath.Split(p)
268 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400269 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400270 if err != nil {
271 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400272 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400273 kust = &k
274 } else {
275 return err
276 }
277 }
278 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400279 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400280 return err
281 }
282 p = filepath.Clean(parent)
283 }
284 return nil
285}
286
gio778577f2024-04-29 09:44:38 +0400287type Resource struct {
giob4a3a192024-08-19 09:55:47 +0400288 Name string `json:"name"`
289 Namespace string `json:"namespace"`
290 Info string `json:"info"`
291 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400292}
293
294type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400295 Release Release
296 Helm []Resource
297 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400298}
299
gio3cdee592024-04-17 10:15:56 +0400300// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400301func installApp(
gioe72b54f2024-04-22 10:44:41 +0400302 repo soft.RepoIO,
303 appDir string,
304 name string,
305 config any,
gioe72b54f2024-04-22 10:44:41 +0400306 resources CueAppData,
307 data CueAppData,
giof8843412024-05-22 16:38:05 +0400308 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400309) error {
giof8843412024-05-22 16:38:05 +0400310 var o installOptions
311 for _, i := range opts {
312 i(&o)
313 }
314 dopts := []soft.DoOption{}
315 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400316 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
317 }
gio94904702024-07-26 16:58:34 +0400318 if o.NoPull {
319 dopts = append(dopts, soft.WithNoPull())
320 }
giof8843412024-05-22 16:38:05 +0400321 if o.NoPublish {
322 dopts = append(dopts, soft.WithNoCommit())
323 }
giof71a0832024-06-27 14:45:45 +0400324 if o.Force {
325 dopts = append(dopts, soft.WithForce())
326 }
gio9d66f322024-07-06 13:45:10 +0400327 if o.NoLock {
328 dopts = append(dopts, soft.WithNoLock())
329 }
giob4a3a192024-08-19 09:55:47 +0400330 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400331 if err := r.RemoveDir(appDir); err != nil {
332 return "", err
333 }
334 resourcesDir := path.Join(appDir, "resources")
335 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400336 return "", err
337 }
gio94904702024-07-26 16:58:34 +0400338 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400339 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400340 return err
gio5e49bb62024-07-20 10:43:19 +0400341 }
gioe72b54f2024-04-22 10:44:41 +0400342 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400343 return err
gio3af43942024-04-16 08:13:50 +0400344 }
gioe72b54f2024-04-22 10:44:41 +0400345 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400346 return err
gio308105e2024-04-19 13:12:13 +0400347 }
gioe72b54f2024-04-22 10:44:41 +0400348 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400349 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
gio94904702024-07-26 16:58:34 +0400350 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400351 }
352 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400353 if err != nil {
gio94904702024-07-26 16:58:34 +0400354 return err
gio3af43942024-04-16 08:13:50 +0400355 }
gio308105e2024-04-19 13:12:13 +0400356 defer w.Close()
357 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400358 return err
gio3af43942024-04-16 08:13:50 +0400359 }
360 }
gio94904702024-07-26 16:58:34 +0400361 return nil
362 }(); err != nil {
363 return "", err
gio308105e2024-04-19 13:12:13 +0400364 }
gio94904702024-07-26 16:58:34 +0400365 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400366 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400367 return err
gio308105e2024-04-19 13:12:13 +0400368 }
gioefa0ed42024-06-13 12:31:43 +0400369 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400370 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400371 appKust.AddResources(name)
372 w, err := r.Writer(path.Join(resourcesDir, name))
373 if err != nil {
gio94904702024-07-26 16:58:34 +0400374 return err
gio308105e2024-04-19 13:12:13 +0400375 }
376 defer w.Close()
377 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400378 return err
gio308105e2024-04-19 13:12:13 +0400379 }
380 }
gioe72b54f2024-04-22 10:44:41 +0400381 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400382 return err
gio3af43942024-04-16 08:13:50 +0400383 }
gio94904702024-07-26 16:58:34 +0400384 return nil
385 }(); err != nil {
386 return "", err
gio3af43942024-04-16 08:13:50 +0400387 }
gioe72b54f2024-04-22 10:44:41 +0400388 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400389 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400390 return err
gio3af43942024-04-16 08:13:50 +0400391}
392
gio3cdee592024-04-17 10:15:56 +0400393// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400394func (m *AppManager) Install(
395 app EnvApp,
396 instanceId string,
397 appDir string,
398 namespace string,
399 values map[string]any,
400 opts ...InstallOption,
401) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400402 o := &installOptions{}
403 for _, i := range opts {
404 i(o)
405 }
406 if !o.NoLock {
407 m.l.Lock()
408 defer m.l.Unlock()
409 }
gioefa0ed42024-06-13 12:31:43 +0400410 portFields := findPortFields(app.Schema())
411 fakeReservations := map[string]reservePortResp{}
412 for i, f := range portFields {
413 fakeReservations[f] = reservePortResp{Port: i}
414 }
415 if err := setPortFields(values, fakeReservations); err != nil {
416 return ReleaseResources{}, err
417 }
gio3af43942024-04-16 08:13:50 +0400418 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400419 if !o.NoPull {
420 if err := m.repoIO.Pull(); err != nil {
421 return ReleaseResources{}, err
422 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400423 }
gio94904702024-07-26 16:58:34 +0400424 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400425 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400426 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400427 }
gio0eaf2712024-04-14 13:08:46 +0400428 var env EnvConfig
429 if o.Env != nil {
430 env = *o.Env
431 } else {
432 var err error
433 env, err = m.Config()
434 if err != nil {
435 return ReleaseResources{}, err
436 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400437 }
giocb34ad22024-07-11 08:01:13 +0400438 var networks []Network
439 if o.Networks != nil {
440 networks = o.Networks
441 } else {
442 var err error
443 networks, err = m.CreateNetworks(env)
444 if err != nil {
445 return ReleaseResources{}, err
446 }
447 }
giof8843412024-05-22 16:38:05 +0400448 var lg LocalChartGenerator
449 if o.LG != nil {
450 lg = o.LG
451 } else {
452 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
453 }
gio3cdee592024-04-17 10:15:56 +0400454 release := Release{
455 AppInstanceId: instanceId,
456 Namespace: namespace,
457 RepoAddr: m.repoIO.FullAddress(),
458 AppDir: appDir,
459 }
giocb34ad22024-07-11 08:01:13 +0400460 rendered, err := app.Render(release, env, networks, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400461 if err != nil {
gio778577f2024-04-29 09:44:38 +0400462 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400463 }
gioefa0ed42024-06-13 12:31:43 +0400464 reservators := map[string]string{}
465 allocators := map[string]string{}
466 for _, pf := range rendered.Ports {
467 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
468 allocators[portFields[pf.SourcePort]] = pf.Allocator
469 }
470 portReservations, err := reservePorts(reservators)
471 if err != nil {
472 return ReleaseResources{}, err
473 }
474 if err := setPortFields(values, portReservations); err != nil {
475 return ReleaseResources{}, err
476 }
gio7841f4f2024-07-26 19:53:49 +0400477 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400478 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
479 if o.FetchContainerImages {
480 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
481 return ReleaseResources{}, err
482 }
gio0eaf2712024-04-14 13:08:46 +0400483 }
giof71a0832024-06-27 14:45:45 +0400484 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
485 if err != nil {
giof8843412024-05-22 16:38:05 +0400486 return ReleaseResources{}, err
487 }
giof71a0832024-06-27 14:45:45 +0400488 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400489 if o.FetchContainerImages {
490 release.ImageRegistry = imageRegistry
491 }
giocb34ad22024-07-11 08:01:13 +0400492 rendered, err = app.Render(release, env, networks, values, localCharts)
giof8843412024-05-22 16:38:05 +0400493 if err != nil {
494 return ReleaseResources{}, err
495 }
gio94904702024-07-26 16:58:34 +0400496 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400497 return ReleaseResources{}, err
498 }
gioff2a29a2024-05-01 17:06:42 +0400499 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400500 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400501 return ReleaseResources{}, err
502 }
gio778577f2024-04-29 09:44:38 +0400503 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400504 Release: rendered.Config.Release,
505 RenderedRaw: rendered.Raw,
506 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400507 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400508}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400509
gio778577f2024-04-29 09:44:38 +0400510type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400511 Metadata struct {
512 Name string `json:"name"`
513 Namespace string `json:"namespace"`
514 Annotations map[string]string `json:"annotations"`
515 } `json:"metadata"`
516 Kind string `json:"kind"`
517 Status struct {
gio778577f2024-04-29 09:44:38 +0400518 Conditions []struct {
519 Type string `json:"type"`
520 Status string `json:"status"`
521 } `json:"conditions"`
522 } `json:"status,omitempty"`
523}
524
525func extractHelm(resources CueAppData) []Resource {
526 ret := make([]Resource, 0, len(resources))
527 for _, contents := range resources {
528 var h helmRelease
529 if err := yaml.Unmarshal(contents, &h); err != nil {
530 panic(err) // TODO(gio): handle
531 }
gio0eaf2712024-04-14 13:08:46 +0400532 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400533 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400534 Name: h.Metadata.Name,
535 Namespace: h.Metadata.Namespace,
536 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
537 Annotations: nil,
giof9f0bee2024-06-11 20:10:05 +0400538 }
539 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400540 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400541 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
542 if ok && len(info) != 0 {
543 res.Info = info
544 }
545 }
546 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400547 }
gio778577f2024-04-29 09:44:38 +0400548 }
549 return ret
550}
551
giof8843412024-05-22 16:38:05 +0400552// TODO(gio): take app configuration from the repo
553func (m *AppManager) Update(
554 instanceId string,
555 values map[string]any,
556 opts ...InstallOption,
557) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400558 m.l.Lock()
559 defer m.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400560 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400561 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400562 }
gio3cdee592024-04-17 10:15:56 +0400563 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400564 if err != nil {
gio778577f2024-04-29 09:44:38 +0400565 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400566 }
gio308105e2024-04-19 13:12:13 +0400567 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400568 app, err := m.GetInstanceApp(instanceId)
569 if err != nil {
570 return ReleaseResources{}, err
571 }
gio308105e2024-04-19 13:12:13 +0400572 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400573 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400574 if err != nil {
gio778577f2024-04-29 09:44:38 +0400575 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400576 }
giocdfa3722024-06-13 20:10:14 +0400577 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400578 if err != nil {
579 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400580 }
giocb34ad22024-07-11 08:01:13 +0400581 networks, err := m.CreateNetworks(env)
582 if err != nil {
583 return ReleaseResources{}, err
584 }
585 rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400586 if err != nil {
gio778577f2024-04-29 09:44:38 +0400587 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400588 }
gio94904702024-07-26 16:58:34 +0400589 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
590 return ReleaseResources{}, err
591 }
592 return ReleaseResources{
593 Release: rendered.Config.Release,
594 RenderedRaw: rendered.Raw,
595 Helm: extractHelm(rendered.Resources),
596 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400597}
598
599func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400600 m.l.Lock()
601 defer m.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400602 if err := m.repoIO.Pull(); err != nil {
603 return err
604 }
giocdfa3722024-06-13 20:10:14 +0400605 var portForward []PortForward
giob4a3a192024-08-19 09:55:47 +0400606 if _, err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400607 instanceDir := filepath.Join(m.appDirRoot, instanceId)
608 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
609 if err != nil {
610 return "", err
611 }
612 portForward = renderedCfg.PortForward
613 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400614 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400615 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400616 if err != nil {
617 return "", err
618 }
619 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400620 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400621 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400622 }); err != nil {
623 return err
624 }
625 if err := closePorts(portForward); err != nil {
giocdfa3722024-06-13 20:10:14 +0400626 return err
627 }
628 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400629}
630
giocb34ad22024-07-11 08:01:13 +0400631func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
632 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400633 {
giocdfa3722024-06-13 20:10:14 +0400634 Name: "Public",
635 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
636 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
637 Domain: env.Domain,
638 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
639 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
640 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400641 },
gio7841f4f2024-07-26 19:53:49 +0400642 }
643 if env.PrivateDomain != "" {
644 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400645 Name: "Private",
646 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
647 Domain: env.PrivateDomain,
648 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
649 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
650 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400651 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400652 }
giocb34ad22024-07-11 08:01:13 +0400653 n, err := m.FindAllAppInstances("network")
654 if err != nil {
655 return nil, err
656 }
657 for _, a := range n {
658 ret = append(ret, Network{
659 Name: a.Input["name"].(string),
660 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
661 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
662 Domain: a.Input["domain"].(string),
663 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
664 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
665 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
666 })
667 }
668 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400669}
gio3cdee592024-04-17 10:15:56 +0400670
gio0eaf2712024-04-14 13:08:46 +0400671type installOptions struct {
gio94904702024-07-26 16:58:34 +0400672 NoPull bool
giof8843412024-05-22 16:38:05 +0400673 NoPublish bool
674 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400675 Networks []Network
giof8843412024-05-22 16:38:05 +0400676 Branch string
677 LG LocalChartGenerator
678 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400679 Force bool
gio9d66f322024-07-06 13:45:10 +0400680 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400681}
682
683type InstallOption func(*installOptions)
684
685func WithConfig(env *EnvConfig) InstallOption {
686 return func(o *installOptions) {
687 o.Env = env
688 }
689}
690
giocb34ad22024-07-11 08:01:13 +0400691func WithNetworks(networks []Network) InstallOption {
692 return func(o *installOptions) {
693 o.Networks = networks
694 }
695}
696
gio23bdc1b2024-07-11 16:07:47 +0400697func WithNoNetworks() InstallOption {
698 return WithNetworks([]Network{})
699}
700
gio0eaf2712024-04-14 13:08:46 +0400701func WithBranch(branch string) InstallOption {
702 return func(o *installOptions) {
703 o.Branch = branch
704 }
705}
706
giof71a0832024-06-27 14:45:45 +0400707func WithForce() InstallOption {
708 return func(o *installOptions) {
709 o.Force = true
710 }
711}
712
giof8843412024-05-22 16:38:05 +0400713func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
714 return func(o *installOptions) {
715 o.LG = lg
716 }
717}
718
719func WithFetchContainerImages() InstallOption {
720 return func(o *installOptions) {
721 o.FetchContainerImages = true
722 }
723}
724
725func WithNoPublish() InstallOption {
726 return func(o *installOptions) {
727 o.NoPublish = true
728 }
729}
730
gio94904702024-07-26 16:58:34 +0400731func WithNoPull() InstallOption {
732 return func(o *installOptions) {
733 o.NoPull = true
734 }
735}
736
gio9d66f322024-07-06 13:45:10 +0400737func WithNoLock() InstallOption {
738 return func(o *installOptions) {
739 o.NoLock = true
740 }
741}
742
giof8843412024-05-22 16:38:05 +0400743// InfraAppmanager
744
745type InfraAppManager struct {
746 repoIO soft.RepoIO
747 nsc NamespaceCreator
748 hf HelmFetcher
749 lg LocalChartGenerator
750}
751
752func NewInfraAppManager(
753 repoIO soft.RepoIO,
754 nsc NamespaceCreator,
755 hf HelmFetcher,
756 lg LocalChartGenerator,
757) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400758 return &InfraAppManager{
759 repoIO,
giof8843412024-05-22 16:38:05 +0400760 nsc,
761 hf,
762 lg,
gio3cdee592024-04-17 10:15:56 +0400763 }, nil
764}
765
766func (m *InfraAppManager) Config() (InfraConfig, error) {
767 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400768 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400769 return InfraConfig{}, err
770 } else {
771 return cfg, nil
772 }
773}
774
gioe72b54f2024-04-22 10:44:41 +0400775func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
776 var cfg InfraAppInstanceConfig
777 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
778 return InfraAppInstanceConfig{}, err
779 } else {
780 return cfg, nil
781 }
782}
783
784func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
785 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
786 if err != nil {
787 return InfraAppInstanceConfig{}, err
788 }
789 for _, app := range kust.Resources {
790 if app == id {
791 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
792 if err != nil {
793 return InfraAppInstanceConfig{}, err
794 }
795 cfg.Id = id
796 return cfg, nil
797 }
798 }
799 return InfraAppInstanceConfig{}, nil
800}
801
gio778577f2024-04-29 09:44:38 +0400802func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400803 appDir = filepath.Clean(appDir)
804 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400805 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400806 }
giof8843412024-05-22 16:38:05 +0400807 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400808 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400809 }
810 infra, err := m.Config()
811 if err != nil {
gio778577f2024-04-29 09:44:38 +0400812 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400813 }
814 release := Release{
815 Namespace: namespace,
816 RepoAddr: m.repoIO.FullAddress(),
817 AppDir: appDir,
818 }
gio7841f4f2024-07-26 19:53:49 +0400819 networks := m.CreateNetworks(infra)
820 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +0400821 if err != nil {
822 return ReleaseResources{}, err
823 }
giof71a0832024-06-27 14:45:45 +0400824 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
825 if err != nil {
giof8843412024-05-22 16:38:05 +0400826 return ReleaseResources{}, err
827 }
giof71a0832024-06-27 14:45:45 +0400828 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +0400829 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400830 if err != nil {
gio778577f2024-04-29 09:44:38 +0400831 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400832 }
gio94904702024-07-26 16:58:34 +0400833 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
834 return ReleaseResources{}, err
835 }
836 return ReleaseResources{
837 Release: rendered.Config.Release,
838 RenderedRaw: rendered.Raw,
839 Helm: extractHelm(rendered.Resources),
840 }, nil
gioe72b54f2024-04-22 10:44:41 +0400841}
842
giof8843412024-05-22 16:38:05 +0400843// TODO(gio): take app configuration from the repo
844func (m *InfraAppManager) Update(
845 instanceId string,
846 values map[string]any,
847 opts ...InstallOption,
848) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400849 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400850 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400851 }
gio7841f4f2024-07-26 19:53:49 +0400852 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +0400853 if err != nil {
gio778577f2024-04-29 09:44:38 +0400854 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400855 }
856 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400857 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
858 if err != nil {
859 return ReleaseResources{}, err
860 }
861 app, err := NewCueInfraApp(appCfg)
862 if err != nil {
863 return ReleaseResources{}, err
864 }
gioe72b54f2024-04-22 10:44:41 +0400865 instanceConfigPath := filepath.Join(instanceDir, "config.json")
866 config, err := m.appConfig(instanceConfigPath)
867 if err != nil {
gio778577f2024-04-29 09:44:38 +0400868 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400869 }
giocdfa3722024-06-13 20:10:14 +0400870 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400871 if err != nil {
872 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400873 }
gio7841f4f2024-07-26 19:53:49 +0400874 networks := m.CreateNetworks(infra)
875 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400876 if err != nil {
gio778577f2024-04-29 09:44:38 +0400877 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400878 }
gio94904702024-07-26 16:58:34 +0400879 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
880 return ReleaseResources{}, err
881 }
882 return ReleaseResources{
883 Release: rendered.Config.Release,
884 RenderedRaw: rendered.Raw,
885 Helm: extractHelm(rendered.Resources),
886 }, nil
gio3cdee592024-04-17 10:15:56 +0400887}
giof8843412024-05-22 16:38:05 +0400888
gio7841f4f2024-07-26 19:53:49 +0400889func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
890 return []InfraNetwork{
891 {
892 Name: "Public",
893 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
894 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
895 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
896 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
897 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
898 },
899 }
900}
901
giof8843412024-05-22 16:38:05 +0400902func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
903 ret := make(map[string]string)
904 for name, chart := range charts.Git {
905 chartRoot := filepath.Join(root, name)
906 ret[name] = chartRoot
907 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
908 return nil, err
909 }
910 }
911 return ret, nil
912}
913
914func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
915 ret := make(map[string]helmv2.HelmChartTemplateSpec)
916 for name, path := range charts {
917 ret[name] = g.Generate(path)
918 }
919 return ret
920}
921
922func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
923 for _, img := range imgs {
924 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
925 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
926 "skopeo",
927 "--insecure-policy",
928 "copy",
929 "--dest-tls-verify=false", // TODO(gio): enable
930 "--multi-arch=all",
931 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
932 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
933 }); err != nil {
934 return err
935 }
936 }
937 return nil
938}
939
940type renderedInstance struct {
941 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400942 PortForward []PortForward `json:"portForward"`
giof8843412024-05-22 16:38:05 +0400943}
944
giocdfa3722024-06-13 20:10:14 +0400945func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400946 r, err := fs.Reader(path)
947 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400948 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400949 }
950 defer r.Close()
951 var cfg renderedInstance
952 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400953 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400954 }
giocdfa3722024-06-13 20:10:14 +0400955 return cfg, nil
giof8843412024-05-22 16:38:05 +0400956}
gioefa0ed42024-06-13 12:31:43 +0400957
958func findPortFields(scm Schema) []string {
959 switch scm.Kind() {
960 case KindBoolean:
961 return []string{}
962 case KindInt:
963 return []string{}
964 case KindString:
965 return []string{}
966 case KindStruct:
967 ret := []string{}
968 for _, f := range scm.Fields() {
969 for _, p := range findPortFields(f.Schema) {
970 if p == "" {
971 ret = append(ret, f.Name)
972 } else {
973 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
974 }
975 }
976 }
977 return ret
978 case KindNetwork:
979 return []string{}
gio4ece99c2024-07-18 11:05:50 +0400980 case KindMultiNetwork:
981 return []string{}
gioefa0ed42024-06-13 12:31:43 +0400982 case KindAuth:
983 return []string{}
984 case KindSSHKey:
985 return []string{}
986 case KindNumber:
987 return []string{}
988 case KindArrayString:
989 return []string{}
990 case KindPort:
991 return []string{""}
992 default:
993 panic("MUST NOT REACH!")
994 }
995}
996
997func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
998 for p, r := range ports {
999 if err := setPortField(values, p, r.Port); err != nil {
1000 return err
1001 }
1002 }
1003 return nil
1004}
1005
1006func setPortField(values map[string]any, field string, port int) error {
1007 f := strings.SplitN(field, ".", 2)
1008 if len(f) == 2 {
1009 var sub map[string]any
1010 if s, ok := values[f[0]]; ok {
1011 sub, ok = s.(map[string]any)
1012 if !ok {
1013 return fmt.Errorf("expected map")
1014 }
1015 } else {
1016 sub = map[string]any{}
1017 values[f[0]] = sub
1018 }
1019 if err := setPortField(sub, f[1], port); err != nil {
1020 return err
1021 }
1022 } else {
1023 values[f[0]] = port
1024 }
1025 return nil
1026}