blob: 47720764110e120bc38fee29934cde3330fbb068 [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 {
291 Helm []Resource
292}
293
gio3cdee592024-04-17 10:15:56 +0400294// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400295func installApp(
gioe72b54f2024-04-22 10:44:41 +0400296 repo soft.RepoIO,
297 appDir string,
298 name string,
299 config any,
300 ports []PortForward,
301 resources CueAppData,
302 data CueAppData,
giof8843412024-05-22 16:38:05 +0400303 opts ...InstallOption,
gio778577f2024-04-29 09:44:38 +0400304) (ReleaseResources, error) {
giof8843412024-05-22 16:38:05 +0400305 var o installOptions
306 for _, i := range opts {
307 i(&o)
308 }
309 dopts := []soft.DoOption{}
giof71a0832024-06-27 14:45:45 +0400310 // NOTE(gio): Expects caller to have pulled already
311 dopts = append(dopts, soft.WithNoPull())
giof8843412024-05-22 16:38:05 +0400312 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400313 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
314 }
315 if o.NoPublish {
316 dopts = append(dopts, soft.WithNoCommit())
317 }
giof71a0832024-06-27 14:45:45 +0400318 if o.Force {
319 dopts = append(dopts, soft.WithForce())
320 }
gio9d66f322024-07-06 13:45:10 +0400321 if o.NoLock {
322 dopts = append(dopts, soft.WithNoLock())
323 }
gio778577f2024-04-29 09:44:38 +0400324 return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400325 if err := r.RemoveDir(appDir); err != nil {
326 return "", err
327 }
328 resourcesDir := path.Join(appDir, "resources")
329 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400330 return "", err
331 }
332 {
gio5e49bb62024-07-20 10:43:19 +0400333 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
334 return "", err
335 }
gioe72b54f2024-04-22 10:44:41 +0400336 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400337 return "", err
338 }
gioe72b54f2024-04-22 10:44:41 +0400339 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400340 return "", err
341 }
gioe72b54f2024-04-22 10:44:41 +0400342 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400343 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
344 return "", fmt.Errorf("%s is forbidden", name)
345 }
346 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400347 if err != nil {
348 return "", err
349 }
gio308105e2024-04-19 13:12:13 +0400350 defer w.Close()
351 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400352 return "", err
353 }
354 }
gio308105e2024-04-19 13:12:13 +0400355 }
356 {
357 if err := createKustomizationChain(r, resourcesDir); err != nil {
358 return "", err
359 }
gioefa0ed42024-06-13 12:31:43 +0400360 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400361 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400362 appKust.AddResources(name)
363 w, err := r.Writer(path.Join(resourcesDir, name))
364 if err != nil {
365 return "", err
366 }
367 defer w.Close()
368 if _, err := w.Write(contents); err != nil {
369 return "", err
370 }
371 }
gioe72b54f2024-04-22 10:44:41 +0400372 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400373 return "", err
374 }
375 }
gioe72b54f2024-04-22 10:44:41 +0400376 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400377 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400378}
379
gio3cdee592024-04-17 10:15:56 +0400380// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400381func (m *AppManager) Install(
382 app EnvApp,
383 instanceId string,
384 appDir string,
385 namespace string,
386 values map[string]any,
387 opts ...InstallOption,
388) (ReleaseResources, error) {
gioefa0ed42024-06-13 12:31:43 +0400389 portFields := findPortFields(app.Schema())
390 fakeReservations := map[string]reservePortResp{}
391 for i, f := range portFields {
392 fakeReservations[f] = reservePortResp{Port: i}
393 }
394 if err := setPortFields(values, fakeReservations); err != nil {
395 return ReleaseResources{}, err
396 }
gio0eaf2712024-04-14 13:08:46 +0400397 o := &installOptions{}
398 for _, i := range opts {
399 i(o)
400 }
gio3af43942024-04-16 08:13:50 +0400401 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400402 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400403 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400404 }
giof8843412024-05-22 16:38:05 +0400405 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400406 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400407 }
gio0eaf2712024-04-14 13:08:46 +0400408 var env EnvConfig
409 if o.Env != nil {
410 env = *o.Env
411 } else {
412 var err error
413 env, err = m.Config()
414 if err != nil {
415 return ReleaseResources{}, err
416 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400417 }
giocb34ad22024-07-11 08:01:13 +0400418 var networks []Network
419 if o.Networks != nil {
420 networks = o.Networks
421 } else {
422 var err error
423 networks, err = m.CreateNetworks(env)
424 if err != nil {
425 return ReleaseResources{}, err
426 }
427 }
giof8843412024-05-22 16:38:05 +0400428 var lg LocalChartGenerator
429 if o.LG != nil {
430 lg = o.LG
431 } else {
432 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
433 }
gio3cdee592024-04-17 10:15:56 +0400434 release := Release{
435 AppInstanceId: instanceId,
436 Namespace: namespace,
437 RepoAddr: m.repoIO.FullAddress(),
438 AppDir: appDir,
439 }
giocb34ad22024-07-11 08:01:13 +0400440 rendered, err := app.Render(release, env, networks, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400441 if err != nil {
gio778577f2024-04-29 09:44:38 +0400442 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400443 }
gioefa0ed42024-06-13 12:31:43 +0400444 reservators := map[string]string{}
445 allocators := map[string]string{}
446 for _, pf := range rendered.Ports {
447 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
448 allocators[portFields[pf.SourcePort]] = pf.Allocator
449 }
450 portReservations, err := reservePorts(reservators)
451 if err != nil {
452 return ReleaseResources{}, err
453 }
454 if err := setPortFields(values, portReservations); err != nil {
455 return ReleaseResources{}, err
456 }
giof8843412024-05-22 16:38:05 +0400457 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
458 if o.FetchContainerImages {
459 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
460 return ReleaseResources{}, err
461 }
gio0eaf2712024-04-14 13:08:46 +0400462 }
giof71a0832024-06-27 14:45:45 +0400463 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
464 if err != nil {
giof8843412024-05-22 16:38:05 +0400465 return ReleaseResources{}, err
466 }
giof71a0832024-06-27 14:45:45 +0400467 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400468 if o.FetchContainerImages {
469 release.ImageRegistry = imageRegistry
470 }
giocb34ad22024-07-11 08:01:13 +0400471 rendered, err = app.Render(release, env, networks, values, localCharts)
giof8843412024-05-22 16:38:05 +0400472 if err != nil {
473 return ReleaseResources{}, err
474 }
475 if _, err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400476 return ReleaseResources{}, err
477 }
gioff2a29a2024-05-01 17:06:42 +0400478 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400479 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400480 return ReleaseResources{}, err
481 }
gio778577f2024-04-29 09:44:38 +0400482 return ReleaseResources{
483 Helm: extractHelm(rendered.Resources),
484 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400485}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400486
gio778577f2024-04-29 09:44:38 +0400487type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400488 Metadata struct {
489 Name string `json:"name"`
490 Namespace string `json:"namespace"`
491 Annotations map[string]string `json:"annotations"`
492 } `json:"metadata"`
493 Kind string `json:"kind"`
494 Status struct {
gio778577f2024-04-29 09:44:38 +0400495 Conditions []struct {
496 Type string `json:"type"`
497 Status string `json:"status"`
498 } `json:"conditions"`
499 } `json:"status,omitempty"`
500}
501
502func extractHelm(resources CueAppData) []Resource {
503 ret := make([]Resource, 0, len(resources))
504 for _, contents := range resources {
505 var h helmRelease
506 if err := yaml.Unmarshal(contents, &h); err != nil {
507 panic(err) // TODO(gio): handle
508 }
gio0eaf2712024-04-14 13:08:46 +0400509 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400510 res := Resource{
511 Name: h.Metadata.Name,
512 Namespace: h.Metadata.Namespace,
513 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
514 }
515 if h.Metadata.Annotations != nil {
516 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
517 if ok && len(info) != 0 {
518 res.Info = info
519 }
520 }
521 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400522 }
gio778577f2024-04-29 09:44:38 +0400523 }
524 return ret
525}
526
giof8843412024-05-22 16:38:05 +0400527// TODO(gio): take app configuration from the repo
528func (m *AppManager) Update(
529 instanceId string,
530 values map[string]any,
531 opts ...InstallOption,
532) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400533 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400534 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400535 }
gio3cdee592024-04-17 10:15:56 +0400536 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400537 if err != nil {
gio778577f2024-04-29 09:44:38 +0400538 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400539 }
gio308105e2024-04-19 13:12:13 +0400540 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400541 app, err := m.GetInstanceApp(instanceId)
542 if err != nil {
543 return ReleaseResources{}, err
544 }
gio308105e2024-04-19 13:12:13 +0400545 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400546 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400547 if err != nil {
gio778577f2024-04-29 09:44:38 +0400548 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400549 }
giocdfa3722024-06-13 20:10:14 +0400550 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400551 if err != nil {
552 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400553 }
giocb34ad22024-07-11 08:01:13 +0400554 networks, err := m.CreateNetworks(env)
555 if err != nil {
556 return ReleaseResources{}, err
557 }
558 rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400559 if err != nil {
gio778577f2024-04-29 09:44:38 +0400560 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400561 }
gio0eaf2712024-04-14 13:08:46 +0400562 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400563}
564
565func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400566 if err := m.repoIO.Pull(); err != nil {
567 return err
568 }
giocdfa3722024-06-13 20:10:14 +0400569 var portForward []PortForward
570 if err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
571 instanceDir := filepath.Join(m.appDirRoot, instanceId)
572 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
573 if err != nil {
574 return "", err
575 }
576 portForward = renderedCfg.PortForward
577 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400578 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400579 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400580 if err != nil {
581 return "", err
582 }
583 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400584 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400585 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400586 }); err != nil {
587 return err
588 }
589 if err := closePorts(portForward); err != nil {
giocdfa3722024-06-13 20:10:14 +0400590 return err
591 }
592 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400593}
594
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400595// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
giocb34ad22024-07-11 08:01:13 +0400596func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
597 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400598 {
giocdfa3722024-06-13 20:10:14 +0400599 Name: "Public",
600 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
601 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
602 Domain: env.Domain,
603 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
604 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
605 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400606 },
607 {
giocdfa3722024-06-13 20:10:14 +0400608 Name: "Private",
609 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
610 Domain: env.PrivateDomain,
611 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
612 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
613 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400614 },
615 }
giocb34ad22024-07-11 08:01:13 +0400616 n, err := m.FindAllAppInstances("network")
617 if err != nil {
618 return nil, err
619 }
620 for _, a := range n {
621 ret = append(ret, Network{
622 Name: a.Input["name"].(string),
623 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
624 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
625 Domain: a.Input["domain"].(string),
626 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
627 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
628 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
629 })
630 }
631 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400632}
gio3cdee592024-04-17 10:15:56 +0400633
gio0eaf2712024-04-14 13:08:46 +0400634type installOptions struct {
giof8843412024-05-22 16:38:05 +0400635 NoPublish bool
636 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400637 Networks []Network
giof8843412024-05-22 16:38:05 +0400638 Branch string
639 LG LocalChartGenerator
640 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400641 Force bool
gio9d66f322024-07-06 13:45:10 +0400642 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400643}
644
645type InstallOption func(*installOptions)
646
647func WithConfig(env *EnvConfig) InstallOption {
648 return func(o *installOptions) {
649 o.Env = env
650 }
651}
652
giocb34ad22024-07-11 08:01:13 +0400653func WithNetworks(networks []Network) InstallOption {
654 return func(o *installOptions) {
655 o.Networks = networks
656 }
657}
658
gio23bdc1b2024-07-11 16:07:47 +0400659func WithNoNetworks() InstallOption {
660 return WithNetworks([]Network{})
661}
662
gio0eaf2712024-04-14 13:08:46 +0400663func WithBranch(branch string) InstallOption {
664 return func(o *installOptions) {
665 o.Branch = branch
666 }
667}
668
giof71a0832024-06-27 14:45:45 +0400669func WithForce() InstallOption {
670 return func(o *installOptions) {
671 o.Force = true
672 }
673}
674
giof8843412024-05-22 16:38:05 +0400675func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
676 return func(o *installOptions) {
677 o.LG = lg
678 }
679}
680
681func WithFetchContainerImages() InstallOption {
682 return func(o *installOptions) {
683 o.FetchContainerImages = true
684 }
685}
686
687func WithNoPublish() InstallOption {
688 return func(o *installOptions) {
689 o.NoPublish = true
690 }
691}
692
gio9d66f322024-07-06 13:45:10 +0400693func WithNoLock() InstallOption {
694 return func(o *installOptions) {
695 o.NoLock = true
696 }
697}
698
giof8843412024-05-22 16:38:05 +0400699// InfraAppmanager
700
701type InfraAppManager struct {
702 repoIO soft.RepoIO
703 nsc NamespaceCreator
704 hf HelmFetcher
705 lg LocalChartGenerator
706}
707
708func NewInfraAppManager(
709 repoIO soft.RepoIO,
710 nsc NamespaceCreator,
711 hf HelmFetcher,
712 lg LocalChartGenerator,
713) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400714 return &InfraAppManager{
715 repoIO,
giof8843412024-05-22 16:38:05 +0400716 nsc,
717 hf,
718 lg,
gio3cdee592024-04-17 10:15:56 +0400719 }, nil
720}
721
722func (m *InfraAppManager) Config() (InfraConfig, error) {
723 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400724 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400725 return InfraConfig{}, err
726 } else {
727 return cfg, nil
728 }
729}
730
gioe72b54f2024-04-22 10:44:41 +0400731func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
732 var cfg InfraAppInstanceConfig
733 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
734 return InfraAppInstanceConfig{}, err
735 } else {
736 return cfg, nil
737 }
738}
739
740func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
741 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
742 if err != nil {
743 return InfraAppInstanceConfig{}, err
744 }
745 for _, app := range kust.Resources {
746 if app == id {
747 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
748 if err != nil {
749 return InfraAppInstanceConfig{}, err
750 }
751 cfg.Id = id
752 return cfg, nil
753 }
754 }
755 return InfraAppInstanceConfig{}, nil
756}
757
gio778577f2024-04-29 09:44:38 +0400758func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400759 appDir = filepath.Clean(appDir)
760 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400761 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400762 }
giof8843412024-05-22 16:38:05 +0400763 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400764 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400765 }
766 infra, err := m.Config()
767 if err != nil {
gio778577f2024-04-29 09:44:38 +0400768 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400769 }
770 release := Release{
771 Namespace: namespace,
772 RepoAddr: m.repoIO.FullAddress(),
773 AppDir: appDir,
774 }
giof8843412024-05-22 16:38:05 +0400775 rendered, err := app.Render(release, infra, values, nil)
776 if err != nil {
777 return ReleaseResources{}, err
778 }
giof71a0832024-06-27 14:45:45 +0400779 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
780 if err != nil {
giof8843412024-05-22 16:38:05 +0400781 return ReleaseResources{}, err
782 }
giof71a0832024-06-27 14:45:45 +0400783 localCharts := generateLocalCharts(m.lg, charts)
giof8843412024-05-22 16:38:05 +0400784 rendered, err = app.Render(release, infra, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400785 if err != nil {
gio778577f2024-04-29 09:44:38 +0400786 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400787 }
gio0eaf2712024-04-14 13:08:46 +0400788 return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
gioe72b54f2024-04-22 10:44:41 +0400789}
790
giof8843412024-05-22 16:38:05 +0400791// TODO(gio): take app configuration from the repo
792func (m *InfraAppManager) Update(
793 instanceId string,
794 values map[string]any,
795 opts ...InstallOption,
796) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400797 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400798 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400799 }
800 env, err := m.Config()
801 if err != nil {
gio778577f2024-04-29 09:44:38 +0400802 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400803 }
804 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400805 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
806 if err != nil {
807 return ReleaseResources{}, err
808 }
809 app, err := NewCueInfraApp(appCfg)
810 if err != nil {
811 return ReleaseResources{}, err
812 }
gioe72b54f2024-04-22 10:44:41 +0400813 instanceConfigPath := filepath.Join(instanceDir, "config.json")
814 config, err := m.appConfig(instanceConfigPath)
815 if err != nil {
gio778577f2024-04-29 09:44:38 +0400816 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400817 }
giocdfa3722024-06-13 20:10:14 +0400818 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400819 if err != nil {
820 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400821 }
giocdfa3722024-06-13 20:10:14 +0400822 rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400823 if err != nil {
gio778577f2024-04-29 09:44:38 +0400824 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400825 }
gio0eaf2712024-04-14 13:08:46 +0400826 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400827}
giof8843412024-05-22 16:38:05 +0400828
829func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
830 ret := make(map[string]string)
831 for name, chart := range charts.Git {
832 chartRoot := filepath.Join(root, name)
833 ret[name] = chartRoot
834 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
835 return nil, err
836 }
837 }
838 return ret, nil
839}
840
841func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
842 ret := make(map[string]helmv2.HelmChartTemplateSpec)
843 for name, path := range charts {
844 ret[name] = g.Generate(path)
845 }
846 return ret
847}
848
849func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
850 for _, img := range imgs {
851 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
852 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
853 "skopeo",
854 "--insecure-policy",
855 "copy",
856 "--dest-tls-verify=false", // TODO(gio): enable
857 "--multi-arch=all",
858 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
859 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
860 }); err != nil {
861 return err
862 }
863 }
864 return nil
865}
866
867type renderedInstance struct {
868 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400869 PortForward []PortForward `json:"portForward"`
giof8843412024-05-22 16:38:05 +0400870}
871
giocdfa3722024-06-13 20:10:14 +0400872func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400873 r, err := fs.Reader(path)
874 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400875 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400876 }
877 defer r.Close()
878 var cfg renderedInstance
879 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400880 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400881 }
giocdfa3722024-06-13 20:10:14 +0400882 return cfg, nil
giof8843412024-05-22 16:38:05 +0400883}
gioefa0ed42024-06-13 12:31:43 +0400884
885func findPortFields(scm Schema) []string {
886 switch scm.Kind() {
887 case KindBoolean:
888 return []string{}
889 case KindInt:
890 return []string{}
891 case KindString:
892 return []string{}
893 case KindStruct:
894 ret := []string{}
895 for _, f := range scm.Fields() {
896 for _, p := range findPortFields(f.Schema) {
897 if p == "" {
898 ret = append(ret, f.Name)
899 } else {
900 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
901 }
902 }
903 }
904 return ret
905 case KindNetwork:
906 return []string{}
gio4ece99c2024-07-18 11:05:50 +0400907 case KindMultiNetwork:
908 return []string{}
gioefa0ed42024-06-13 12:31:43 +0400909 case KindAuth:
910 return []string{}
911 case KindSSHKey:
912 return []string{}
913 case KindNumber:
914 return []string{}
915 case KindArrayString:
916 return []string{}
917 case KindPort:
918 return []string{""}
919 default:
920 panic("MUST NOT REACH!")
921 }
922}
923
924func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
925 for p, r := range ports {
926 if err := setPortField(values, p, r.Port); err != nil {
927 return err
928 }
929 }
930 return nil
931}
932
933func setPortField(values map[string]any, field string, port int) error {
934 f := strings.SplitN(field, ".", 2)
935 if len(f) == 2 {
936 var sub map[string]any
937 if s, ok := values[f[0]]; ok {
938 sub, ok = s.(map[string]any)
939 if !ok {
940 return fmt.Errorf("expected map")
941 }
942 } else {
943 sub = map[string]any{}
944 values[f[0]] = sub
945 }
946 if err := setPortField(sub, f[1], port); err != nil {
947 return err
948 }
949 } else {
950 values[f[0]] = port
951 }
952 return nil
953}