blob: 3157a45da2636242c6135fc31f984dc22f52ae6b [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
gio36b23b32024-08-25 12:20:54 +040038 vpnKeyGen VPNAuthKeyGenerator
gio308105e2024-04-19 13:12:13 +040039 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,
gio36b23b32024-08-25 12:20:54 +040047 vpnKeyGen VPNAuthKeyGenerator,
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
gio308105e2024-04-19 13:12:13 +040079func (m *AppManager) FindAllInstances() ([]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
97func (m *AppManager) FindAllAppInstances(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
gio778577f2024-04-29 09:44:38 +0400120func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +0400121 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
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 for _, app := range kust.Resources {
126 if app == id {
gio308105e2024-04-19 13:12:13 +0400127 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400128 if err != nil {
gio778577f2024-04-29 09:44:38 +0400129 return nil, err
gio3af43942024-04-16 08:13:50 +0400130 }
131 cfg.Id = id
gio778577f2024-04-29 09:44:38 +0400132 return &cfg, nil
gio3af43942024-04-16 08:13:50 +0400133 }
134 }
gio778577f2024-04-29 09:44:38 +0400135 return nil, ErrorNotFound
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400136}
137
giof8843412024-05-22 16:38:05 +0400138func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
139 files, err := fs.ListDir(dir)
140 if err != nil {
141 return nil, err
142 }
143 cfg := CueAppData{}
144 for _, f := range files {
145 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
146 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
147 if err != nil {
148 return nil, err
149 }
150 cfg[f.Name()] = contents
151 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400152 }
gio308105e2024-04-19 13:12:13 +0400153 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400154}
155
giof8843412024-05-22 16:38:05 +0400156func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
157 cfg, err := GetCueAppData(m.repoIO, filepath.Join(m.appDirRoot, id))
158 if err != nil {
159 return nil, err
160 }
161 return NewCueEnvApp(cfg)
162}
163
gio3af43942024-04-16 08:13:50 +0400164type allocatePortReq struct {
165 Protocol string `json:"protocol"`
166 SourcePort int `json:"sourcePort"`
167 TargetService string `json:"targetService"`
168 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400169 Secret string `json:"secret,omitempty"`
170}
171
172type removePortReq struct {
173 Protocol string `json:"protocol"`
174 SourcePort int `json:"sourcePort"`
175 TargetService string `json:"targetService"`
176 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400177}
178
gioefa0ed42024-06-13 12:31:43 +0400179type reservePortResp struct {
180 Port int `json:"port"`
181 Secret string `json:"secret"`
182}
183
184func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
185 ret := map[string]reservePortResp{}
186 for p, reserveAddr := range ports {
187 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
188 if err != nil {
189 return nil, err
190 }
191 if resp.StatusCode != http.StatusOK {
192 var e bytes.Buffer
193 io.Copy(&e, resp.Body)
194 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
195 }
196 var r reservePortResp
197 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
198 return nil, err
199 }
200 ret[p] = r
201 }
202 return ret, nil
203}
204
205func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400206 for _, p := range ports {
207 var buf bytes.Buffer
208 req := allocatePortReq{
209 Protocol: p.Protocol,
210 SourcePort: p.SourcePort,
211 TargetService: p.TargetService,
212 TargetPort: p.TargetPort,
213 }
gioefa0ed42024-06-13 12:31:43 +0400214 allocator := ""
215 for n, r := range reservations {
216 if p.SourcePort == r.Port {
217 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400218 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400219 break
220 }
221 }
222 if allocator == "" {
223 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
224 }
giobd7ab0b2024-06-17 12:55:17 +0400225 if err := json.NewEncoder(&buf).Encode(req); err != nil {
226 return err
227 }
gioefa0ed42024-06-13 12:31:43 +0400228 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400229 if err != nil {
230 return err
231 }
232 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400233 var r bytes.Buffer
234 io.Copy(&r, resp.Body)
235 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 +0400236 }
237 }
238 return nil
239}
240
giocdfa3722024-06-13 20:10:14 +0400241func closePorts(ports []PortForward) error {
242 var retErr error
243 for _, p := range ports {
244 var buf bytes.Buffer
245 req := removePortReq{
246 Protocol: p.Protocol,
247 SourcePort: p.SourcePort,
248 TargetService: p.TargetService,
249 TargetPort: p.TargetPort,
250 }
251 if err := json.NewEncoder(&buf).Encode(req); err != nil {
252 retErr = err
253 continue
254 }
255 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
256 if err != nil {
257 retErr = err
258 continue
259 }
260 if resp.StatusCode != http.StatusOK {
261 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
262 continue
263 }
264 }
265 return retErr
266}
267
gioe72b54f2024-04-22 10:44:41 +0400268func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400269 for p := filepath.Clean(path); p != "/"; {
270 parent, child := filepath.Split(p)
271 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400272 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400273 if err != nil {
274 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400275 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400276 kust = &k
277 } else {
278 return err
279 }
280 }
281 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400282 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400283 return err
284 }
285 p = filepath.Clean(parent)
286 }
287 return nil
288}
289
gio778577f2024-04-29 09:44:38 +0400290type Resource struct {
giob4a3a192024-08-19 09:55:47 +0400291 Name string `json:"name"`
292 Namespace string `json:"namespace"`
293 Info string `json:"info"`
294 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400295}
296
297type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400298 Release Release
299 Helm []Resource
300 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400301}
302
gio3cdee592024-04-17 10:15:56 +0400303// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400304func installApp(
gioe72b54f2024-04-22 10:44:41 +0400305 repo soft.RepoIO,
306 appDir string,
307 name string,
308 config any,
gioe72b54f2024-04-22 10:44:41 +0400309 resources CueAppData,
310 data CueAppData,
giof8843412024-05-22 16:38:05 +0400311 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400312) error {
giof8843412024-05-22 16:38:05 +0400313 var o installOptions
314 for _, i := range opts {
315 i(&o)
316 }
317 dopts := []soft.DoOption{}
318 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400319 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
320 }
gio94904702024-07-26 16:58:34 +0400321 if o.NoPull {
322 dopts = append(dopts, soft.WithNoPull())
323 }
giof8843412024-05-22 16:38:05 +0400324 if o.NoPublish {
325 dopts = append(dopts, soft.WithNoCommit())
326 }
giof71a0832024-06-27 14:45:45 +0400327 if o.Force {
328 dopts = append(dopts, soft.WithForce())
329 }
gio9d66f322024-07-06 13:45:10 +0400330 if o.NoLock {
331 dopts = append(dopts, soft.WithNoLock())
332 }
giob4a3a192024-08-19 09:55:47 +0400333 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400334 if err := r.RemoveDir(appDir); err != nil {
335 return "", err
336 }
337 resourcesDir := path.Join(appDir, "resources")
338 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400339 return "", err
340 }
gio94904702024-07-26 16:58:34 +0400341 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400342 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400343 return err
gio5e49bb62024-07-20 10:43:19 +0400344 }
gioe72b54f2024-04-22 10:44:41 +0400345 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400346 return err
gio3af43942024-04-16 08:13:50 +0400347 }
gioe72b54f2024-04-22 10:44:41 +0400348 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400349 return err
gio308105e2024-04-19 13:12:13 +0400350 }
gioe72b54f2024-04-22 10:44:41 +0400351 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400352 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
gio94904702024-07-26 16:58:34 +0400353 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400354 }
355 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400356 if err != nil {
gio94904702024-07-26 16:58:34 +0400357 return err
gio3af43942024-04-16 08:13:50 +0400358 }
gio308105e2024-04-19 13:12:13 +0400359 defer w.Close()
360 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400361 return err
gio3af43942024-04-16 08:13:50 +0400362 }
363 }
gio94904702024-07-26 16:58:34 +0400364 return nil
365 }(); err != nil {
366 return "", err
gio308105e2024-04-19 13:12:13 +0400367 }
gio94904702024-07-26 16:58:34 +0400368 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400369 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400370 return err
gio308105e2024-04-19 13:12:13 +0400371 }
gioefa0ed42024-06-13 12:31:43 +0400372 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400373 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400374 appKust.AddResources(name)
375 w, err := r.Writer(path.Join(resourcesDir, name))
376 if err != nil {
gio94904702024-07-26 16:58:34 +0400377 return err
gio308105e2024-04-19 13:12:13 +0400378 }
379 defer w.Close()
380 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400381 return err
gio308105e2024-04-19 13:12:13 +0400382 }
383 }
gioe72b54f2024-04-22 10:44:41 +0400384 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400385 return err
gio3af43942024-04-16 08:13:50 +0400386 }
gio94904702024-07-26 16:58:34 +0400387 return nil
388 }(); err != nil {
389 return "", err
gio3af43942024-04-16 08:13:50 +0400390 }
gioe72b54f2024-04-22 10:44:41 +0400391 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400392 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400393 return err
gio3af43942024-04-16 08:13:50 +0400394}
395
gio3cdee592024-04-17 10:15:56 +0400396// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400397func (m *AppManager) Install(
398 app EnvApp,
399 instanceId string,
400 appDir string,
401 namespace string,
402 values map[string]any,
403 opts ...InstallOption,
404) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400405 o := &installOptions{}
406 for _, i := range opts {
407 i(o)
408 }
409 if !o.NoLock {
410 m.l.Lock()
411 defer m.l.Unlock()
412 }
gioefa0ed42024-06-13 12:31:43 +0400413 portFields := findPortFields(app.Schema())
414 fakeReservations := map[string]reservePortResp{}
415 for i, f := range portFields {
416 fakeReservations[f] = reservePortResp{Port: i}
417 }
418 if err := setPortFields(values, fakeReservations); err != nil {
419 return ReleaseResources{}, err
420 }
gio3af43942024-04-16 08:13:50 +0400421 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400422 if !o.NoPull {
423 if err := m.repoIO.Pull(); err != nil {
424 return ReleaseResources{}, err
425 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400426 }
gio94904702024-07-26 16:58:34 +0400427 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400428 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400429 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400430 }
gio0eaf2712024-04-14 13:08:46 +0400431 var env EnvConfig
432 if o.Env != nil {
433 env = *o.Env
434 } else {
435 var err error
436 env, err = m.Config()
437 if err != nil {
438 return ReleaseResources{}, err
439 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400440 }
giocb34ad22024-07-11 08:01:13 +0400441 var networks []Network
442 if o.Networks != nil {
443 networks = o.Networks
444 } else {
445 var err error
446 networks, err = m.CreateNetworks(env)
447 if err != nil {
448 return ReleaseResources{}, err
449 }
450 }
giof8843412024-05-22 16:38:05 +0400451 var lg LocalChartGenerator
452 if o.LG != nil {
453 lg = o.LG
454 } else {
455 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
456 }
gio3cdee592024-04-17 10:15:56 +0400457 release := Release{
458 AppInstanceId: instanceId,
459 Namespace: namespace,
460 RepoAddr: m.repoIO.FullAddress(),
461 AppDir: appDir,
462 }
gio36b23b32024-08-25 12:20:54 +0400463 rendered, err := app.Render(release, env, networks, values, nil, m.vpnKeyGen)
gioef01fbb2024-04-12 16:52:59 +0400464 if err != nil {
gio778577f2024-04-29 09:44:38 +0400465 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400466 }
gioefa0ed42024-06-13 12:31:43 +0400467 reservators := map[string]string{}
468 allocators := map[string]string{}
469 for _, pf := range rendered.Ports {
470 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
471 allocators[portFields[pf.SourcePort]] = pf.Allocator
472 }
473 portReservations, err := reservePorts(reservators)
474 if err != nil {
475 return ReleaseResources{}, err
476 }
477 if err := setPortFields(values, portReservations); err != nil {
478 return ReleaseResources{}, err
479 }
gio7841f4f2024-07-26 19:53:49 +0400480 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400481 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
482 if o.FetchContainerImages {
483 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
484 return ReleaseResources{}, err
485 }
gio0eaf2712024-04-14 13:08:46 +0400486 }
giof71a0832024-06-27 14:45:45 +0400487 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
488 if err != nil {
giof8843412024-05-22 16:38:05 +0400489 return ReleaseResources{}, err
490 }
giof71a0832024-06-27 14:45:45 +0400491 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400492 if o.FetchContainerImages {
493 release.ImageRegistry = imageRegistry
494 }
gio36b23b32024-08-25 12:20:54 +0400495 rendered, err = app.Render(release, env, networks, values, localCharts, m.vpnKeyGen)
giof8843412024-05-22 16:38:05 +0400496 if err != nil {
497 return ReleaseResources{}, err
498 }
gio94904702024-07-26 16:58:34 +0400499 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400500 return ReleaseResources{}, err
501 }
gioff2a29a2024-05-01 17:06:42 +0400502 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400503 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400504 return ReleaseResources{}, err
505 }
gio778577f2024-04-29 09:44:38 +0400506 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400507 Release: rendered.Config.Release,
508 RenderedRaw: rendered.Raw,
509 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400510 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400511}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400512
gio778577f2024-04-29 09:44:38 +0400513type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400514 Metadata struct {
515 Name string `json:"name"`
516 Namespace string `json:"namespace"`
517 Annotations map[string]string `json:"annotations"`
518 } `json:"metadata"`
519 Kind string `json:"kind"`
520 Status struct {
gio778577f2024-04-29 09:44:38 +0400521 Conditions []struct {
522 Type string `json:"type"`
523 Status string `json:"status"`
524 } `json:"conditions"`
525 } `json:"status,omitempty"`
526}
527
528func extractHelm(resources CueAppData) []Resource {
529 ret := make([]Resource, 0, len(resources))
530 for _, contents := range resources {
531 var h helmRelease
532 if err := yaml.Unmarshal(contents, &h); err != nil {
533 panic(err) // TODO(gio): handle
534 }
gio0eaf2712024-04-14 13:08:46 +0400535 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400536 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400537 Name: h.Metadata.Name,
538 Namespace: h.Metadata.Namespace,
539 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
540 Annotations: nil,
giof9f0bee2024-06-11 20:10:05 +0400541 }
542 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400543 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400544 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
545 if ok && len(info) != 0 {
546 res.Info = info
547 }
548 }
549 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400550 }
gio778577f2024-04-29 09:44:38 +0400551 }
552 return ret
553}
554
giof8843412024-05-22 16:38:05 +0400555// TODO(gio): take app configuration from the repo
556func (m *AppManager) Update(
557 instanceId string,
558 values map[string]any,
559 opts ...InstallOption,
560) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400561 m.l.Lock()
562 defer m.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400563 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400564 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400565 }
gio3cdee592024-04-17 10:15:56 +0400566 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400567 if err != nil {
gio778577f2024-04-29 09:44:38 +0400568 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400569 }
gio308105e2024-04-19 13:12:13 +0400570 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400571 app, err := m.GetInstanceApp(instanceId)
572 if err != nil {
573 return ReleaseResources{}, err
574 }
gio308105e2024-04-19 13:12:13 +0400575 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400576 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400577 if err != nil {
gio778577f2024-04-29 09:44:38 +0400578 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400579 }
giocdfa3722024-06-13 20:10:14 +0400580 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400581 if err != nil {
582 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400583 }
giocb34ad22024-07-11 08:01:13 +0400584 networks, err := m.CreateNetworks(env)
585 if err != nil {
586 return ReleaseResources{}, err
587 }
gio36b23b32024-08-25 12:20:54 +0400588 rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts, m.vpnKeyGen)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400589 if err != nil {
gio778577f2024-04-29 09:44:38 +0400590 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400591 }
gio94904702024-07-26 16:58:34 +0400592 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
593 return ReleaseResources{}, err
594 }
595 return ReleaseResources{
596 Release: rendered.Config.Release,
597 RenderedRaw: rendered.Raw,
598 Helm: extractHelm(rendered.Resources),
599 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400600}
601
602func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400603 m.l.Lock()
604 defer m.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400605 if err := m.repoIO.Pull(); err != nil {
606 return err
607 }
giocdfa3722024-06-13 20:10:14 +0400608 var portForward []PortForward
giob4a3a192024-08-19 09:55:47 +0400609 if _, err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400610 instanceDir := filepath.Join(m.appDirRoot, instanceId)
611 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
612 if err != nil {
613 return "", err
614 }
615 portForward = renderedCfg.PortForward
616 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400617 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400618 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400619 if err != nil {
620 return "", err
621 }
622 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400623 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400624 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400625 }); err != nil {
626 return err
627 }
628 if err := closePorts(portForward); err != nil {
giocdfa3722024-06-13 20:10:14 +0400629 return err
630 }
631 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400632}
633
giocb34ad22024-07-11 08:01:13 +0400634func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
635 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400636 {
giocdfa3722024-06-13 20:10:14 +0400637 Name: "Public",
638 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
639 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
640 Domain: env.Domain,
641 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
642 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
643 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400644 },
gio7841f4f2024-07-26 19:53:49 +0400645 }
646 if env.PrivateDomain != "" {
647 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400648 Name: "Private",
649 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
650 Domain: env.PrivateDomain,
651 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
652 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
653 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400654 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400655 }
giocb34ad22024-07-11 08:01:13 +0400656 n, err := m.FindAllAppInstances("network")
657 if err != nil {
658 return nil, err
659 }
660 for _, a := range n {
661 ret = append(ret, Network{
662 Name: a.Input["name"].(string),
663 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
664 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
665 Domain: a.Input["domain"].(string),
666 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
667 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
668 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
669 })
670 }
671 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400672}
gio3cdee592024-04-17 10:15:56 +0400673
gio0eaf2712024-04-14 13:08:46 +0400674type installOptions struct {
gio94904702024-07-26 16:58:34 +0400675 NoPull bool
giof8843412024-05-22 16:38:05 +0400676 NoPublish bool
677 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400678 Networks []Network
giof8843412024-05-22 16:38:05 +0400679 Branch string
680 LG LocalChartGenerator
681 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400682 Force bool
gio9d66f322024-07-06 13:45:10 +0400683 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400684}
685
686type InstallOption func(*installOptions)
687
688func WithConfig(env *EnvConfig) InstallOption {
689 return func(o *installOptions) {
690 o.Env = env
691 }
692}
693
giocb34ad22024-07-11 08:01:13 +0400694func WithNetworks(networks []Network) InstallOption {
695 return func(o *installOptions) {
696 o.Networks = networks
697 }
698}
699
gio23bdc1b2024-07-11 16:07:47 +0400700func WithNoNetworks() InstallOption {
701 return WithNetworks([]Network{})
702}
703
gio0eaf2712024-04-14 13:08:46 +0400704func WithBranch(branch string) InstallOption {
705 return func(o *installOptions) {
706 o.Branch = branch
707 }
708}
709
giof71a0832024-06-27 14:45:45 +0400710func WithForce() InstallOption {
711 return func(o *installOptions) {
712 o.Force = true
713 }
714}
715
giof8843412024-05-22 16:38:05 +0400716func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
717 return func(o *installOptions) {
718 o.LG = lg
719 }
720}
721
722func WithFetchContainerImages() InstallOption {
723 return func(o *installOptions) {
724 o.FetchContainerImages = true
725 }
726}
727
728func WithNoPublish() InstallOption {
729 return func(o *installOptions) {
730 o.NoPublish = true
731 }
732}
733
gio94904702024-07-26 16:58:34 +0400734func WithNoPull() InstallOption {
735 return func(o *installOptions) {
736 o.NoPull = true
737 }
738}
739
gio9d66f322024-07-06 13:45:10 +0400740func WithNoLock() InstallOption {
741 return func(o *installOptions) {
742 o.NoLock = true
743 }
744}
745
giof8843412024-05-22 16:38:05 +0400746// InfraAppmanager
747
748type InfraAppManager struct {
749 repoIO soft.RepoIO
750 nsc NamespaceCreator
751 hf HelmFetcher
752 lg LocalChartGenerator
753}
754
755func NewInfraAppManager(
756 repoIO soft.RepoIO,
757 nsc NamespaceCreator,
758 hf HelmFetcher,
759 lg LocalChartGenerator,
760) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400761 return &InfraAppManager{
762 repoIO,
giof8843412024-05-22 16:38:05 +0400763 nsc,
764 hf,
765 lg,
gio3cdee592024-04-17 10:15:56 +0400766 }, nil
767}
768
769func (m *InfraAppManager) Config() (InfraConfig, error) {
770 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400771 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400772 return InfraConfig{}, err
773 } else {
774 return cfg, nil
775 }
776}
777
gioe72b54f2024-04-22 10:44:41 +0400778func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
779 var cfg InfraAppInstanceConfig
780 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
781 return InfraAppInstanceConfig{}, err
782 } else {
783 return cfg, nil
784 }
785}
786
787func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
788 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
789 if err != nil {
790 return InfraAppInstanceConfig{}, err
791 }
792 for _, app := range kust.Resources {
793 if app == id {
794 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
795 if err != nil {
796 return InfraAppInstanceConfig{}, err
797 }
798 cfg.Id = id
799 return cfg, nil
800 }
801 }
802 return InfraAppInstanceConfig{}, nil
803}
804
gio778577f2024-04-29 09:44:38 +0400805func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400806 appDir = filepath.Clean(appDir)
807 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400808 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400809 }
giof8843412024-05-22 16:38:05 +0400810 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400811 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400812 }
813 infra, err := m.Config()
814 if err != nil {
gio778577f2024-04-29 09:44:38 +0400815 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400816 }
817 release := Release{
818 Namespace: namespace,
819 RepoAddr: m.repoIO.FullAddress(),
820 AppDir: appDir,
821 }
gio7841f4f2024-07-26 19:53:49 +0400822 networks := m.CreateNetworks(infra)
823 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +0400824 if err != nil {
825 return ReleaseResources{}, err
826 }
giof71a0832024-06-27 14:45:45 +0400827 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
828 if err != nil {
giof8843412024-05-22 16:38:05 +0400829 return ReleaseResources{}, err
830 }
giof71a0832024-06-27 14:45:45 +0400831 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +0400832 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400833 if err != nil {
gio778577f2024-04-29 09:44:38 +0400834 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400835 }
gio94904702024-07-26 16:58:34 +0400836 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
837 return ReleaseResources{}, err
838 }
839 return ReleaseResources{
840 Release: rendered.Config.Release,
841 RenderedRaw: rendered.Raw,
842 Helm: extractHelm(rendered.Resources),
843 }, nil
gioe72b54f2024-04-22 10:44:41 +0400844}
845
giof8843412024-05-22 16:38:05 +0400846// TODO(gio): take app configuration from the repo
847func (m *InfraAppManager) Update(
848 instanceId string,
849 values map[string]any,
850 opts ...InstallOption,
851) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400852 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400853 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400854 }
gio7841f4f2024-07-26 19:53:49 +0400855 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +0400856 if err != nil {
gio778577f2024-04-29 09:44:38 +0400857 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400858 }
859 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400860 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
861 if err != nil {
862 return ReleaseResources{}, err
863 }
864 app, err := NewCueInfraApp(appCfg)
865 if err != nil {
866 return ReleaseResources{}, err
867 }
gioe72b54f2024-04-22 10:44:41 +0400868 instanceConfigPath := filepath.Join(instanceDir, "config.json")
869 config, err := m.appConfig(instanceConfigPath)
870 if err != nil {
gio778577f2024-04-29 09:44:38 +0400871 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400872 }
giocdfa3722024-06-13 20:10:14 +0400873 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400874 if err != nil {
875 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400876 }
gio7841f4f2024-07-26 19:53:49 +0400877 networks := m.CreateNetworks(infra)
878 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400879 if err != nil {
gio778577f2024-04-29 09:44:38 +0400880 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400881 }
gio94904702024-07-26 16:58:34 +0400882 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
883 return ReleaseResources{}, err
884 }
885 return ReleaseResources{
886 Release: rendered.Config.Release,
887 RenderedRaw: rendered.Raw,
888 Helm: extractHelm(rendered.Resources),
889 }, nil
gio3cdee592024-04-17 10:15:56 +0400890}
giof8843412024-05-22 16:38:05 +0400891
gio7841f4f2024-07-26 19:53:49 +0400892func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
893 return []InfraNetwork{
894 {
895 Name: "Public",
896 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
897 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
898 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
899 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
900 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
901 },
902 }
903}
904
giof8843412024-05-22 16:38:05 +0400905func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
906 ret := make(map[string]string)
907 for name, chart := range charts.Git {
908 chartRoot := filepath.Join(root, name)
909 ret[name] = chartRoot
910 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
911 return nil, err
912 }
913 }
914 return ret, nil
915}
916
917func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
918 ret := make(map[string]helmv2.HelmChartTemplateSpec)
919 for name, path := range charts {
920 ret[name] = g.Generate(path)
921 }
922 return ret
923}
924
925func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
926 for _, img := range imgs {
927 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
928 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
929 "skopeo",
930 "--insecure-policy",
931 "copy",
932 "--dest-tls-verify=false", // TODO(gio): enable
933 "--multi-arch=all",
934 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
935 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
936 }); err != nil {
937 return err
938 }
939 }
940 return nil
941}
942
943type renderedInstance struct {
944 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400945 PortForward []PortForward `json:"portForward"`
giof8843412024-05-22 16:38:05 +0400946}
947
giocdfa3722024-06-13 20:10:14 +0400948func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400949 r, err := fs.Reader(path)
950 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400951 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400952 }
953 defer r.Close()
954 var cfg renderedInstance
955 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400956 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400957 }
giocdfa3722024-06-13 20:10:14 +0400958 return cfg, nil
giof8843412024-05-22 16:38:05 +0400959}
gioefa0ed42024-06-13 12:31:43 +0400960
961func findPortFields(scm Schema) []string {
962 switch scm.Kind() {
963 case KindBoolean:
964 return []string{}
965 case KindInt:
966 return []string{}
967 case KindString:
968 return []string{}
969 case KindStruct:
970 ret := []string{}
971 for _, f := range scm.Fields() {
972 for _, p := range findPortFields(f.Schema) {
973 if p == "" {
974 ret = append(ret, f.Name)
975 } else {
976 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
977 }
978 }
979 }
980 return ret
981 case KindNetwork:
982 return []string{}
gio4ece99c2024-07-18 11:05:50 +0400983 case KindMultiNetwork:
984 return []string{}
gioefa0ed42024-06-13 12:31:43 +0400985 case KindAuth:
986 return []string{}
987 case KindSSHKey:
988 return []string{}
989 case KindNumber:
990 return []string{}
991 case KindArrayString:
992 return []string{}
993 case KindPort:
994 return []string{""}
gio36b23b32024-08-25 12:20:54 +0400995 case KindVPNAuthKey:
996 return []string{}
gioefa0ed42024-06-13 12:31:43 +0400997 default:
998 panic("MUST NOT REACH!")
999 }
1000}
1001
1002func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1003 for p, r := range ports {
1004 if err := setPortField(values, p, r.Port); err != nil {
1005 return err
1006 }
1007 }
1008 return nil
1009}
1010
1011func setPortField(values map[string]any, field string, port int) error {
1012 f := strings.SplitN(field, ".", 2)
1013 if len(f) == 2 {
1014 var sub map[string]any
1015 if s, ok := values[f[0]]; ok {
1016 sub, ok = s.(map[string]any)
1017 if !ok {
1018 return fmt.Errorf("expected map")
1019 }
1020 } else {
1021 sub = map[string]any{}
1022 values[f[0]] = sub
1023 }
1024 if err := setPortField(sub, f[1], port); err != nil {
1025 return err
1026 }
1027 } else {
1028 values[f[0]] = port
1029 }
1030 return nil
1031}