blob: 4f01d46ffbc0ea81b8e01325688b46d3d05951ac [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 {
90 return nil, err
91 }
92 ret := make([]AppInstanceConfig, 0)
93 for _, app := range kust.Resources {
94 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +040095 if err != nil {
96 return nil, err
97 }
98 cfg.Id = app
99 if cfg.AppId == name {
100 ret = append(ret, cfg)
101 }
102 }
103 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400104}
105
gio778577f2024-04-29 09:44:38 +0400106func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +0400107 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +0400108 if err != nil {
gio778577f2024-04-29 09:44:38 +0400109 return nil, err
gio3af43942024-04-16 08:13:50 +0400110 }
111 for _, app := range kust.Resources {
112 if app == id {
gio308105e2024-04-19 13:12:13 +0400113 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400114 if err != nil {
gio778577f2024-04-29 09:44:38 +0400115 return nil, err
gio3af43942024-04-16 08:13:50 +0400116 }
117 cfg.Id = id
gio778577f2024-04-29 09:44:38 +0400118 return &cfg, nil
gio3af43942024-04-16 08:13:50 +0400119 }
120 }
gio778577f2024-04-29 09:44:38 +0400121 return nil, ErrorNotFound
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400122}
123
giof8843412024-05-22 16:38:05 +0400124func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
125 files, err := fs.ListDir(dir)
126 if err != nil {
127 return nil, err
128 }
129 cfg := CueAppData{}
130 for _, f := range files {
131 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
132 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
133 if err != nil {
134 return nil, err
135 }
136 cfg[f.Name()] = contents
137 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400138 }
gio308105e2024-04-19 13:12:13 +0400139 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400140}
141
giof8843412024-05-22 16:38:05 +0400142func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
143 cfg, err := GetCueAppData(m.repoIO, filepath.Join(m.appDirRoot, id))
144 if err != nil {
145 return nil, err
146 }
147 return NewCueEnvApp(cfg)
148}
149
gio3af43942024-04-16 08:13:50 +0400150type allocatePortReq struct {
151 Protocol string `json:"protocol"`
152 SourcePort int `json:"sourcePort"`
153 TargetService string `json:"targetService"`
154 TargetPort int `json:"targetPort"`
gioefa0ed42024-06-13 12:31:43 +0400155 Secret string `json:"secret"`
gio3af43942024-04-16 08:13:50 +0400156}
157
gioefa0ed42024-06-13 12:31:43 +0400158type reservePortResp struct {
159 Port int `json:"port"`
160 Secret string `json:"secret"`
161}
162
163func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
164 ret := map[string]reservePortResp{}
165 for p, reserveAddr := range ports {
166 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
167 if err != nil {
168 return nil, err
169 }
170 if resp.StatusCode != http.StatusOK {
171 var e bytes.Buffer
172 io.Copy(&e, resp.Body)
173 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
174 }
175 var r reservePortResp
176 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
177 return nil, err
178 }
179 ret[p] = r
180 }
181 return ret, nil
182}
183
184func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400185 for _, p := range ports {
186 var buf bytes.Buffer
187 req := allocatePortReq{
188 Protocol: p.Protocol,
189 SourcePort: p.SourcePort,
190 TargetService: p.TargetService,
191 TargetPort: p.TargetPort,
192 }
gioefa0ed42024-06-13 12:31:43 +0400193 allocator := ""
194 for n, r := range reservations {
195 if p.SourcePort == r.Port {
196 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400197 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400198 break
199 }
200 }
201 if allocator == "" {
202 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
203 }
giobd7ab0b2024-06-17 12:55:17 +0400204 if err := json.NewEncoder(&buf).Encode(req); err != nil {
205 return err
206 }
gioefa0ed42024-06-13 12:31:43 +0400207 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400208 if err != nil {
209 return err
210 }
211 if resp.StatusCode != http.StatusOK {
212 return fmt.Errorf("Could not allocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
213 }
214 }
215 return nil
216}
217
gioe72b54f2024-04-22 10:44:41 +0400218func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400219 for p := filepath.Clean(path); p != "/"; {
220 parent, child := filepath.Split(p)
221 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400222 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400223 if err != nil {
224 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400225 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400226 kust = &k
227 } else {
228 return err
229 }
230 }
231 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400232 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400233 return err
234 }
235 p = filepath.Clean(parent)
236 }
237 return nil
238}
239
gio778577f2024-04-29 09:44:38 +0400240type Resource struct {
241 Name string `json:"name"`
242 Namespace string `json:"namespace"`
giof9f0bee2024-06-11 20:10:05 +0400243 Info string `json:"info"`
gio778577f2024-04-29 09:44:38 +0400244}
245
246type ReleaseResources struct {
247 Helm []Resource
248}
249
gio3cdee592024-04-17 10:15:56 +0400250// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400251func installApp(
gioe72b54f2024-04-22 10:44:41 +0400252 repo soft.RepoIO,
253 appDir string,
254 name string,
255 config any,
256 ports []PortForward,
257 resources CueAppData,
258 data CueAppData,
giof8843412024-05-22 16:38:05 +0400259 opts ...InstallOption,
gio778577f2024-04-29 09:44:38 +0400260) (ReleaseResources, error) {
giof8843412024-05-22 16:38:05 +0400261 var o installOptions
262 for _, i := range opts {
263 i(&o)
264 }
265 dopts := []soft.DoOption{}
266 if o.Branch != "" {
267 dopts = append(dopts, soft.WithForce())
268 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
269 }
270 if o.NoPublish {
271 dopts = append(dopts, soft.WithNoCommit())
272 }
gio778577f2024-04-29 09:44:38 +0400273 return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400274 if err := r.RemoveDir(appDir); err != nil {
275 return "", err
276 }
277 resourcesDir := path.Join(appDir, "resources")
278 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400279 return "", err
280 }
281 {
gioe72b54f2024-04-22 10:44:41 +0400282 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400283 return "", err
284 }
gioe72b54f2024-04-22 10:44:41 +0400285 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400286 return "", err
287 }
gioe72b54f2024-04-22 10:44:41 +0400288 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400289 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
290 return "", fmt.Errorf("%s is forbidden", name)
291 }
292 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400293 if err != nil {
294 return "", err
295 }
gio308105e2024-04-19 13:12:13 +0400296 defer w.Close()
297 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400298 return "", err
299 }
300 }
gio308105e2024-04-19 13:12:13 +0400301 }
302 {
303 if err := createKustomizationChain(r, resourcesDir); err != nil {
304 return "", err
305 }
gioefa0ed42024-06-13 12:31:43 +0400306 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400307 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400308 appKust.AddResources(name)
309 w, err := r.Writer(path.Join(resourcesDir, name))
310 if err != nil {
311 return "", err
312 }
313 defer w.Close()
314 if _, err := w.Write(contents); err != nil {
315 return "", err
316 }
317 }
gioe72b54f2024-04-22 10:44:41 +0400318 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400319 return "", err
320 }
321 }
gioe72b54f2024-04-22 10:44:41 +0400322 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400323 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400324}
325
gio3cdee592024-04-17 10:15:56 +0400326// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400327func (m *AppManager) Install(
328 app EnvApp,
329 instanceId string,
330 appDir string,
331 namespace string,
332 values map[string]any,
333 opts ...InstallOption,
334) (ReleaseResources, error) {
gioefa0ed42024-06-13 12:31:43 +0400335 portFields := findPortFields(app.Schema())
336 fakeReservations := map[string]reservePortResp{}
337 for i, f := range portFields {
338 fakeReservations[f] = reservePortResp{Port: i}
339 }
340 if err := setPortFields(values, fakeReservations); err != nil {
341 return ReleaseResources{}, err
342 }
gio0eaf2712024-04-14 13:08:46 +0400343 o := &installOptions{}
344 for _, i := range opts {
345 i(o)
346 }
gio3af43942024-04-16 08:13:50 +0400347 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400348 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400349 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400350 }
giof8843412024-05-22 16:38:05 +0400351 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400352 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400353 }
gio0eaf2712024-04-14 13:08:46 +0400354 var env EnvConfig
355 if o.Env != nil {
356 env = *o.Env
357 } else {
358 var err error
359 env, err = m.Config()
360 if err != nil {
361 return ReleaseResources{}, err
362 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400363 }
giof8843412024-05-22 16:38:05 +0400364 var lg LocalChartGenerator
365 if o.LG != nil {
366 lg = o.LG
367 } else {
368 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
369 }
gio3cdee592024-04-17 10:15:56 +0400370 release := Release{
371 AppInstanceId: instanceId,
372 Namespace: namespace,
373 RepoAddr: m.repoIO.FullAddress(),
374 AppDir: appDir,
375 }
giof8843412024-05-22 16:38:05 +0400376 rendered, err := app.Render(release, env, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400377 if err != nil {
gio778577f2024-04-29 09:44:38 +0400378 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400379 }
gioefa0ed42024-06-13 12:31:43 +0400380 reservators := map[string]string{}
381 allocators := map[string]string{}
382 for _, pf := range rendered.Ports {
383 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
384 allocators[portFields[pf.SourcePort]] = pf.Allocator
385 }
386 portReservations, err := reservePorts(reservators)
387 if err != nil {
388 return ReleaseResources{}, err
389 }
390 if err := setPortFields(values, portReservations); err != nil {
391 return ReleaseResources{}, err
392 }
giof8843412024-05-22 16:38:05 +0400393 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
394 if o.FetchContainerImages {
395 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
396 return ReleaseResources{}, err
397 }
gio0eaf2712024-04-14 13:08:46 +0400398 }
giof8843412024-05-22 16:38:05 +0400399 var localCharts map[string]helmv2.HelmChartTemplateSpec
400 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
401 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
402 if err != nil {
403 return "", err
404 }
405 localCharts = generateLocalCharts(lg, charts)
406 return "pull helm charts", nil
407 }); err != nil {
408 return ReleaseResources{}, err
409 }
410 if o.FetchContainerImages {
411 release.ImageRegistry = imageRegistry
412 }
413 rendered, err = app.Render(release, env, values, localCharts)
414 if err != nil {
415 return ReleaseResources{}, err
416 }
417 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 +0400418 return ReleaseResources{}, err
419 }
gioff2a29a2024-05-01 17:06:42 +0400420 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400421 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400422 return ReleaseResources{}, err
423 }
gio778577f2024-04-29 09:44:38 +0400424 return ReleaseResources{
425 Helm: extractHelm(rendered.Resources),
426 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400427}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400428
gio778577f2024-04-29 09:44:38 +0400429type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400430 Metadata struct {
431 Name string `json:"name"`
432 Namespace string `json:"namespace"`
433 Annotations map[string]string `json:"annotations"`
434 } `json:"metadata"`
435 Kind string `json:"kind"`
436 Status struct {
gio778577f2024-04-29 09:44:38 +0400437 Conditions []struct {
438 Type string `json:"type"`
439 Status string `json:"status"`
440 } `json:"conditions"`
441 } `json:"status,omitempty"`
442}
443
444func extractHelm(resources CueAppData) []Resource {
445 ret := make([]Resource, 0, len(resources))
446 for _, contents := range resources {
447 var h helmRelease
448 if err := yaml.Unmarshal(contents, &h); err != nil {
449 panic(err) // TODO(gio): handle
450 }
gio0eaf2712024-04-14 13:08:46 +0400451 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400452 res := Resource{
453 Name: h.Metadata.Name,
454 Namespace: h.Metadata.Namespace,
455 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
456 }
457 if h.Metadata.Annotations != nil {
458 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
459 if ok && len(info) != 0 {
460 res.Info = info
461 }
462 }
463 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400464 }
gio778577f2024-04-29 09:44:38 +0400465 }
466 return ret
467}
468
giof8843412024-05-22 16:38:05 +0400469// TODO(gio): take app configuration from the repo
470func (m *AppManager) Update(
471 instanceId string,
472 values map[string]any,
473 opts ...InstallOption,
474) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400475 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400476 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400477 }
gio3cdee592024-04-17 10:15:56 +0400478 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400479 if err != nil {
gio778577f2024-04-29 09:44:38 +0400480 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400481 }
gio308105e2024-04-19 13:12:13 +0400482 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400483 app, err := m.GetInstanceApp(instanceId)
484 if err != nil {
485 return ReleaseResources{}, err
486 }
gio308105e2024-04-19 13:12:13 +0400487 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400488 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400489 if err != nil {
gio778577f2024-04-29 09:44:38 +0400490 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400491 }
giof8843412024-05-22 16:38:05 +0400492 localCharts, err := extractLocalCharts(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
493 if err != nil {
494 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400495 }
giof8843412024-05-22 16:38:05 +0400496 rendered, err := app.Render(config.Release, env, values, localCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400497 if err != nil {
gio778577f2024-04-29 09:44:38 +0400498 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400499 }
gio0eaf2712024-04-14 13:08:46 +0400500 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400501}
502
503func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400504 if err := m.repoIO.Pull(); err != nil {
505 return err
506 }
gioe72b54f2024-04-22 10:44:41 +0400507 return m.repoIO.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400508 r.RemoveDir(filepath.Join(m.appDirRoot, instanceId))
509 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400510 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400511 if err != nil {
512 return "", err
513 }
514 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400515 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400516 return fmt.Sprintf("uninstall: %s", instanceId), nil
517 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400518}
519
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400520// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gioe72b54f2024-04-22 10:44:41 +0400521func CreateNetworks(env EnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400522 return []Network{
523 {
524 Name: "Public",
gio3cdee592024-04-17 10:15:56 +0400525 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
526 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
527 Domain: env.Domain,
528 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
gioefa0ed42024-06-13 12:31:43 +0400529 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400530 },
531 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400532 Name: "Private",
gio3cdee592024-04-17 10:15:56 +0400533 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
534 Domain: env.PrivateDomain,
535 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
gioefa0ed42024-06-13 12:31:43 +0400536 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400537 },
538 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400539}
gio3cdee592024-04-17 10:15:56 +0400540
gio0eaf2712024-04-14 13:08:46 +0400541type installOptions struct {
giof8843412024-05-22 16:38:05 +0400542 NoPublish bool
543 Env *EnvConfig
544 Branch string
545 LG LocalChartGenerator
546 FetchContainerImages bool
gio0eaf2712024-04-14 13:08:46 +0400547}
548
549type InstallOption func(*installOptions)
550
551func WithConfig(env *EnvConfig) InstallOption {
552 return func(o *installOptions) {
553 o.Env = env
554 }
555}
556
557func WithBranch(branch string) InstallOption {
558 return func(o *installOptions) {
559 o.Branch = branch
560 }
561}
562
giof8843412024-05-22 16:38:05 +0400563func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
564 return func(o *installOptions) {
565 o.LG = lg
566 }
567}
568
569func WithFetchContainerImages() InstallOption {
570 return func(o *installOptions) {
571 o.FetchContainerImages = true
572 }
573}
574
575func WithNoPublish() InstallOption {
576 return func(o *installOptions) {
577 o.NoPublish = true
578 }
579}
580
581// InfraAppmanager
582
583type InfraAppManager struct {
584 repoIO soft.RepoIO
585 nsc NamespaceCreator
586 hf HelmFetcher
587 lg LocalChartGenerator
588}
589
590func NewInfraAppManager(
591 repoIO soft.RepoIO,
592 nsc NamespaceCreator,
593 hf HelmFetcher,
594 lg LocalChartGenerator,
595) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400596 return &InfraAppManager{
597 repoIO,
giof8843412024-05-22 16:38:05 +0400598 nsc,
599 hf,
600 lg,
gio3cdee592024-04-17 10:15:56 +0400601 }, nil
602}
603
604func (m *InfraAppManager) Config() (InfraConfig, error) {
605 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400606 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400607 return InfraConfig{}, err
608 } else {
609 return cfg, nil
610 }
611}
612
gioe72b54f2024-04-22 10:44:41 +0400613func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
614 var cfg InfraAppInstanceConfig
615 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
616 return InfraAppInstanceConfig{}, err
617 } else {
618 return cfg, nil
619 }
620}
621
622func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
623 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
624 if err != nil {
625 return InfraAppInstanceConfig{}, err
626 }
627 for _, app := range kust.Resources {
628 if app == id {
629 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
630 if err != nil {
631 return InfraAppInstanceConfig{}, err
632 }
633 cfg.Id = id
634 return cfg, nil
635 }
636 }
637 return InfraAppInstanceConfig{}, nil
638}
639
gio778577f2024-04-29 09:44:38 +0400640func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400641 appDir = filepath.Clean(appDir)
642 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400643 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400644 }
giof8843412024-05-22 16:38:05 +0400645 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400646 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400647 }
648 infra, err := m.Config()
649 if err != nil {
gio778577f2024-04-29 09:44:38 +0400650 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400651 }
652 release := Release{
653 Namespace: namespace,
654 RepoAddr: m.repoIO.FullAddress(),
655 AppDir: appDir,
656 }
giof8843412024-05-22 16:38:05 +0400657 rendered, err := app.Render(release, infra, values, nil)
658 if err != nil {
659 return ReleaseResources{}, err
660 }
661 var localCharts map[string]helmv2.HelmChartTemplateSpec
662 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
663 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
664 if err != nil {
665 return "", err
666 }
667 localCharts = generateLocalCharts(m.lg, charts)
668 return "pull helm charts", nil
669 }); err != nil {
670 return ReleaseResources{}, err
671 }
672 rendered, err = app.Render(release, infra, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400673 if err != nil {
gio778577f2024-04-29 09:44:38 +0400674 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400675 }
gio0eaf2712024-04-14 13:08:46 +0400676 return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
gioe72b54f2024-04-22 10:44:41 +0400677}
678
giof8843412024-05-22 16:38:05 +0400679// TODO(gio): take app configuration from the repo
680func (m *InfraAppManager) Update(
681 instanceId string,
682 values map[string]any,
683 opts ...InstallOption,
684) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400685 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400686 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400687 }
688 env, err := m.Config()
689 if err != nil {
gio778577f2024-04-29 09:44:38 +0400690 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400691 }
692 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400693 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
694 if err != nil {
695 return ReleaseResources{}, err
696 }
697 app, err := NewCueInfraApp(appCfg)
698 if err != nil {
699 return ReleaseResources{}, err
700 }
gioe72b54f2024-04-22 10:44:41 +0400701 instanceConfigPath := filepath.Join(instanceDir, "config.json")
702 config, err := m.appConfig(instanceConfigPath)
703 if err != nil {
gio778577f2024-04-29 09:44:38 +0400704 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400705 }
giof8843412024-05-22 16:38:05 +0400706 localCharts, err := extractLocalCharts(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
707 if err != nil {
708 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400709 }
giof8843412024-05-22 16:38:05 +0400710 rendered, err := app.Render(config.Release, env, values, localCharts)
gioe72b54f2024-04-22 10:44:41 +0400711 if err != nil {
gio778577f2024-04-29 09:44:38 +0400712 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400713 }
gio0eaf2712024-04-14 13:08:46 +0400714 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400715}
giof8843412024-05-22 16:38:05 +0400716
717func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
718 ret := make(map[string]string)
719 for name, chart := range charts.Git {
720 chartRoot := filepath.Join(root, name)
721 ret[name] = chartRoot
722 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
723 return nil, err
724 }
725 }
726 return ret, nil
727}
728
729func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
730 ret := make(map[string]helmv2.HelmChartTemplateSpec)
731 for name, path := range charts {
732 ret[name] = g.Generate(path)
733 }
734 return ret
735}
736
737func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
738 for _, img := range imgs {
739 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
740 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
741 "skopeo",
742 "--insecure-policy",
743 "copy",
744 "--dest-tls-verify=false", // TODO(gio): enable
745 "--multi-arch=all",
746 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
747 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
748 }); err != nil {
749 return err
750 }
751 }
752 return nil
753}
754
755type renderedInstance struct {
756 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
757}
758
759func extractLocalCharts(fs soft.RepoFS, path string) (map[string]helmv2.HelmChartTemplateSpec, error) {
760 r, err := fs.Reader(path)
761 if err != nil {
762 return nil, err
763 }
764 defer r.Close()
765 var cfg renderedInstance
766 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
767 return nil, err
768 }
769 return cfg.LocalCharts, nil
770}
gioefa0ed42024-06-13 12:31:43 +0400771
772func findPortFields(scm Schema) []string {
773 switch scm.Kind() {
774 case KindBoolean:
775 return []string{}
776 case KindInt:
777 return []string{}
778 case KindString:
779 return []string{}
780 case KindStruct:
781 ret := []string{}
782 for _, f := range scm.Fields() {
783 for _, p := range findPortFields(f.Schema) {
784 if p == "" {
785 ret = append(ret, f.Name)
786 } else {
787 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
788 }
789 }
790 }
791 return ret
792 case KindNetwork:
793 return []string{}
794 case KindAuth:
795 return []string{}
796 case KindSSHKey:
797 return []string{}
798 case KindNumber:
799 return []string{}
800 case KindArrayString:
801 return []string{}
802 case KindPort:
803 return []string{""}
804 default:
805 panic("MUST NOT REACH!")
806 }
807}
808
809func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
810 for p, r := range ports {
811 if err := setPortField(values, p, r.Port); err != nil {
812 return err
813 }
814 }
815 return nil
816}
817
818func setPortField(values map[string]any, field string, port int) error {
819 f := strings.SplitN(field, ".", 2)
820 if len(f) == 2 {
821 var sub map[string]any
822 if s, ok := values[f[0]]; ok {
823 sub, ok = s.(map[string]any)
824 if !ok {
825 return fmt.Errorf("expected map")
826 }
827 } else {
828 sub = map[string]any{}
829 values[f[0]] = sub
830 }
831 if err := setPortField(sub, f[1], port); err != nil {
832 return err
833 }
834 } else {
835 values[f[0]] = port
836 }
837 return nil
838}