blob: dda7f8bfc14735506ada8639d60d5283e1e410ec [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
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040022const configFileName = "config.yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040023const kustomizationFileName = "kustomization.yaml"
24
gio778577f2024-04-29 09:44:38 +040025var ErrorNotFound = errors.New("not found")
26
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040027type AppManager struct {
gioe72b54f2024-04-22 10:44:41 +040028 repoIO soft.RepoIO
giof8843412024-05-22 16:38:05 +040029 nsc NamespaceCreator
30 jc JobCreator
31 hf HelmFetcher
gio308105e2024-04-19 13:12:13 +040032 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040033}
34
giof8843412024-05-22 16:38:05 +040035func NewAppManager(
36 repoIO soft.RepoIO,
37 nsc NamespaceCreator,
38 jc JobCreator,
39 hf HelmFetcher,
40 appDirRoot string,
41) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040042 return &AppManager{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040043 repoIO,
giof8843412024-05-22 16:38:05 +040044 nsc,
45 jc,
46 hf,
gio308105e2024-04-19 13:12:13 +040047 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040048 }, nil
49}
50
gioe72b54f2024-04-22 10:44:41 +040051func (m *AppManager) Config() (EnvConfig, error) {
52 var cfg EnvConfig
53 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
54 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040055 } else {
56 return cfg, nil
57 }
58}
59
gio3cdee592024-04-17 10:15:56 +040060func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
61 var cfg AppInstanceConfig
gioe72b54f2024-04-22 10:44:41 +040062 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040063 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040064 } else {
65 return cfg, nil
66 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040067}
68
gio308105e2024-04-19 13:12:13 +040069func (m *AppManager) FindAllInstances() ([]AppInstanceConfig, error) {
gio09a3e5b2024-04-26 14:11:06 +040070 m.repoIO.Pull()
gioe72b54f2024-04-22 10:44:41 +040071 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040072 if err != nil {
73 return nil, err
74 }
gio3cdee592024-04-17 10:15:56 +040075 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040076 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040077 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
78 if err != nil {
79 return nil, err
80 }
81 cfg.Id = app
82 ret = append(ret, cfg)
83 }
84 return ret, nil
85}
86
87func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040088 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio308105e2024-04-19 13:12:13 +040089 if err != nil {
giocb34ad22024-07-11 08:01:13 +040090 if errors.Is(err, fs.ErrNotExist) {
91 return nil, nil
92 } else {
93 return nil, err
94 }
gio308105e2024-04-19 13:12:13 +040095 }
96 ret := make([]AppInstanceConfig, 0)
97 for _, app := range kust.Resources {
98 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +040099 if err != nil {
100 return nil, err
101 }
102 cfg.Id = app
103 if cfg.AppId == name {
104 ret = append(ret, cfg)
105 }
106 }
107 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400108}
109
gio778577f2024-04-29 09:44:38 +0400110func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +0400111 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +0400112 if err != nil {
gio778577f2024-04-29 09:44:38 +0400113 return nil, err
gio3af43942024-04-16 08:13:50 +0400114 }
115 for _, app := range kust.Resources {
116 if app == id {
gio308105e2024-04-19 13:12:13 +0400117 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400118 if err != nil {
gio778577f2024-04-29 09:44:38 +0400119 return nil, err
gio3af43942024-04-16 08:13:50 +0400120 }
121 cfg.Id = id
gio778577f2024-04-29 09:44:38 +0400122 return &cfg, nil
gio3af43942024-04-16 08:13:50 +0400123 }
124 }
gio778577f2024-04-29 09:44:38 +0400125 return nil, ErrorNotFound
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400126}
127
giof8843412024-05-22 16:38:05 +0400128func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
129 files, err := fs.ListDir(dir)
130 if err != nil {
131 return nil, err
132 }
133 cfg := CueAppData{}
134 for _, f := range files {
135 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
136 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
137 if err != nil {
138 return nil, err
139 }
140 cfg[f.Name()] = contents
141 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400142 }
gio308105e2024-04-19 13:12:13 +0400143 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400144}
145
giof8843412024-05-22 16:38:05 +0400146func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
147 cfg, err := GetCueAppData(m.repoIO, filepath.Join(m.appDirRoot, id))
148 if err != nil {
149 return nil, err
150 }
151 return NewCueEnvApp(cfg)
152}
153
gio3af43942024-04-16 08:13:50 +0400154type allocatePortReq struct {
155 Protocol string `json:"protocol"`
156 SourcePort int `json:"sourcePort"`
157 TargetService string `json:"targetService"`
158 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400159 Secret string `json:"secret,omitempty"`
160}
161
162type removePortReq struct {
163 Protocol string `json:"protocol"`
164 SourcePort int `json:"sourcePort"`
165 TargetService string `json:"targetService"`
166 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400167}
168
gioefa0ed42024-06-13 12:31:43 +0400169type reservePortResp struct {
170 Port int `json:"port"`
171 Secret string `json:"secret"`
172}
173
174func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
175 ret := map[string]reservePortResp{}
176 for p, reserveAddr := range ports {
177 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
178 if err != nil {
179 return nil, err
180 }
181 if resp.StatusCode != http.StatusOK {
182 var e bytes.Buffer
183 io.Copy(&e, resp.Body)
184 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
185 }
186 var r reservePortResp
187 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
188 return nil, err
189 }
190 ret[p] = r
191 }
192 return ret, nil
193}
194
195func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400196 for _, p := range ports {
197 var buf bytes.Buffer
198 req := allocatePortReq{
199 Protocol: p.Protocol,
200 SourcePort: p.SourcePort,
201 TargetService: p.TargetService,
202 TargetPort: p.TargetPort,
203 }
gioefa0ed42024-06-13 12:31:43 +0400204 allocator := ""
205 for n, r := range reservations {
206 if p.SourcePort == r.Port {
207 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400208 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400209 break
210 }
211 }
212 if allocator == "" {
213 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
214 }
giobd7ab0b2024-06-17 12:55:17 +0400215 if err := json.NewEncoder(&buf).Encode(req); err != nil {
216 return err
217 }
gioefa0ed42024-06-13 12:31:43 +0400218 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400219 if err != nil {
220 return err
221 }
222 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400223 var r bytes.Buffer
224 io.Copy(&r, resp.Body)
225 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 +0400226 }
227 }
228 return nil
229}
230
giocdfa3722024-06-13 20:10:14 +0400231func closePorts(ports []PortForward) error {
232 var retErr error
233 for _, p := range ports {
234 var buf bytes.Buffer
235 req := removePortReq{
236 Protocol: p.Protocol,
237 SourcePort: p.SourcePort,
238 TargetService: p.TargetService,
239 TargetPort: p.TargetPort,
240 }
241 if err := json.NewEncoder(&buf).Encode(req); err != nil {
242 retErr = err
243 continue
244 }
245 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
246 if err != nil {
247 retErr = err
248 continue
249 }
250 if resp.StatusCode != http.StatusOK {
251 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
252 continue
253 }
254 }
255 return retErr
256}
257
gioe72b54f2024-04-22 10:44:41 +0400258func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400259 for p := filepath.Clean(path); p != "/"; {
260 parent, child := filepath.Split(p)
261 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400262 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400263 if err != nil {
264 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400265 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400266 kust = &k
267 } else {
268 return err
269 }
270 }
271 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400272 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400273 return err
274 }
275 p = filepath.Clean(parent)
276 }
277 return nil
278}
279
gio778577f2024-04-29 09:44:38 +0400280type Resource struct {
281 Name string `json:"name"`
282 Namespace string `json:"namespace"`
giof9f0bee2024-06-11 20:10:05 +0400283 Info string `json:"info"`
gio778577f2024-04-29 09:44:38 +0400284}
285
286type ReleaseResources struct {
287 Helm []Resource
288}
289
gio3cdee592024-04-17 10:15:56 +0400290// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400291func installApp(
gioe72b54f2024-04-22 10:44:41 +0400292 repo soft.RepoIO,
293 appDir string,
294 name string,
295 config any,
296 ports []PortForward,
297 resources CueAppData,
298 data CueAppData,
giof8843412024-05-22 16:38:05 +0400299 opts ...InstallOption,
gio778577f2024-04-29 09:44:38 +0400300) (ReleaseResources, error) {
giof8843412024-05-22 16:38:05 +0400301 var o installOptions
302 for _, i := range opts {
303 i(&o)
304 }
305 dopts := []soft.DoOption{}
giof71a0832024-06-27 14:45:45 +0400306 // NOTE(gio): Expects caller to have pulled already
307 dopts = append(dopts, soft.WithNoPull())
giof8843412024-05-22 16:38:05 +0400308 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400309 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
310 }
311 if o.NoPublish {
312 dopts = append(dopts, soft.WithNoCommit())
313 }
giof71a0832024-06-27 14:45:45 +0400314 if o.Force {
315 dopts = append(dopts, soft.WithForce())
316 }
gio9d66f322024-07-06 13:45:10 +0400317 if o.NoLock {
318 dopts = append(dopts, soft.WithNoLock())
319 }
gio778577f2024-04-29 09:44:38 +0400320 return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400321 if err := r.RemoveDir(appDir); err != nil {
322 return "", err
323 }
324 resourcesDir := path.Join(appDir, "resources")
325 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400326 return "", err
327 }
328 {
gioe72b54f2024-04-22 10:44:41 +0400329 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400330 return "", err
331 }
gioe72b54f2024-04-22 10:44:41 +0400332 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400333 return "", err
334 }
gioe72b54f2024-04-22 10:44:41 +0400335 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400336 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
337 return "", fmt.Errorf("%s is forbidden", name)
338 }
339 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400340 if err != nil {
341 return "", err
342 }
gio308105e2024-04-19 13:12:13 +0400343 defer w.Close()
344 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400345 return "", err
346 }
347 }
gio308105e2024-04-19 13:12:13 +0400348 }
349 {
350 if err := createKustomizationChain(r, resourcesDir); err != nil {
351 return "", err
352 }
gioefa0ed42024-06-13 12:31:43 +0400353 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400354 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400355 appKust.AddResources(name)
356 w, err := r.Writer(path.Join(resourcesDir, name))
357 if err != nil {
358 return "", err
359 }
360 defer w.Close()
361 if _, err := w.Write(contents); err != nil {
362 return "", err
363 }
364 }
gioe72b54f2024-04-22 10:44:41 +0400365 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400366 return "", err
367 }
368 }
gioe72b54f2024-04-22 10:44:41 +0400369 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400370 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400371}
372
gio3cdee592024-04-17 10:15:56 +0400373// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400374func (m *AppManager) Install(
375 app EnvApp,
376 instanceId string,
377 appDir string,
378 namespace string,
379 values map[string]any,
380 opts ...InstallOption,
381) (ReleaseResources, error) {
gioefa0ed42024-06-13 12:31:43 +0400382 portFields := findPortFields(app.Schema())
383 fakeReservations := map[string]reservePortResp{}
384 for i, f := range portFields {
385 fakeReservations[f] = reservePortResp{Port: i}
386 }
387 if err := setPortFields(values, fakeReservations); err != nil {
388 return ReleaseResources{}, err
389 }
gio0eaf2712024-04-14 13:08:46 +0400390 o := &installOptions{}
391 for _, i := range opts {
392 i(o)
393 }
gio3af43942024-04-16 08:13:50 +0400394 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400395 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400396 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400397 }
giof8843412024-05-22 16:38:05 +0400398 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400399 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400400 }
gio0eaf2712024-04-14 13:08:46 +0400401 var env EnvConfig
402 if o.Env != nil {
403 env = *o.Env
404 } else {
405 var err error
406 env, err = m.Config()
407 if err != nil {
408 return ReleaseResources{}, err
409 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400410 }
giocb34ad22024-07-11 08:01:13 +0400411 var networks []Network
412 if o.Networks != nil {
413 networks = o.Networks
414 } else {
415 var err error
416 networks, err = m.CreateNetworks(env)
417 if err != nil {
418 return ReleaseResources{}, err
419 }
420 }
giof8843412024-05-22 16:38:05 +0400421 var lg LocalChartGenerator
422 if o.LG != nil {
423 lg = o.LG
424 } else {
425 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
426 }
gio3cdee592024-04-17 10:15:56 +0400427 release := Release{
428 AppInstanceId: instanceId,
429 Namespace: namespace,
430 RepoAddr: m.repoIO.FullAddress(),
431 AppDir: appDir,
432 }
giocb34ad22024-07-11 08:01:13 +0400433 rendered, err := app.Render(release, env, networks, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400434 if err != nil {
gio778577f2024-04-29 09:44:38 +0400435 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400436 }
gioefa0ed42024-06-13 12:31:43 +0400437 reservators := map[string]string{}
438 allocators := map[string]string{}
439 for _, pf := range rendered.Ports {
440 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
441 allocators[portFields[pf.SourcePort]] = pf.Allocator
442 }
443 portReservations, err := reservePorts(reservators)
444 if err != nil {
445 return ReleaseResources{}, err
446 }
447 if err := setPortFields(values, portReservations); err != nil {
448 return ReleaseResources{}, err
449 }
giof8843412024-05-22 16:38:05 +0400450 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
451 if o.FetchContainerImages {
452 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
453 return ReleaseResources{}, err
454 }
gio0eaf2712024-04-14 13:08:46 +0400455 }
giof71a0832024-06-27 14:45:45 +0400456 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
457 if err != nil {
giof8843412024-05-22 16:38:05 +0400458 return ReleaseResources{}, err
459 }
giof71a0832024-06-27 14:45:45 +0400460 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400461 if o.FetchContainerImages {
462 release.ImageRegistry = imageRegistry
463 }
giocb34ad22024-07-11 08:01:13 +0400464 rendered, err = app.Render(release, env, networks, values, localCharts)
giof8843412024-05-22 16:38:05 +0400465 if err != nil {
466 return ReleaseResources{}, err
467 }
468 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 +0400469 return ReleaseResources{}, err
470 }
gioff2a29a2024-05-01 17:06:42 +0400471 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400472 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400473 return ReleaseResources{}, err
474 }
gio778577f2024-04-29 09:44:38 +0400475 return ReleaseResources{
476 Helm: extractHelm(rendered.Resources),
477 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400478}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400479
gio778577f2024-04-29 09:44:38 +0400480type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400481 Metadata struct {
482 Name string `json:"name"`
483 Namespace string `json:"namespace"`
484 Annotations map[string]string `json:"annotations"`
485 } `json:"metadata"`
486 Kind string `json:"kind"`
487 Status struct {
gio778577f2024-04-29 09:44:38 +0400488 Conditions []struct {
489 Type string `json:"type"`
490 Status string `json:"status"`
491 } `json:"conditions"`
492 } `json:"status,omitempty"`
493}
494
495func extractHelm(resources CueAppData) []Resource {
496 ret := make([]Resource, 0, len(resources))
497 for _, contents := range resources {
498 var h helmRelease
499 if err := yaml.Unmarshal(contents, &h); err != nil {
500 panic(err) // TODO(gio): handle
501 }
gio0eaf2712024-04-14 13:08:46 +0400502 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400503 res := Resource{
504 Name: h.Metadata.Name,
505 Namespace: h.Metadata.Namespace,
506 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
507 }
508 if h.Metadata.Annotations != nil {
509 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
510 if ok && len(info) != 0 {
511 res.Info = info
512 }
513 }
514 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400515 }
gio778577f2024-04-29 09:44:38 +0400516 }
517 return ret
518}
519
giof8843412024-05-22 16:38:05 +0400520// TODO(gio): take app configuration from the repo
521func (m *AppManager) Update(
522 instanceId string,
523 values map[string]any,
524 opts ...InstallOption,
525) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400526 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400527 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400528 }
gio3cdee592024-04-17 10:15:56 +0400529 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400530 if err != nil {
gio778577f2024-04-29 09:44:38 +0400531 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400532 }
gio308105e2024-04-19 13:12:13 +0400533 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400534 app, err := m.GetInstanceApp(instanceId)
535 if err != nil {
536 return ReleaseResources{}, err
537 }
gio308105e2024-04-19 13:12:13 +0400538 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400539 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400540 if err != nil {
gio778577f2024-04-29 09:44:38 +0400541 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400542 }
giocdfa3722024-06-13 20:10:14 +0400543 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400544 if err != nil {
545 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400546 }
giocb34ad22024-07-11 08:01:13 +0400547 networks, err := m.CreateNetworks(env)
548 if err != nil {
549 return ReleaseResources{}, err
550 }
551 rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400552 if err != nil {
gio778577f2024-04-29 09:44:38 +0400553 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400554 }
gio0eaf2712024-04-14 13:08:46 +0400555 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400556}
557
558func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400559 if err := m.repoIO.Pull(); err != nil {
560 return err
561 }
giocdfa3722024-06-13 20:10:14 +0400562 var portForward []PortForward
563 if err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
564 instanceDir := filepath.Join(m.appDirRoot, instanceId)
565 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
566 if err != nil {
567 return "", err
568 }
569 portForward = renderedCfg.PortForward
570 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400571 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400572 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400573 if err != nil {
574 return "", err
575 }
576 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400577 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400578 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400579 }); err != nil {
580 return err
581 }
582 if err := closePorts(portForward); err != nil {
giocdfa3722024-06-13 20:10:14 +0400583 return err
584 }
585 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400586}
587
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400588// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
giocb34ad22024-07-11 08:01:13 +0400589func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
590 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400591 {
giocdfa3722024-06-13 20:10:14 +0400592 Name: "Public",
593 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
594 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
595 Domain: env.Domain,
596 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
597 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
598 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400599 },
600 {
giocdfa3722024-06-13 20:10:14 +0400601 Name: "Private",
602 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
603 Domain: env.PrivateDomain,
604 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
605 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
606 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400607 },
608 }
giocb34ad22024-07-11 08:01:13 +0400609 n, err := m.FindAllAppInstances("network")
610 if err != nil {
611 return nil, err
612 }
613 for _, a := range n {
614 ret = append(ret, Network{
615 Name: a.Input["name"].(string),
616 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
617 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
618 Domain: a.Input["domain"].(string),
619 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
620 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
621 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
622 })
623 }
624 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400625}
gio3cdee592024-04-17 10:15:56 +0400626
gio0eaf2712024-04-14 13:08:46 +0400627type installOptions struct {
giof8843412024-05-22 16:38:05 +0400628 NoPublish bool
629 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400630 Networks []Network
giof8843412024-05-22 16:38:05 +0400631 Branch string
632 LG LocalChartGenerator
633 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400634 Force bool
gio9d66f322024-07-06 13:45:10 +0400635 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400636}
637
638type InstallOption func(*installOptions)
639
640func WithConfig(env *EnvConfig) InstallOption {
641 return func(o *installOptions) {
642 o.Env = env
643 }
644}
645
giocb34ad22024-07-11 08:01:13 +0400646func WithNetworks(networks []Network) InstallOption {
647 return func(o *installOptions) {
648 o.Networks = networks
649 }
650}
651
gio23bdc1b2024-07-11 16:07:47 +0400652func WithNoNetworks() InstallOption {
653 return WithNetworks([]Network{})
654}
655
gio0eaf2712024-04-14 13:08:46 +0400656func WithBranch(branch string) InstallOption {
657 return func(o *installOptions) {
658 o.Branch = branch
659 }
660}
661
giof71a0832024-06-27 14:45:45 +0400662func WithForce() InstallOption {
663 return func(o *installOptions) {
664 o.Force = true
665 }
666}
667
giof8843412024-05-22 16:38:05 +0400668func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
669 return func(o *installOptions) {
670 o.LG = lg
671 }
672}
673
674func WithFetchContainerImages() InstallOption {
675 return func(o *installOptions) {
676 o.FetchContainerImages = true
677 }
678}
679
680func WithNoPublish() InstallOption {
681 return func(o *installOptions) {
682 o.NoPublish = true
683 }
684}
685
gio9d66f322024-07-06 13:45:10 +0400686func WithNoLock() InstallOption {
687 return func(o *installOptions) {
688 o.NoLock = true
689 }
690}
691
giof8843412024-05-22 16:38:05 +0400692// InfraAppmanager
693
694type InfraAppManager struct {
695 repoIO soft.RepoIO
696 nsc NamespaceCreator
697 hf HelmFetcher
698 lg LocalChartGenerator
699}
700
701func NewInfraAppManager(
702 repoIO soft.RepoIO,
703 nsc NamespaceCreator,
704 hf HelmFetcher,
705 lg LocalChartGenerator,
706) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400707 return &InfraAppManager{
708 repoIO,
giof8843412024-05-22 16:38:05 +0400709 nsc,
710 hf,
711 lg,
gio3cdee592024-04-17 10:15:56 +0400712 }, nil
713}
714
715func (m *InfraAppManager) Config() (InfraConfig, error) {
716 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400717 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400718 return InfraConfig{}, err
719 } else {
720 return cfg, nil
721 }
722}
723
gioe72b54f2024-04-22 10:44:41 +0400724func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
725 var cfg InfraAppInstanceConfig
726 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
727 return InfraAppInstanceConfig{}, err
728 } else {
729 return cfg, nil
730 }
731}
732
733func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
734 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
735 if err != nil {
736 return InfraAppInstanceConfig{}, err
737 }
738 for _, app := range kust.Resources {
739 if app == id {
740 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
741 if err != nil {
742 return InfraAppInstanceConfig{}, err
743 }
744 cfg.Id = id
745 return cfg, nil
746 }
747 }
748 return InfraAppInstanceConfig{}, nil
749}
750
gio778577f2024-04-29 09:44:38 +0400751func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400752 appDir = filepath.Clean(appDir)
753 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400754 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400755 }
giof8843412024-05-22 16:38:05 +0400756 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400757 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400758 }
759 infra, err := m.Config()
760 if err != nil {
gio778577f2024-04-29 09:44:38 +0400761 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400762 }
763 release := Release{
764 Namespace: namespace,
765 RepoAddr: m.repoIO.FullAddress(),
766 AppDir: appDir,
767 }
giof8843412024-05-22 16:38:05 +0400768 rendered, err := app.Render(release, infra, values, nil)
769 if err != nil {
770 return ReleaseResources{}, err
771 }
giof71a0832024-06-27 14:45:45 +0400772 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
773 if err != nil {
giof8843412024-05-22 16:38:05 +0400774 return ReleaseResources{}, err
775 }
giof71a0832024-06-27 14:45:45 +0400776 localCharts := generateLocalCharts(m.lg, charts)
giof8843412024-05-22 16:38:05 +0400777 rendered, err = app.Render(release, infra, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400778 if err != nil {
gio778577f2024-04-29 09:44:38 +0400779 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400780 }
gio0eaf2712024-04-14 13:08:46 +0400781 return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
gioe72b54f2024-04-22 10:44:41 +0400782}
783
giof8843412024-05-22 16:38:05 +0400784// TODO(gio): take app configuration from the repo
785func (m *InfraAppManager) Update(
786 instanceId string,
787 values map[string]any,
788 opts ...InstallOption,
789) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400790 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400791 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400792 }
793 env, err := m.Config()
794 if err != nil {
gio778577f2024-04-29 09:44:38 +0400795 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400796 }
797 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400798 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
799 if err != nil {
800 return ReleaseResources{}, err
801 }
802 app, err := NewCueInfraApp(appCfg)
803 if err != nil {
804 return ReleaseResources{}, err
805 }
gioe72b54f2024-04-22 10:44:41 +0400806 instanceConfigPath := filepath.Join(instanceDir, "config.json")
807 config, err := m.appConfig(instanceConfigPath)
808 if err != nil {
gio778577f2024-04-29 09:44:38 +0400809 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400810 }
giocdfa3722024-06-13 20:10:14 +0400811 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400812 if err != nil {
813 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400814 }
giocdfa3722024-06-13 20:10:14 +0400815 rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400816 if err != nil {
gio778577f2024-04-29 09:44:38 +0400817 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400818 }
gio0eaf2712024-04-14 13:08:46 +0400819 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400820}
giof8843412024-05-22 16:38:05 +0400821
822func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
823 ret := make(map[string]string)
824 for name, chart := range charts.Git {
825 chartRoot := filepath.Join(root, name)
826 ret[name] = chartRoot
827 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
828 return nil, err
829 }
830 }
831 return ret, nil
832}
833
834func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
835 ret := make(map[string]helmv2.HelmChartTemplateSpec)
836 for name, path := range charts {
837 ret[name] = g.Generate(path)
838 }
839 return ret
840}
841
842func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
843 for _, img := range imgs {
844 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
845 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
846 "skopeo",
847 "--insecure-policy",
848 "copy",
849 "--dest-tls-verify=false", // TODO(gio): enable
850 "--multi-arch=all",
851 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
852 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
853 }); err != nil {
854 return err
855 }
856 }
857 return nil
858}
859
860type renderedInstance struct {
861 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400862 PortForward []PortForward `json:"portForward"`
giof8843412024-05-22 16:38:05 +0400863}
864
giocdfa3722024-06-13 20:10:14 +0400865func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400866 r, err := fs.Reader(path)
867 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400868 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400869 }
870 defer r.Close()
871 var cfg renderedInstance
872 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400873 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400874 }
giocdfa3722024-06-13 20:10:14 +0400875 return cfg, nil
giof8843412024-05-22 16:38:05 +0400876}
gioefa0ed42024-06-13 12:31:43 +0400877
878func findPortFields(scm Schema) []string {
879 switch scm.Kind() {
880 case KindBoolean:
881 return []string{}
882 case KindInt:
883 return []string{}
884 case KindString:
885 return []string{}
886 case KindStruct:
887 ret := []string{}
888 for _, f := range scm.Fields() {
889 for _, p := range findPortFields(f.Schema) {
890 if p == "" {
891 ret = append(ret, f.Name)
892 } else {
893 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
894 }
895 }
896 }
897 return ret
898 case KindNetwork:
899 return []string{}
gio4ece99c2024-07-18 11:05:50 +0400900 case KindMultiNetwork:
901 return []string{}
gioefa0ed42024-06-13 12:31:43 +0400902 case KindAuth:
903 return []string{}
904 case KindSSHKey:
905 return []string{}
906 case KindNumber:
907 return []string{}
908 case KindArrayString:
909 return []string{}
910 case KindPort:
911 return []string{""}
912 default:
913 panic("MUST NOT REACH!")
914 }
915}
916
917func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
918 for p, r := range ports {
919 if err := setPortField(values, p, r.Port); err != nil {
920 return err
921 }
922 }
923 return nil
924}
925
926func setPortField(values map[string]any, field string, port int) error {
927 f := strings.SplitN(field, ".", 2)
928 if len(f) == 2 {
929 var sub map[string]any
930 if s, ok := values[f[0]]; ok {
931 sub, ok = s.(map[string]any)
932 if !ok {
933 return fmt.Errorf("expected map")
934 }
935 } else {
936 sub = map[string]any{}
937 values[f[0]] = sub
938 }
939 if err := setPortField(sub, f[1], port); err != nil {
940 return err
941 }
942 } else {
943 values[f[0]] = port
944 }
945 return nil
946}