blob: cad18eb54a99840df130f64f2c87a0f97e04a40c [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"`
giocdfa3722024-06-13 20:10:14 +0400155 Secret string `json:"secret,omitempty"`
156}
157
158type removePortReq struct {
159 Protocol string `json:"protocol"`
160 SourcePort int `json:"sourcePort"`
161 TargetService string `json:"targetService"`
162 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400163}
164
gioefa0ed42024-06-13 12:31:43 +0400165type reservePortResp struct {
166 Port int `json:"port"`
167 Secret string `json:"secret"`
168}
169
170func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
171 ret := map[string]reservePortResp{}
172 for p, reserveAddr := range ports {
173 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
174 if err != nil {
175 return nil, err
176 }
177 if resp.StatusCode != http.StatusOK {
178 var e bytes.Buffer
179 io.Copy(&e, resp.Body)
180 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
181 }
182 var r reservePortResp
183 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
184 return nil, err
185 }
186 ret[p] = r
187 }
188 return ret, nil
189}
190
191func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400192 for _, p := range ports {
193 var buf bytes.Buffer
194 req := allocatePortReq{
195 Protocol: p.Protocol,
196 SourcePort: p.SourcePort,
197 TargetService: p.TargetService,
198 TargetPort: p.TargetPort,
199 }
gioefa0ed42024-06-13 12:31:43 +0400200 allocator := ""
201 for n, r := range reservations {
202 if p.SourcePort == r.Port {
203 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400204 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400205 break
206 }
207 }
208 if allocator == "" {
209 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
210 }
giobd7ab0b2024-06-17 12:55:17 +0400211 if err := json.NewEncoder(&buf).Encode(req); err != nil {
212 return err
213 }
gioefa0ed42024-06-13 12:31:43 +0400214 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400215 if err != nil {
216 return err
217 }
218 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400219 var r bytes.Buffer
220 io.Copy(&r, resp.Body)
221 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 +0400222 }
223 }
224 return nil
225}
226
giocdfa3722024-06-13 20:10:14 +0400227func closePorts(ports []PortForward) error {
228 var retErr error
229 for _, p := range ports {
230 var buf bytes.Buffer
231 req := removePortReq{
232 Protocol: p.Protocol,
233 SourcePort: p.SourcePort,
234 TargetService: p.TargetService,
235 TargetPort: p.TargetPort,
236 }
237 if err := json.NewEncoder(&buf).Encode(req); err != nil {
238 retErr = err
239 continue
240 }
241 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
242 if err != nil {
243 retErr = err
244 continue
245 }
246 if resp.StatusCode != http.StatusOK {
247 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
248 continue
249 }
250 }
251 return retErr
252}
253
gioe72b54f2024-04-22 10:44:41 +0400254func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400255 for p := filepath.Clean(path); p != "/"; {
256 parent, child := filepath.Split(p)
257 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400258 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400259 if err != nil {
260 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400261 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400262 kust = &k
263 } else {
264 return err
265 }
266 }
267 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400268 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400269 return err
270 }
271 p = filepath.Clean(parent)
272 }
273 return nil
274}
275
gio778577f2024-04-29 09:44:38 +0400276type Resource struct {
277 Name string `json:"name"`
278 Namespace string `json:"namespace"`
giof9f0bee2024-06-11 20:10:05 +0400279 Info string `json:"info"`
gio778577f2024-04-29 09:44:38 +0400280}
281
282type ReleaseResources struct {
283 Helm []Resource
284}
285
gio3cdee592024-04-17 10:15:56 +0400286// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400287func installApp(
gioe72b54f2024-04-22 10:44:41 +0400288 repo soft.RepoIO,
289 appDir string,
290 name string,
291 config any,
292 ports []PortForward,
293 resources CueAppData,
294 data CueAppData,
giof8843412024-05-22 16:38:05 +0400295 opts ...InstallOption,
gio778577f2024-04-29 09:44:38 +0400296) (ReleaseResources, error) {
giof8843412024-05-22 16:38:05 +0400297 var o installOptions
298 for _, i := range opts {
299 i(&o)
300 }
301 dopts := []soft.DoOption{}
302 if o.Branch != "" {
303 dopts = append(dopts, soft.WithForce())
304 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
305 }
306 if o.NoPublish {
307 dopts = append(dopts, soft.WithNoCommit())
308 }
gio778577f2024-04-29 09:44:38 +0400309 return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400310 if err := r.RemoveDir(appDir); err != nil {
311 return "", err
312 }
313 resourcesDir := path.Join(appDir, "resources")
314 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400315 return "", err
316 }
317 {
gioe72b54f2024-04-22 10:44:41 +0400318 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400319 return "", err
320 }
gioe72b54f2024-04-22 10:44:41 +0400321 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400322 return "", err
323 }
gioe72b54f2024-04-22 10:44:41 +0400324 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400325 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
326 return "", fmt.Errorf("%s is forbidden", name)
327 }
328 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400329 if err != nil {
330 return "", err
331 }
gio308105e2024-04-19 13:12:13 +0400332 defer w.Close()
333 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400334 return "", err
335 }
336 }
gio308105e2024-04-19 13:12:13 +0400337 }
338 {
339 if err := createKustomizationChain(r, resourcesDir); err != nil {
340 return "", err
341 }
gioefa0ed42024-06-13 12:31:43 +0400342 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400343 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400344 appKust.AddResources(name)
345 w, err := r.Writer(path.Join(resourcesDir, name))
346 if err != nil {
347 return "", err
348 }
349 defer w.Close()
350 if _, err := w.Write(contents); err != nil {
351 return "", err
352 }
353 }
gioe72b54f2024-04-22 10:44:41 +0400354 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400355 return "", err
356 }
357 }
gioe72b54f2024-04-22 10:44:41 +0400358 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400359 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400360}
361
gio3cdee592024-04-17 10:15:56 +0400362// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400363func (m *AppManager) Install(
364 app EnvApp,
365 instanceId string,
366 appDir string,
367 namespace string,
368 values map[string]any,
369 opts ...InstallOption,
370) (ReleaseResources, error) {
gioefa0ed42024-06-13 12:31:43 +0400371 portFields := findPortFields(app.Schema())
372 fakeReservations := map[string]reservePortResp{}
373 for i, f := range portFields {
374 fakeReservations[f] = reservePortResp{Port: i}
375 }
376 if err := setPortFields(values, fakeReservations); err != nil {
377 return ReleaseResources{}, err
378 }
gio0eaf2712024-04-14 13:08:46 +0400379 o := &installOptions{}
380 for _, i := range opts {
381 i(o)
382 }
gio3af43942024-04-16 08:13:50 +0400383 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400384 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400385 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400386 }
giof8843412024-05-22 16:38:05 +0400387 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400388 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400389 }
gio0eaf2712024-04-14 13:08:46 +0400390 var env EnvConfig
391 if o.Env != nil {
392 env = *o.Env
393 } else {
394 var err error
395 env, err = m.Config()
396 if err != nil {
397 return ReleaseResources{}, err
398 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400399 }
giof8843412024-05-22 16:38:05 +0400400 var lg LocalChartGenerator
401 if o.LG != nil {
402 lg = o.LG
403 } else {
404 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
405 }
gio3cdee592024-04-17 10:15:56 +0400406 release := Release{
407 AppInstanceId: instanceId,
408 Namespace: namespace,
409 RepoAddr: m.repoIO.FullAddress(),
410 AppDir: appDir,
411 }
giof8843412024-05-22 16:38:05 +0400412 rendered, err := app.Render(release, env, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400413 if err != nil {
gio778577f2024-04-29 09:44:38 +0400414 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400415 }
gioefa0ed42024-06-13 12:31:43 +0400416 reservators := map[string]string{}
417 allocators := map[string]string{}
418 for _, pf := range rendered.Ports {
419 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
420 allocators[portFields[pf.SourcePort]] = pf.Allocator
421 }
422 portReservations, err := reservePorts(reservators)
423 if err != nil {
424 return ReleaseResources{}, err
425 }
426 if err := setPortFields(values, portReservations); err != nil {
427 return ReleaseResources{}, err
428 }
giof8843412024-05-22 16:38:05 +0400429 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
430 if o.FetchContainerImages {
431 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
432 return ReleaseResources{}, err
433 }
gio0eaf2712024-04-14 13:08:46 +0400434 }
giof8843412024-05-22 16:38:05 +0400435 var localCharts map[string]helmv2.HelmChartTemplateSpec
436 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
437 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
438 if err != nil {
439 return "", err
440 }
441 localCharts = generateLocalCharts(lg, charts)
442 return "pull helm charts", nil
443 }); err != nil {
444 return ReleaseResources{}, err
445 }
446 if o.FetchContainerImages {
447 release.ImageRegistry = imageRegistry
448 }
449 rendered, err = app.Render(release, env, values, localCharts)
450 if err != nil {
451 return ReleaseResources{}, err
452 }
453 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 +0400454 return ReleaseResources{}, err
455 }
gioff2a29a2024-05-01 17:06:42 +0400456 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400457 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
giocdfa3722024-06-13 20:10:14 +0400458 fmt.Println(err)
gioff2a29a2024-05-01 17:06:42 +0400459 return ReleaseResources{}, err
460 }
gio778577f2024-04-29 09:44:38 +0400461 return ReleaseResources{
462 Helm: extractHelm(rendered.Resources),
463 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400464}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400465
gio778577f2024-04-29 09:44:38 +0400466type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400467 Metadata struct {
468 Name string `json:"name"`
469 Namespace string `json:"namespace"`
470 Annotations map[string]string `json:"annotations"`
471 } `json:"metadata"`
472 Kind string `json:"kind"`
473 Status struct {
gio778577f2024-04-29 09:44:38 +0400474 Conditions []struct {
475 Type string `json:"type"`
476 Status string `json:"status"`
477 } `json:"conditions"`
478 } `json:"status,omitempty"`
479}
480
481func extractHelm(resources CueAppData) []Resource {
482 ret := make([]Resource, 0, len(resources))
483 for _, contents := range resources {
484 var h helmRelease
485 if err := yaml.Unmarshal(contents, &h); err != nil {
486 panic(err) // TODO(gio): handle
487 }
gio0eaf2712024-04-14 13:08:46 +0400488 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400489 res := Resource{
490 Name: h.Metadata.Name,
491 Namespace: h.Metadata.Namespace,
492 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
493 }
494 if h.Metadata.Annotations != nil {
495 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
496 if ok && len(info) != 0 {
497 res.Info = info
498 }
499 }
500 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400501 }
gio778577f2024-04-29 09:44:38 +0400502 }
503 return ret
504}
505
giof8843412024-05-22 16:38:05 +0400506// TODO(gio): take app configuration from the repo
507func (m *AppManager) Update(
508 instanceId string,
509 values map[string]any,
510 opts ...InstallOption,
511) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400512 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400513 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400514 }
gio3cdee592024-04-17 10:15:56 +0400515 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400516 if err != nil {
gio778577f2024-04-29 09:44:38 +0400517 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400518 }
gio308105e2024-04-19 13:12:13 +0400519 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400520 app, err := m.GetInstanceApp(instanceId)
521 if err != nil {
522 return ReleaseResources{}, err
523 }
gio308105e2024-04-19 13:12:13 +0400524 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400525 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400526 if err != nil {
gio778577f2024-04-29 09:44:38 +0400527 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400528 }
giocdfa3722024-06-13 20:10:14 +0400529 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400530 if err != nil {
531 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400532 }
giocdfa3722024-06-13 20:10:14 +0400533 rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400534 if err != nil {
gio778577f2024-04-29 09:44:38 +0400535 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400536 }
gio0eaf2712024-04-14 13:08:46 +0400537 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400538}
539
540func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400541 if err := m.repoIO.Pull(); err != nil {
542 return err
543 }
giocdfa3722024-06-13 20:10:14 +0400544 var portForward []PortForward
545 if err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
546 instanceDir := filepath.Join(m.appDirRoot, instanceId)
547 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
548 if err != nil {
549 return "", err
550 }
551 portForward = renderedCfg.PortForward
552 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400553 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400554 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400555 if err != nil {
556 return "", err
557 }
558 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400559 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400560 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400561 }); err != nil {
562 return err
563 }
564 if err := closePorts(portForward); err != nil {
565 fmt.Println(err)
566 return err
567 }
568 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400569}
570
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400571// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gioe72b54f2024-04-22 10:44:41 +0400572func CreateNetworks(env EnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400573 return []Network{
574 {
giocdfa3722024-06-13 20:10:14 +0400575 Name: "Public",
576 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
577 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
578 Domain: env.Domain,
579 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
580 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
581 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400582 },
583 {
giocdfa3722024-06-13 20:10:14 +0400584 Name: "Private",
585 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
586 Domain: env.PrivateDomain,
587 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
588 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
589 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400590 },
591 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400592}
gio3cdee592024-04-17 10:15:56 +0400593
gio0eaf2712024-04-14 13:08:46 +0400594type installOptions struct {
giof8843412024-05-22 16:38:05 +0400595 NoPublish bool
596 Env *EnvConfig
597 Branch string
598 LG LocalChartGenerator
599 FetchContainerImages bool
gio0eaf2712024-04-14 13:08:46 +0400600}
601
602type InstallOption func(*installOptions)
603
604func WithConfig(env *EnvConfig) InstallOption {
605 return func(o *installOptions) {
606 o.Env = env
607 }
608}
609
610func WithBranch(branch string) InstallOption {
611 return func(o *installOptions) {
612 o.Branch = branch
613 }
614}
615
giof8843412024-05-22 16:38:05 +0400616func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
617 return func(o *installOptions) {
618 o.LG = lg
619 }
620}
621
622func WithFetchContainerImages() InstallOption {
623 return func(o *installOptions) {
624 o.FetchContainerImages = true
625 }
626}
627
628func WithNoPublish() InstallOption {
629 return func(o *installOptions) {
630 o.NoPublish = true
631 }
632}
633
634// InfraAppmanager
635
636type InfraAppManager struct {
637 repoIO soft.RepoIO
638 nsc NamespaceCreator
639 hf HelmFetcher
640 lg LocalChartGenerator
641}
642
643func NewInfraAppManager(
644 repoIO soft.RepoIO,
645 nsc NamespaceCreator,
646 hf HelmFetcher,
647 lg LocalChartGenerator,
648) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400649 return &InfraAppManager{
650 repoIO,
giof8843412024-05-22 16:38:05 +0400651 nsc,
652 hf,
653 lg,
gio3cdee592024-04-17 10:15:56 +0400654 }, nil
655}
656
657func (m *InfraAppManager) Config() (InfraConfig, error) {
658 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400659 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400660 return InfraConfig{}, err
661 } else {
662 return cfg, nil
663 }
664}
665
gioe72b54f2024-04-22 10:44:41 +0400666func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
667 var cfg InfraAppInstanceConfig
668 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
669 return InfraAppInstanceConfig{}, err
670 } else {
671 return cfg, nil
672 }
673}
674
675func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
676 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
677 if err != nil {
678 return InfraAppInstanceConfig{}, err
679 }
680 for _, app := range kust.Resources {
681 if app == id {
682 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
683 if err != nil {
684 return InfraAppInstanceConfig{}, err
685 }
686 cfg.Id = id
687 return cfg, nil
688 }
689 }
690 return InfraAppInstanceConfig{}, nil
691}
692
gio778577f2024-04-29 09:44:38 +0400693func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400694 appDir = filepath.Clean(appDir)
695 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400696 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400697 }
giof8843412024-05-22 16:38:05 +0400698 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400699 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400700 }
701 infra, err := m.Config()
702 if err != nil {
gio778577f2024-04-29 09:44:38 +0400703 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400704 }
705 release := Release{
706 Namespace: namespace,
707 RepoAddr: m.repoIO.FullAddress(),
708 AppDir: appDir,
709 }
giof8843412024-05-22 16:38:05 +0400710 rendered, err := app.Render(release, infra, values, nil)
711 if err != nil {
712 return ReleaseResources{}, err
713 }
714 var localCharts map[string]helmv2.HelmChartTemplateSpec
715 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
716 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
717 if err != nil {
718 return "", err
719 }
720 localCharts = generateLocalCharts(m.lg, charts)
721 return "pull helm charts", nil
722 }); err != nil {
723 return ReleaseResources{}, err
724 }
725 rendered, err = app.Render(release, infra, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400726 if err != nil {
gio778577f2024-04-29 09:44:38 +0400727 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400728 }
gio0eaf2712024-04-14 13:08:46 +0400729 return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
gioe72b54f2024-04-22 10:44:41 +0400730}
731
giof8843412024-05-22 16:38:05 +0400732// TODO(gio): take app configuration from the repo
733func (m *InfraAppManager) Update(
734 instanceId string,
735 values map[string]any,
736 opts ...InstallOption,
737) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400738 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400739 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400740 }
741 env, err := m.Config()
742 if err != nil {
gio778577f2024-04-29 09:44:38 +0400743 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400744 }
745 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400746 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
747 if err != nil {
748 return ReleaseResources{}, err
749 }
750 app, err := NewCueInfraApp(appCfg)
751 if err != nil {
752 return ReleaseResources{}, err
753 }
gioe72b54f2024-04-22 10:44:41 +0400754 instanceConfigPath := filepath.Join(instanceDir, "config.json")
755 config, err := m.appConfig(instanceConfigPath)
756 if err != nil {
gio778577f2024-04-29 09:44:38 +0400757 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400758 }
giocdfa3722024-06-13 20:10:14 +0400759 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400760 if err != nil {
761 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400762 }
giocdfa3722024-06-13 20:10:14 +0400763 rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400764 if err != nil {
gio778577f2024-04-29 09:44:38 +0400765 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400766 }
gio0eaf2712024-04-14 13:08:46 +0400767 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400768}
giof8843412024-05-22 16:38:05 +0400769
770func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
771 ret := make(map[string]string)
772 for name, chart := range charts.Git {
773 chartRoot := filepath.Join(root, name)
774 ret[name] = chartRoot
775 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
776 return nil, err
777 }
778 }
779 return ret, nil
780}
781
782func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
783 ret := make(map[string]helmv2.HelmChartTemplateSpec)
784 for name, path := range charts {
785 ret[name] = g.Generate(path)
786 }
787 return ret
788}
789
790func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
791 for _, img := range imgs {
792 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
793 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
794 "skopeo",
795 "--insecure-policy",
796 "copy",
797 "--dest-tls-verify=false", // TODO(gio): enable
798 "--multi-arch=all",
799 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
800 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
801 }); err != nil {
802 return err
803 }
804 }
805 return nil
806}
807
808type renderedInstance struct {
809 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400810 PortForward []PortForward `json:"portForward"`
giof8843412024-05-22 16:38:05 +0400811}
812
giocdfa3722024-06-13 20:10:14 +0400813func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400814 r, err := fs.Reader(path)
815 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400816 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400817 }
818 defer r.Close()
819 var cfg renderedInstance
820 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400821 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400822 }
giocdfa3722024-06-13 20:10:14 +0400823 return cfg, nil
giof8843412024-05-22 16:38:05 +0400824}
gioefa0ed42024-06-13 12:31:43 +0400825
826func findPortFields(scm Schema) []string {
827 switch scm.Kind() {
828 case KindBoolean:
829 return []string{}
830 case KindInt:
831 return []string{}
832 case KindString:
833 return []string{}
834 case KindStruct:
835 ret := []string{}
836 for _, f := range scm.Fields() {
837 for _, p := range findPortFields(f.Schema) {
838 if p == "" {
839 ret = append(ret, f.Name)
840 } else {
841 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
842 }
843 }
844 }
845 return ret
846 case KindNetwork:
847 return []string{}
848 case KindAuth:
849 return []string{}
850 case KindSSHKey:
851 return []string{}
852 case KindNumber:
853 return []string{}
854 case KindArrayString:
855 return []string{}
856 case KindPort:
857 return []string{""}
858 default:
859 panic("MUST NOT REACH!")
860 }
861}
862
863func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
864 for p, r := range ports {
865 if err := setPortField(values, p, r.Port); err != nil {
866 return err
867 }
868 }
869 return nil
870}
871
872func setPortField(values map[string]any, field string, port int) error {
873 f := strings.SplitN(field, ".", 2)
874 if len(f) == 2 {
875 var sub map[string]any
876 if s, ok := values[f[0]]; ok {
877 sub, ok = s.(map[string]any)
878 if !ok {
879 return fmt.Errorf("expected map")
880 }
881 } else {
882 sub = map[string]any{}
883 values[f[0]] = sub
884 }
885 if err := setPortField(sub, f[1], port); err != nil {
886 return err
887 }
888 } else {
889 values[f[0]] = port
890 }
891 return nil
892}