blob: d3b64ab5b0548332eeeb70a9374c9214de721251 [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{}
giof71a0832024-06-27 14:45:45 +0400302 // NOTE(gio): Expects caller to have pulled already
303 dopts = append(dopts, soft.WithNoPull())
giof8843412024-05-22 16:38:05 +0400304 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400305 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
306 }
307 if o.NoPublish {
308 dopts = append(dopts, soft.WithNoCommit())
309 }
giof71a0832024-06-27 14:45:45 +0400310 if o.Force {
311 dopts = append(dopts, soft.WithForce())
312 }
gio778577f2024-04-29 09:44:38 +0400313 return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400314 if err := r.RemoveDir(appDir); err != nil {
315 return "", err
316 }
317 resourcesDir := path.Join(appDir, "resources")
318 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400319 return "", err
320 }
321 {
gioe72b54f2024-04-22 10:44:41 +0400322 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400323 return "", err
324 }
gioe72b54f2024-04-22 10:44:41 +0400325 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400326 return "", err
327 }
gioe72b54f2024-04-22 10:44:41 +0400328 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400329 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
330 return "", fmt.Errorf("%s is forbidden", name)
331 }
332 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400333 if err != nil {
334 return "", err
335 }
gio308105e2024-04-19 13:12:13 +0400336 defer w.Close()
337 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400338 return "", err
339 }
340 }
gio308105e2024-04-19 13:12:13 +0400341 }
342 {
343 if err := createKustomizationChain(r, resourcesDir); err != nil {
344 return "", err
345 }
gioefa0ed42024-06-13 12:31:43 +0400346 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400347 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400348 appKust.AddResources(name)
349 w, err := r.Writer(path.Join(resourcesDir, name))
350 if err != nil {
351 return "", err
352 }
353 defer w.Close()
354 if _, err := w.Write(contents); err != nil {
355 return "", err
356 }
357 }
gioe72b54f2024-04-22 10:44:41 +0400358 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400359 return "", err
360 }
361 }
gioe72b54f2024-04-22 10:44:41 +0400362 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400363 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400364}
365
gio3cdee592024-04-17 10:15:56 +0400366// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400367func (m *AppManager) Install(
368 app EnvApp,
369 instanceId string,
370 appDir string,
371 namespace string,
372 values map[string]any,
373 opts ...InstallOption,
374) (ReleaseResources, error) {
gioefa0ed42024-06-13 12:31:43 +0400375 portFields := findPortFields(app.Schema())
376 fakeReservations := map[string]reservePortResp{}
377 for i, f := range portFields {
378 fakeReservations[f] = reservePortResp{Port: i}
379 }
380 if err := setPortFields(values, fakeReservations); err != nil {
381 return ReleaseResources{}, err
382 }
gio0eaf2712024-04-14 13:08:46 +0400383 o := &installOptions{}
384 for _, i := range opts {
385 i(o)
386 }
gio3af43942024-04-16 08:13:50 +0400387 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400388 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400389 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400390 }
giof8843412024-05-22 16:38:05 +0400391 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400392 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400393 }
gio0eaf2712024-04-14 13:08:46 +0400394 var env EnvConfig
395 if o.Env != nil {
396 env = *o.Env
397 } else {
398 var err error
399 env, err = m.Config()
400 if err != nil {
401 return ReleaseResources{}, err
402 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400403 }
giof8843412024-05-22 16:38:05 +0400404 var lg LocalChartGenerator
405 if o.LG != nil {
406 lg = o.LG
407 } else {
408 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
409 }
gio3cdee592024-04-17 10:15:56 +0400410 release := Release{
411 AppInstanceId: instanceId,
412 Namespace: namespace,
413 RepoAddr: m.repoIO.FullAddress(),
414 AppDir: appDir,
415 }
giof8843412024-05-22 16:38:05 +0400416 rendered, err := app.Render(release, env, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400417 if err != nil {
gio778577f2024-04-29 09:44:38 +0400418 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400419 }
gioefa0ed42024-06-13 12:31:43 +0400420 reservators := map[string]string{}
421 allocators := map[string]string{}
422 for _, pf := range rendered.Ports {
423 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
424 allocators[portFields[pf.SourcePort]] = pf.Allocator
425 }
426 portReservations, err := reservePorts(reservators)
427 if err != nil {
428 return ReleaseResources{}, err
429 }
430 if err := setPortFields(values, portReservations); err != nil {
431 return ReleaseResources{}, err
432 }
giof8843412024-05-22 16:38:05 +0400433 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
434 if o.FetchContainerImages {
435 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
436 return ReleaseResources{}, err
437 }
gio0eaf2712024-04-14 13:08:46 +0400438 }
giof71a0832024-06-27 14:45:45 +0400439 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
440 if err != nil {
giof8843412024-05-22 16:38:05 +0400441 return ReleaseResources{}, err
442 }
giof71a0832024-06-27 14:45:45 +0400443 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400444 if o.FetchContainerImages {
445 release.ImageRegistry = imageRegistry
446 }
447 rendered, err = app.Render(release, env, values, localCharts)
448 if err != nil {
449 return ReleaseResources{}, err
450 }
451 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 +0400452 return ReleaseResources{}, err
453 }
gioff2a29a2024-05-01 17:06:42 +0400454 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400455 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
giocdfa3722024-06-13 20:10:14 +0400456 fmt.Println(err)
gioff2a29a2024-05-01 17:06:42 +0400457 return ReleaseResources{}, err
458 }
gio778577f2024-04-29 09:44:38 +0400459 return ReleaseResources{
460 Helm: extractHelm(rendered.Resources),
461 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400462}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400463
gio778577f2024-04-29 09:44:38 +0400464type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400465 Metadata struct {
466 Name string `json:"name"`
467 Namespace string `json:"namespace"`
468 Annotations map[string]string `json:"annotations"`
469 } `json:"metadata"`
470 Kind string `json:"kind"`
471 Status struct {
gio778577f2024-04-29 09:44:38 +0400472 Conditions []struct {
473 Type string `json:"type"`
474 Status string `json:"status"`
475 } `json:"conditions"`
476 } `json:"status,omitempty"`
477}
478
479func extractHelm(resources CueAppData) []Resource {
480 ret := make([]Resource, 0, len(resources))
481 for _, contents := range resources {
482 var h helmRelease
483 if err := yaml.Unmarshal(contents, &h); err != nil {
484 panic(err) // TODO(gio): handle
485 }
gio0eaf2712024-04-14 13:08:46 +0400486 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400487 res := Resource{
488 Name: h.Metadata.Name,
489 Namespace: h.Metadata.Namespace,
490 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
491 }
492 if h.Metadata.Annotations != nil {
493 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
494 if ok && len(info) != 0 {
495 res.Info = info
496 }
497 }
498 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400499 }
gio778577f2024-04-29 09:44:38 +0400500 }
501 return ret
502}
503
giof8843412024-05-22 16:38:05 +0400504// TODO(gio): take app configuration from the repo
505func (m *AppManager) Update(
506 instanceId string,
507 values map[string]any,
508 opts ...InstallOption,
509) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400510 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400511 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400512 }
gio3cdee592024-04-17 10:15:56 +0400513 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400514 if err != nil {
gio778577f2024-04-29 09:44:38 +0400515 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400516 }
gio308105e2024-04-19 13:12:13 +0400517 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400518 app, err := m.GetInstanceApp(instanceId)
519 if err != nil {
520 return ReleaseResources{}, err
521 }
gio308105e2024-04-19 13:12:13 +0400522 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400523 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400524 if err != nil {
gio778577f2024-04-29 09:44:38 +0400525 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400526 }
giocdfa3722024-06-13 20:10:14 +0400527 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400528 if err != nil {
529 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400530 }
giocdfa3722024-06-13 20:10:14 +0400531 rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400532 if err != nil {
gio778577f2024-04-29 09:44:38 +0400533 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400534 }
gio0eaf2712024-04-14 13:08:46 +0400535 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400536}
537
538func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400539 if err := m.repoIO.Pull(); err != nil {
540 return err
541 }
giocdfa3722024-06-13 20:10:14 +0400542 var portForward []PortForward
543 if err := m.repoIO.Do(func(r soft.RepoFS) (string, error) {
544 instanceDir := filepath.Join(m.appDirRoot, instanceId)
545 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
546 if err != nil {
547 return "", err
548 }
549 portForward = renderedCfg.PortForward
550 r.RemoveDir(instanceDir)
gio308105e2024-04-19 13:12:13 +0400551 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400552 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400553 if err != nil {
554 return "", err
555 }
556 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400557 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400558 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400559 }); err != nil {
560 return err
561 }
562 if err := closePorts(portForward); err != nil {
563 fmt.Println(err)
564 return err
565 }
566 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400567}
568
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400569// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gioe72b54f2024-04-22 10:44:41 +0400570func CreateNetworks(env EnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400571 return []Network{
572 {
giocdfa3722024-06-13 20:10:14 +0400573 Name: "Public",
574 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
575 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
576 Domain: env.Domain,
577 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
578 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
579 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400580 },
581 {
giocdfa3722024-06-13 20:10:14 +0400582 Name: "Private",
583 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
584 Domain: env.PrivateDomain,
585 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
586 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
587 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400588 },
589 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400590}
gio3cdee592024-04-17 10:15:56 +0400591
gio0eaf2712024-04-14 13:08:46 +0400592type installOptions struct {
giof8843412024-05-22 16:38:05 +0400593 NoPublish bool
594 Env *EnvConfig
595 Branch string
596 LG LocalChartGenerator
597 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400598 Force bool
gio0eaf2712024-04-14 13:08:46 +0400599}
600
601type InstallOption func(*installOptions)
602
603func WithConfig(env *EnvConfig) InstallOption {
604 return func(o *installOptions) {
605 o.Env = env
606 }
607}
608
609func WithBranch(branch string) InstallOption {
610 return func(o *installOptions) {
611 o.Branch = branch
612 }
613}
614
giof71a0832024-06-27 14:45:45 +0400615func WithForce() InstallOption {
616 return func(o *installOptions) {
617 o.Force = true
618 }
619}
620
giof8843412024-05-22 16:38:05 +0400621func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
622 return func(o *installOptions) {
623 o.LG = lg
624 }
625}
626
627func WithFetchContainerImages() InstallOption {
628 return func(o *installOptions) {
629 o.FetchContainerImages = true
630 }
631}
632
633func WithNoPublish() InstallOption {
634 return func(o *installOptions) {
635 o.NoPublish = true
636 }
637}
638
639// InfraAppmanager
640
641type InfraAppManager struct {
642 repoIO soft.RepoIO
643 nsc NamespaceCreator
644 hf HelmFetcher
645 lg LocalChartGenerator
646}
647
648func NewInfraAppManager(
649 repoIO soft.RepoIO,
650 nsc NamespaceCreator,
651 hf HelmFetcher,
652 lg LocalChartGenerator,
653) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400654 return &InfraAppManager{
655 repoIO,
giof8843412024-05-22 16:38:05 +0400656 nsc,
657 hf,
658 lg,
gio3cdee592024-04-17 10:15:56 +0400659 }, nil
660}
661
662func (m *InfraAppManager) Config() (InfraConfig, error) {
663 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400664 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400665 return InfraConfig{}, err
666 } else {
667 return cfg, nil
668 }
669}
670
gioe72b54f2024-04-22 10:44:41 +0400671func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
672 var cfg InfraAppInstanceConfig
673 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
674 return InfraAppInstanceConfig{}, err
675 } else {
676 return cfg, nil
677 }
678}
679
680func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
681 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
682 if err != nil {
683 return InfraAppInstanceConfig{}, err
684 }
685 for _, app := range kust.Resources {
686 if app == id {
687 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
688 if err != nil {
689 return InfraAppInstanceConfig{}, err
690 }
691 cfg.Id = id
692 return cfg, nil
693 }
694 }
695 return InfraAppInstanceConfig{}, nil
696}
697
gio778577f2024-04-29 09:44:38 +0400698func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400699 appDir = filepath.Clean(appDir)
700 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400701 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400702 }
giof8843412024-05-22 16:38:05 +0400703 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400704 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400705 }
706 infra, err := m.Config()
707 if err != nil {
gio778577f2024-04-29 09:44:38 +0400708 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400709 }
710 release := Release{
711 Namespace: namespace,
712 RepoAddr: m.repoIO.FullAddress(),
713 AppDir: appDir,
714 }
giof8843412024-05-22 16:38:05 +0400715 rendered, err := app.Render(release, infra, values, nil)
716 if err != nil {
717 return ReleaseResources{}, err
718 }
giof71a0832024-06-27 14:45:45 +0400719 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
720 if err != nil {
giof8843412024-05-22 16:38:05 +0400721 return ReleaseResources{}, err
722 }
giof71a0832024-06-27 14:45:45 +0400723 localCharts := generateLocalCharts(m.lg, charts)
giof8843412024-05-22 16:38:05 +0400724 rendered, err = app.Render(release, infra, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400725 if err != nil {
gio778577f2024-04-29 09:44:38 +0400726 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400727 }
gio0eaf2712024-04-14 13:08:46 +0400728 return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
gioe72b54f2024-04-22 10:44:41 +0400729}
730
giof8843412024-05-22 16:38:05 +0400731// TODO(gio): take app configuration from the repo
732func (m *InfraAppManager) Update(
733 instanceId string,
734 values map[string]any,
735 opts ...InstallOption,
736) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400737 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400738 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400739 }
740 env, err := m.Config()
741 if err != nil {
gio778577f2024-04-29 09:44:38 +0400742 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400743 }
744 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400745 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
746 if err != nil {
747 return ReleaseResources{}, err
748 }
749 app, err := NewCueInfraApp(appCfg)
750 if err != nil {
751 return ReleaseResources{}, err
752 }
gioe72b54f2024-04-22 10:44:41 +0400753 instanceConfigPath := filepath.Join(instanceDir, "config.json")
754 config, err := m.appConfig(instanceConfigPath)
755 if err != nil {
gio778577f2024-04-29 09:44:38 +0400756 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400757 }
giocdfa3722024-06-13 20:10:14 +0400758 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400759 if err != nil {
760 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400761 }
giocdfa3722024-06-13 20:10:14 +0400762 rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +0400763 if err != nil {
gio778577f2024-04-29 09:44:38 +0400764 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400765 }
gio0eaf2712024-04-14 13:08:46 +0400766 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400767}
giof8843412024-05-22 16:38:05 +0400768
769func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
770 ret := make(map[string]string)
771 for name, chart := range charts.Git {
772 chartRoot := filepath.Join(root, name)
773 ret[name] = chartRoot
774 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
775 return nil, err
776 }
777 }
778 return ret, nil
779}
780
781func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
782 ret := make(map[string]helmv2.HelmChartTemplateSpec)
783 for name, path := range charts {
784 ret[name] = g.Generate(path)
785 }
786 return ret
787}
788
789func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
790 for _, img := range imgs {
791 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
792 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
793 "skopeo",
794 "--insecure-policy",
795 "copy",
796 "--dest-tls-verify=false", // TODO(gio): enable
797 "--multi-arch=all",
798 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
799 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
800 }); err != nil {
801 return err
802 }
803 }
804 return nil
805}
806
807type renderedInstance struct {
808 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +0400809 PortForward []PortForward `json:"portForward"`
giof8843412024-05-22 16:38:05 +0400810}
811
giocdfa3722024-06-13 20:10:14 +0400812func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +0400813 r, err := fs.Reader(path)
814 if err != nil {
giocdfa3722024-06-13 20:10:14 +0400815 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400816 }
817 defer r.Close()
818 var cfg renderedInstance
819 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +0400820 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +0400821 }
giocdfa3722024-06-13 20:10:14 +0400822 return cfg, nil
giof8843412024-05-22 16:38:05 +0400823}
gioefa0ed42024-06-13 12:31:43 +0400824
825func findPortFields(scm Schema) []string {
826 switch scm.Kind() {
827 case KindBoolean:
828 return []string{}
829 case KindInt:
830 return []string{}
831 case KindString:
832 return []string{}
833 case KindStruct:
834 ret := []string{}
835 for _, f := range scm.Fields() {
836 for _, p := range findPortFields(f.Schema) {
837 if p == "" {
838 ret = append(ret, f.Name)
839 } else {
840 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
841 }
842 }
843 }
844 return ret
845 case KindNetwork:
846 return []string{}
847 case KindAuth:
848 return []string{}
849 case KindSSHKey:
850 return []string{}
851 case KindNumber:
852 return []string{}
853 case KindArrayString:
854 return []string{}
855 case KindPort:
856 return []string{""}
857 default:
858 panic("MUST NOT REACH!")
859 }
860}
861
862func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
863 for p, r := range ports {
864 if err := setPortField(values, p, r.Port); err != nil {
865 return err
866 }
867 }
868 return nil
869}
870
871func setPortField(values map[string]any, field string, port int) error {
872 f := strings.SplitN(field, ".", 2)
873 if len(f) == 2 {
874 var sub map[string]any
875 if s, ok := values[f[0]]; ok {
876 sub, ok = s.(map[string]any)
877 if !ok {
878 return fmt.Errorf("expected map")
879 }
880 } else {
881 sub = map[string]any{}
882 values[f[0]] = sub
883 }
884 if err := setPortField(sub, f[1], port); err != nil {
885 return err
886 }
887 } else {
888 values[f[0]] = port
889 }
890 return nil
891}