blob: c98be4b4e4c60b8b5ecaee699cd16538253a63f6 [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"
gio838bcb82025-05-15 19:39:04 +040013 "slices"
giof8843412024-05-22 16:38:05 +040014 "strings"
gio69731e82024-08-01 14:15:55 +040015 "sync"
gioe72b54f2024-04-22 10:44:41 +040016
giof6ad2982024-08-23 17:42:49 +040017 "github.com/giolekva/pcloud/core/installer/cluster"
gioefa0ed42024-06-13 12:31:43 +040018 gio "github.com/giolekva/pcloud/core/installer/io"
giof6ad2982024-08-23 17:42:49 +040019 "github.com/giolekva/pcloud/core/installer/kube"
gioe72b54f2024-04-22 10:44:41 +040020 "github.com/giolekva/pcloud/core/installer/soft"
gio778577f2024-04-29 09:44:38 +040021
giof8843412024-05-22 16:38:05 +040022 helmv2 "github.com/fluxcd/helm-controller/api/v2"
gio778577f2024-04-29 09:44:38 +040023 "sigs.k8s.io/yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040024)
25
gio5e49bb62024-07-20 10:43:19 +040026const (
27 configFileName = "config.yaml"
28 kustomizationFileName = "kustomization.yaml"
29 gitIgnoreFileName = ".gitignore"
30 includeEverything = "!*"
31)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040032
gio778577f2024-04-29 09:44:38 +040033var ErrorNotFound = errors.New("not found")
34
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040035type AppManager struct {
gio864b4332024-09-05 13:56:47 +040036 l sync.Locker
giof6ad2982024-08-23 17:42:49 +040037 repo soft.RepoIO
gio864b4332024-09-05 13:56:47 +040038 nsc NamespaceCreator
39 jc JobCreator
40 hf HelmFetcher
41 vpnAPIClient VPNAPIClient
giof6ad2982024-08-23 17:42:49 +040042 cnc ClusterNetworkConfigurator
gio864b4332024-09-05 13:56:47 +040043 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040044}
45
giof8843412024-05-22 16:38:05 +040046func NewAppManager(
giof6ad2982024-08-23 17:42:49 +040047 repo soft.RepoIO,
giof8843412024-05-22 16:38:05 +040048 nsc NamespaceCreator,
49 jc JobCreator,
50 hf HelmFetcher,
gio864b4332024-09-05 13:56:47 +040051 vpnKeyGen VPNAPIClient,
giof6ad2982024-08-23 17:42:49 +040052 cnc ClusterNetworkConfigurator,
giof8843412024-05-22 16:38:05 +040053 appDirRoot string,
54) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040055 return &AppManager{
gio69731e82024-08-01 14:15:55 +040056 &sync.Mutex{},
giof6ad2982024-08-23 17:42:49 +040057 repo,
giof8843412024-05-22 16:38:05 +040058 nsc,
59 jc,
60 hf,
gio36b23b32024-08-25 12:20:54 +040061 vpnKeyGen,
giof6ad2982024-08-23 17:42:49 +040062 cnc,
gio308105e2024-04-19 13:12:13 +040063 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040064 }, nil
65}
66
gioe72b54f2024-04-22 10:44:41 +040067func (m *AppManager) Config() (EnvConfig, error) {
68 var cfg EnvConfig
giof6ad2982024-08-23 17:42:49 +040069 if err := soft.ReadYaml(m.repo, configFileName, &cfg); err != nil {
gioe72b54f2024-04-22 10:44:41 +040070 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040071 } else {
72 return cfg, nil
73 }
74}
75
gio3cdee592024-04-17 10:15:56 +040076func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
77 var cfg AppInstanceConfig
giof6ad2982024-08-23 17:42:49 +040078 if err := soft.ReadJson(m.repo, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040079 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040080 } else {
81 return cfg, nil
82 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040083}
84
giof8acc612025-04-26 08:20:55 +040085func (m *AppManager) AppRendered(instanceId string) ([]byte, error) {
86 path := filepath.Join(m.appDirRoot, instanceId, "rendered.json")
87 return soft.ReadFile(m.repo, path)
88}
89
gio7fbd4ad2024-08-27 10:06:39 +040090func (m *AppManager) GetAllInstances() ([]AppInstanceConfig, error) {
giof6ad2982024-08-23 17:42:49 +040091 m.repo.Pull()
gio92116ca2024-10-06 13:55:46 +040092 kust, err := soft.ReadKustomization(m.repo, filepath.Join(m.appDirRoot, kustomizationFileName))
gio3af43942024-04-16 08:13:50 +040093 if err != nil {
giof6ad2982024-08-23 17:42:49 +040094 if errors.Is(err, fs.ErrNotExist) {
95 return nil, nil
96 }
gio3af43942024-04-16 08:13:50 +040097 return nil, err
98 }
gio3cdee592024-04-17 10:15:56 +040099 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +0400100 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +0400101 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
102 if err != nil {
103 return nil, err
104 }
105 cfg.Id = app
106 ret = append(ret, cfg)
107 }
108 return ret, nil
109}
110
gio7fbd4ad2024-08-27 10:06:39 +0400111func (m *AppManager) GetAllAppInstances(name string) ([]AppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400112 kust, err := soft.ReadKustomization(m.repo, filepath.Join(m.appDirRoot, kustomizationFileName))
gio308105e2024-04-19 13:12:13 +0400113 if err != nil {
giocb34ad22024-07-11 08:01:13 +0400114 if errors.Is(err, fs.ErrNotExist) {
115 return nil, nil
116 } else {
117 return nil, err
118 }
gio308105e2024-04-19 13:12:13 +0400119 }
120 ret := make([]AppInstanceConfig, 0)
121 for _, app := range kust.Resources {
122 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400123 if err != nil {
124 return nil, err
125 }
126 cfg.Id = app
127 if cfg.AppId == name {
128 ret = append(ret, cfg)
129 }
130 }
131 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400132}
133
gio7fbd4ad2024-08-27 10:06:39 +0400134func (m *AppManager) GetInstance(id string) (*AppInstanceConfig, error) {
135 appDir := filepath.Clean(filepath.Join(m.appDirRoot, id))
136 cfgPath := filepath.Join(appDir, "config.json")
gio7fbd4ad2024-08-27 10:06:39 +0400137 cfg, err := m.appConfig(cfgPath)
gio3af43942024-04-16 08:13:50 +0400138 if err != nil {
gio778577f2024-04-29 09:44:38 +0400139 return nil, err
gio3af43942024-04-16 08:13:50 +0400140 }
gio7fbd4ad2024-08-27 10:06:39 +0400141 cfg.Id = id
142 return &cfg, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400143}
144
gio63a1a822025-04-23 12:59:40 +0400145func GetCueAppData(fs soft.RepoFS, dir string, overrides CueAppData) (CueAppData, error) {
giof8843412024-05-22 16:38:05 +0400146 files, err := fs.ListDir(dir)
147 if err != nil {
148 return nil, err
149 }
150 cfg := CueAppData{}
151 for _, f := range files {
152 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
153 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
154 if err != nil {
155 return nil, err
156 }
157 cfg[f.Name()] = contents
158 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400159 }
gio63a1a822025-04-23 12:59:40 +0400160 for k, v := range overrides {
161 cfg[k] = v
162 }
gio308105e2024-04-19 13:12:13 +0400163 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400164}
165
gio63a1a822025-04-23 12:59:40 +0400166func (m *AppManager) GetInstanceApp(id string, overrides CueAppData) (EnvApp, error) {
167 cfg, err := GetCueAppData(m.repo, filepath.Join(m.appDirRoot, id), overrides)
giof8843412024-05-22 16:38:05 +0400168 if err != nil {
169 return nil, err
170 }
171 return NewCueEnvApp(cfg)
172}
173
gio3af43942024-04-16 08:13:50 +0400174type allocatePortReq struct {
175 Protocol string `json:"protocol"`
176 SourcePort int `json:"sourcePort"`
177 TargetService string `json:"targetService"`
178 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400179 Secret string `json:"secret,omitempty"`
180}
181
182type removePortReq struct {
183 Protocol string `json:"protocol"`
184 SourcePort int `json:"sourcePort"`
185 TargetService string `json:"targetService"`
186 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400187}
188
gioefa0ed42024-06-13 12:31:43 +0400189type reservePortResp struct {
190 Port int `json:"port"`
191 Secret string `json:"secret"`
192}
193
gio721c0042025-04-03 11:56:36 +0400194type reservePortInfo struct {
195 reserveAddr string
196 RemoteProxy bool `json:"remoteProxy"`
197}
198
199func reservePorts(ports map[string]reservePortInfo) (map[string]reservePortResp, error) {
gioefa0ed42024-06-13 12:31:43 +0400200 ret := map[string]reservePortResp{}
gio721c0042025-04-03 11:56:36 +0400201 for p, cfg := range ports {
202 var buf bytes.Buffer
203 if err := json.NewEncoder(&buf).Encode(cfg); err != nil {
204 return nil, err
205 }
206 resp, err := http.Post(cfg.reserveAddr, "application/json", &buf)
gioefa0ed42024-06-13 12:31:43 +0400207 if err != nil {
208 return nil, err
209 }
210 if resp.StatusCode != http.StatusOK {
211 var e bytes.Buffer
212 io.Copy(&e, resp.Body)
213 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
214 }
215 var r reservePortResp
216 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
217 return nil, err
218 }
219 ret[p] = r
220 }
221 return ret, nil
222}
223
gio802311e2024-11-04 08:37:34 +0400224func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string, ns string) error {
gio6ce44812025-05-17 07:31:54 +0400225 fmt.Printf("-- %+v\n-- %+v\n--%+v\n", ports, reservations, allocators)
gio3af43942024-04-16 08:13:50 +0400226 for _, p := range ports {
gio721c0042025-04-03 11:56:36 +0400227 var target string
228 if p.Cluster == "" {
giof4344632025-04-08 20:04:35 +0400229 if p.Service.Namespace == "" {
230 target = fmt.Sprintf("%s/%s", ns, p.Service.Name)
231 } else {
232 target = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
233 }
gio721c0042025-04-03 11:56:36 +0400234 } else {
235 target = p.Service.Name
236 }
gio3af43942024-04-16 08:13:50 +0400237 var buf bytes.Buffer
238 req := allocatePortReq{
239 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400240 SourcePort: p.Port,
gio721c0042025-04-03 11:56:36 +0400241 TargetService: target,
gio802311e2024-11-04 08:37:34 +0400242 TargetPort: p.Service.Port,
gio3af43942024-04-16 08:13:50 +0400243 }
gioefa0ed42024-06-13 12:31:43 +0400244 allocator := ""
245 for n, r := range reservations {
gio802311e2024-11-04 08:37:34 +0400246 if p.Port == r.Port {
gioefa0ed42024-06-13 12:31:43 +0400247 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400248 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400249 break
250 }
251 }
252 if allocator == "" {
gio802311e2024-11-04 08:37:34 +0400253 return fmt.Errorf("Could not find allocator for: %d", p.Port)
gioefa0ed42024-06-13 12:31:43 +0400254 }
giobd7ab0b2024-06-17 12:55:17 +0400255 if err := json.NewEncoder(&buf).Encode(req); err != nil {
256 return err
257 }
gioefa0ed42024-06-13 12:31:43 +0400258 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400259 if err != nil {
260 return err
261 }
262 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400263 var r bytes.Buffer
264 io.Copy(&r, resp.Body)
gio802311e2024-11-04 08:37:34 +0400265 return fmt.Errorf("Could not allocate port %d, status code %d, message: %s", p.Port, resp.StatusCode, r.String())
gio3af43942024-04-16 08:13:50 +0400266 }
267 }
268 return nil
269}
270
gio802311e2024-11-04 08:37:34 +0400271func closePorts(ports []PortForward, ns string) error {
giocdfa3722024-06-13 20:10:14 +0400272 var retErr error
273 for _, p := range ports {
274 var buf bytes.Buffer
giof4344632025-04-08 20:04:35 +0400275 var fullName string
276 if p.Service.Namespace == "" {
277 fullName = fmt.Sprintf("%s/%s", ns, p.Service.Name)
278 } else {
279 fullName = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
280 }
giocdfa3722024-06-13 20:10:14 +0400281 req := removePortReq{
282 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400283 SourcePort: p.Port,
giof4344632025-04-08 20:04:35 +0400284 TargetService: fullName,
gio802311e2024-11-04 08:37:34 +0400285 TargetPort: p.Service.Port,
giocdfa3722024-06-13 20:10:14 +0400286 }
287 if err := json.NewEncoder(&buf).Encode(req); err != nil {
288 retErr = err
289 continue
290 }
giod78896a2025-04-10 07:42:13 +0400291 resp, err := http.Post(p.Network.DeallocatePortAddr, "application/json", &buf)
giocdfa3722024-06-13 20:10:14 +0400292 if err != nil {
293 retErr = err
294 continue
295 }
296 if resp.StatusCode != http.StatusOK {
gio802311e2024-11-04 08:37:34 +0400297 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.Port, resp.StatusCode)
giocdfa3722024-06-13 20:10:14 +0400298 continue
299 }
300 }
301 return retErr
302}
303
gioe72b54f2024-04-22 10:44:41 +0400304func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400305 for p := filepath.Clean(path); p != "/"; {
306 parent, child := filepath.Split(p)
gio92116ca2024-10-06 13:55:46 +0400307 kustPath := filepath.Join(parent, kustomizationFileName)
gioe72b54f2024-04-22 10:44:41 +0400308 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400309 if err != nil {
310 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400311 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400312 kust = &k
313 } else {
314 return err
315 }
316 }
317 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400318 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400319 return err
320 }
321 p = filepath.Clean(parent)
322 }
323 return nil
324}
325
gio778577f2024-04-29 09:44:38 +0400326type Resource struct {
giof8acc612025-04-26 08:20:55 +0400327 Id string `json:"id"`
giob4a3a192024-08-19 09:55:47 +0400328 Name string `json:"name"`
329 Namespace string `json:"namespace"`
330 Info string `json:"info"`
331 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400332}
333
334type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400335 Release Release
336 Helm []Resource
gio6ce44812025-05-17 07:31:54 +0400337 Access []Access
gio94904702024-07-26 16:58:34 +0400338 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400339}
340
gio3cdee592024-04-17 10:15:56 +0400341// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400342func installApp(
gioe72b54f2024-04-22 10:44:41 +0400343 repo soft.RepoIO,
344 appDir string,
345 name string,
346 config any,
gioe72b54f2024-04-22 10:44:41 +0400347 resources CueAppData,
348 data CueAppData,
giof8843412024-05-22 16:38:05 +0400349 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400350) error {
giof8843412024-05-22 16:38:05 +0400351 var o installOptions
352 for _, i := range opts {
353 i(&o)
354 }
355 dopts := []soft.DoOption{}
356 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400357 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
358 }
gio94904702024-07-26 16:58:34 +0400359 if o.NoPull {
360 dopts = append(dopts, soft.WithNoPull())
361 }
giof8843412024-05-22 16:38:05 +0400362 if o.NoPublish {
363 dopts = append(dopts, soft.WithNoCommit())
364 }
giof71a0832024-06-27 14:45:45 +0400365 if o.Force {
366 dopts = append(dopts, soft.WithForce())
367 }
gio9d66f322024-07-06 13:45:10 +0400368 if o.NoLock {
369 dopts = append(dopts, soft.WithNoLock())
370 }
giob4a3a192024-08-19 09:55:47 +0400371 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
giof6ad2982024-08-23 17:42:49 +0400372 if err := r.RemoveAll(appDir); err != nil {
gio308105e2024-04-19 13:12:13 +0400373 return "", err
374 }
375 resourcesDir := path.Join(appDir, "resources")
376 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400377 return "", err
378 }
gio94904702024-07-26 16:58:34 +0400379 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400380 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400381 return err
gio5e49bb62024-07-20 10:43:19 +0400382 }
gioe72b54f2024-04-22 10:44:41 +0400383 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400384 return err
gio3af43942024-04-16 08:13:50 +0400385 }
gioe72b54f2024-04-22 10:44:41 +0400386 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400387 return err
gio308105e2024-04-19 13:12:13 +0400388 }
gioe72b54f2024-04-22 10:44:41 +0400389 for name, contents := range data {
gio92116ca2024-10-06 13:55:46 +0400390 if name == "config.json" || name == kustomizationFileName || name == "resources" {
gio94904702024-07-26 16:58:34 +0400391 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400392 }
393 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400394 if err != nil {
gio94904702024-07-26 16:58:34 +0400395 return err
gio3af43942024-04-16 08:13:50 +0400396 }
gio308105e2024-04-19 13:12:13 +0400397 defer w.Close()
398 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400399 return err
gio3af43942024-04-16 08:13:50 +0400400 }
401 }
gio94904702024-07-26 16:58:34 +0400402 return nil
403 }(); err != nil {
404 return "", err
gio308105e2024-04-19 13:12:13 +0400405 }
gio94904702024-07-26 16:58:34 +0400406 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400407 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400408 return err
gio308105e2024-04-19 13:12:13 +0400409 }
gioefa0ed42024-06-13 12:31:43 +0400410 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400411 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400412 appKust.AddResources(name)
413 w, err := r.Writer(path.Join(resourcesDir, name))
414 if err != nil {
gio94904702024-07-26 16:58:34 +0400415 return err
gio308105e2024-04-19 13:12:13 +0400416 }
417 defer w.Close()
418 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400419 return err
gio308105e2024-04-19 13:12:13 +0400420 }
421 }
gio92116ca2024-10-06 13:55:46 +0400422 if err := soft.WriteYaml(r, path.Join(resourcesDir, kustomizationFileName), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400423 return err
gio3af43942024-04-16 08:13:50 +0400424 }
gio94904702024-07-26 16:58:34 +0400425 return nil
426 }(); err != nil {
427 return "", err
gio3af43942024-04-16 08:13:50 +0400428 }
gioe72b54f2024-04-22 10:44:41 +0400429 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400430 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400431 return err
gio3af43942024-04-16 08:13:50 +0400432}
433
gio3cdee592024-04-17 10:15:56 +0400434// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400435func (m *AppManager) Install(
436 app EnvApp,
437 instanceId string,
438 appDir string,
439 namespace string,
440 values map[string]any,
441 opts ...InstallOption,
442) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400443 o := &installOptions{}
444 for _, i := range opts {
445 i(o)
446 }
447 if !o.NoLock {
448 m.l.Lock()
449 defer m.l.Unlock()
450 }
gioefa0ed42024-06-13 12:31:43 +0400451 portFields := findPortFields(app.Schema())
452 fakeReservations := map[string]reservePortResp{}
453 for i, f := range portFields {
454 fakeReservations[f] = reservePortResp{Port: i}
455 }
456 if err := setPortFields(values, fakeReservations); err != nil {
457 return ReleaseResources{}, err
458 }
gio3af43942024-04-16 08:13:50 +0400459 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400460 if !o.NoPull {
giof6ad2982024-08-23 17:42:49 +0400461 if err := m.repo.Pull(); err != nil {
gio94904702024-07-26 16:58:34 +0400462 return ReleaseResources{}, err
463 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400464 }
gio94904702024-07-26 16:58:34 +0400465 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400466 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400467 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400468 }
gio0eaf2712024-04-14 13:08:46 +0400469 var env EnvConfig
470 if o.Env != nil {
471 env = *o.Env
472 } else {
473 var err error
474 env, err = m.Config()
475 if err != nil {
476 return ReleaseResources{}, err
477 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400478 }
giocb34ad22024-07-11 08:01:13 +0400479 var networks []Network
480 if o.Networks != nil {
481 networks = o.Networks
482 } else {
483 var err error
484 networks, err = m.CreateNetworks(env)
485 if err != nil {
486 return ReleaseResources{}, err
487 }
488 }
giof15b9da2024-09-19 06:59:16 +0400489 var clusters []Cluster
490 if o.Clusters != nil {
491 clusters = o.Clusters
492 } else {
493 if cls, err := m.GetClusters(); err != nil {
494 return ReleaseResources{}, err
495 } else {
496 clusters = ToAccessConfigs(cls)
497 }
giof6ad2982024-08-23 17:42:49 +0400498 }
giof8843412024-05-22 16:38:05 +0400499 var lg LocalChartGenerator
500 if o.LG != nil {
501 lg = o.LG
502 } else {
503 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
504 }
gio3cdee592024-04-17 10:15:56 +0400505 release := Release{
506 AppInstanceId: instanceId,
507 Namespace: namespace,
giof6ad2982024-08-23 17:42:49 +0400508 RepoAddr: m.repo.FullAddress(),
gio3cdee592024-04-17 10:15:56 +0400509 AppDir: appDir,
510 }
giof15b9da2024-09-19 06:59:16 +0400511 rendered, err := app.Render(release, env, networks, clusters, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400512 if err != nil {
gio778577f2024-04-29 09:44:38 +0400513 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400514 }
gio721c0042025-04-03 11:56:36 +0400515 reservators := map[string]reservePortInfo{}
gioefa0ed42024-06-13 12:31:43 +0400516 allocators := map[string]string{}
517 for _, pf := range rendered.Ports {
gio721c0042025-04-03 11:56:36 +0400518 reservators[portFields[pf.Port]] = reservePortInfo{
giod78896a2025-04-10 07:42:13 +0400519 reserveAddr: pf.Network.ReservePortAddr,
gio721c0042025-04-03 11:56:36 +0400520 RemoteProxy: pf.Cluster != "",
521 }
giod78896a2025-04-10 07:42:13 +0400522 allocators[portFields[pf.Port]] = pf.Network.AllocatePortAddr
gioefa0ed42024-06-13 12:31:43 +0400523 }
524 portReservations, err := reservePorts(reservators)
525 if err != nil {
526 return ReleaseResources{}, err
527 }
528 if err := setPortFields(values, portReservations); err != nil {
529 return ReleaseResources{}, err
530 }
gio7841f4f2024-07-26 19:53:49 +0400531 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400532 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
533 if o.FetchContainerImages {
534 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
535 return ReleaseResources{}, err
536 }
gio0eaf2712024-04-14 13:08:46 +0400537 }
giof6ad2982024-08-23 17:42:49 +0400538 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
giof71a0832024-06-27 14:45:45 +0400539 if err != nil {
giof8843412024-05-22 16:38:05 +0400540 return ReleaseResources{}, err
541 }
giof71a0832024-06-27 14:45:45 +0400542 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400543 if o.FetchContainerImages {
544 release.ImageRegistry = imageRegistry
545 }
giof15b9da2024-09-19 06:59:16 +0400546 rendered, err = app.Render(release, env, networks, clusters, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400547 if err != nil {
548 return ReleaseResources{}, err
549 }
giof6ad2982024-08-23 17:42:49 +0400550 for _, ns := range rendered.Namespaces {
551 if ns.Name == "" {
552 return ReleaseResources{}, fmt.Errorf("namespace name missing")
553 }
554 if ns.Kubeconfig == "" {
555 continue
556 }
557 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
558 if err != nil {
559 return ReleaseResources{}, err
560 }
561 if err := nsc.Create(ns.Name); err != nil {
562 return ReleaseResources{}, err
563 }
564 }
565 if err := installApp(m.repo, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400566 return ReleaseResources{}, err
567 }
gioff2a29a2024-05-01 17:06:42 +0400568 // TODO(gio): add ingress-nginx to release resources
gio802311e2024-11-04 08:37:34 +0400569 if err := openPorts(rendered.Ports, portReservations, allocators, release.Namespace); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400570 return ReleaseResources{}, err
571 }
giof6ad2982024-08-23 17:42:49 +0400572 for _, p := range rendered.ClusterProxies {
gio721c0042025-04-03 11:56:36 +0400573 if err := m.cnc.AddIngressProxy(p.From, p.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400574 return ReleaseResources{}, err
575 }
576 }
gio778577f2024-04-29 09:44:38 +0400577 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400578 Release: rendered.Config.Release,
579 RenderedRaw: rendered.Raw,
gio6ce44812025-05-17 07:31:54 +0400580 Access: rendered.Access,
gio94904702024-07-26 16:58:34 +0400581 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400582 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400583}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400584
gio778577f2024-04-29 09:44:38 +0400585type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400586 Metadata struct {
587 Name string `json:"name"`
588 Namespace string `json:"namespace"`
589 Annotations map[string]string `json:"annotations"`
590 } `json:"metadata"`
591 Kind string `json:"kind"`
592 Status struct {
gio778577f2024-04-29 09:44:38 +0400593 Conditions []struct {
594 Type string `json:"type"`
595 Status string `json:"status"`
596 } `json:"conditions"`
597 } `json:"status,omitempty"`
598}
599
600func extractHelm(resources CueAppData) []Resource {
601 ret := make([]Resource, 0, len(resources))
602 for _, contents := range resources {
603 var h helmRelease
604 if err := yaml.Unmarshal(contents, &h); err != nil {
605 panic(err) // TODO(gio): handle
606 }
gio0eaf2712024-04-14 13:08:46 +0400607 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400608 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400609 Name: h.Metadata.Name,
610 Namespace: h.Metadata.Namespace,
611 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
giof8acc612025-04-26 08:20:55 +0400612 Annotations: h.Metadata.Annotations,
giof9f0bee2024-06-11 20:10:05 +0400613 }
614 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400615 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400616 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
617 if ok && len(info) != 0 {
618 res.Info = info
619 }
giof8acc612025-04-26 08:20:55 +0400620 id, ok := h.Metadata.Annotations["dodo.cloud/id"]
621 if ok && len(id) != 0 {
622 res.Id = id
623 }
giof9f0bee2024-06-11 20:10:05 +0400624 }
625 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400626 }
gio778577f2024-04-29 09:44:38 +0400627 }
628 return ret
629}
630
giof8843412024-05-22 16:38:05 +0400631// TODO(gio): take app configuration from the repo
632func (m *AppManager) Update(
633 instanceId string,
634 values map[string]any,
gio63a1a822025-04-23 12:59:40 +0400635 // TODO(gio): this should not be cue specific
636 overrides CueAppData,
giof8843412024-05-22 16:38:05 +0400637 opts ...InstallOption,
638) (ReleaseResources, error) {
gio838bcb82025-05-15 19:39:04 +0400639 if values == nil {
640 values = map[string]any{}
641 }
gio69731e82024-08-01 14:15:55 +0400642 m.l.Lock()
643 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400644 if err := m.repo.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400645 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400646 }
gio3cdee592024-04-17 10:15:56 +0400647 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400648 if err != nil {
gio778577f2024-04-29 09:44:38 +0400649 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400650 }
gio308105e2024-04-19 13:12:13 +0400651 instanceDir := filepath.Join(m.appDirRoot, instanceId)
gio838bcb82025-05-15 19:39:04 +0400652 oldApp, err := m.GetInstanceApp(instanceId, nil)
giof8843412024-05-22 16:38:05 +0400653 if err != nil {
654 return ReleaseResources{}, err
655 }
gio838bcb82025-05-15 19:39:04 +0400656 newApp, err := m.GetInstanceApp(instanceId, overrides)
657 if err != nil {
658 return ReleaseResources{}, err
659 }
660 oldPorts := findPortFields(oldApp.Schema())
661 newPorts := findPortFields(newApp.Schema())
662 portFields := []string{}
663 for _, np := range newPorts {
664 if !slices.Contains(oldPorts, np) {
665 portFields = append(portFields, np)
666 }
667 }
gio6ce44812025-05-17 07:31:54 +0400668 fmt.Printf("%+v %+v %+v\n", oldPorts, newPorts, portFields)
gio838bcb82025-05-15 19:39:04 +0400669 fakeReservations := map[string]reservePortResp{}
670 for i, f := range portFields {
671 fakeReservations[f] = reservePortResp{Port: i}
672 }
673 if err := setPortFields(values, fakeReservations); err != nil {
674 return ReleaseResources{}, err
675 }
gio308105e2024-04-19 13:12:13 +0400676 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400677 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400678 if err != nil {
gio778577f2024-04-29 09:44:38 +0400679 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400680 }
giof6ad2982024-08-23 17:42:49 +0400681 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400682 if err != nil {
683 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400684 }
giocb34ad22024-07-11 08:01:13 +0400685 networks, err := m.CreateNetworks(env)
686 if err != nil {
687 return ReleaseResources{}, err
688 }
giof6ad2982024-08-23 17:42:49 +0400689 clusters, err := m.GetClusters()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400690 if err != nil {
gio778577f2024-04-29 09:44:38 +0400691 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400692 }
gio838bcb82025-05-15 19:39:04 +0400693 rendered, err := newApp.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
694 if err != nil {
695 return ReleaseResources{}, err
696 }
697 reservators := map[string]reservePortInfo{}
698 allocators := map[string]string{}
699 for _, pf := range rendered.Ports {
700 found := false
701 for _, fr := range fakeReservations {
702 if fr.Port == pf.Port {
703 found = true
704 }
705 }
706 if !found {
707 continue
708 }
709 reservators[portFields[pf.Port]] = reservePortInfo{
710 reserveAddr: pf.Network.ReservePortAddr,
711 RemoteProxy: pf.Cluster != "",
712 }
713 allocators[portFields[pf.Port]] = pf.Network.AllocatePortAddr
714 }
715 portReservations, err := reservePorts(reservators)
716 if err != nil {
717 return ReleaseResources{}, err
718 }
719 if err := setPortFields(values, portReservations); err != nil {
720 return ReleaseResources{}, err
721 }
722 rendered, err = newApp.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
giof6ad2982024-08-23 17:42:49 +0400723 if err != nil {
gio94904702024-07-26 16:58:34 +0400724 return ReleaseResources{}, err
725 }
giof6ad2982024-08-23 17:42:49 +0400726 for _, ns := range rendered.Namespaces {
727 if ns.Name == "" {
728 return ReleaseResources{}, fmt.Errorf("namespace name missing")
729 }
730 if ns.Kubeconfig == "" {
731 continue
732 }
733 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
734 if err != nil {
735 return ReleaseResources{}, err
736 }
737 if err := nsc.Create(ns.Name); err != nil {
738 return ReleaseResources{}, err
739 }
740 }
741 if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
742 return ReleaseResources{}, err
743 }
gio838bcb82025-05-15 19:39:04 +0400744 toOpen := []PortForward{}
745 for _, op := range rendered.Ports {
746 found := false
747 for _, rp := range portReservations {
748 if rp.Port == op.Port {
749 found = true
750 break
751 }
752 }
753 if !found {
754 toOpen = append(toOpen, op)
755 }
756 }
757 if err := openPorts(toOpen, portReservations, allocators, config.Release.Namespace); err != nil {
758 return ReleaseResources{}, err
759 }
giof6ad2982024-08-23 17:42:49 +0400760 for _, ocp := range renderedCfg.Out.ClusterProxy {
761 found := false
762 for _, ncp := range rendered.ClusterProxies {
763 if ocp == ncp {
764 found = true
765 break
766 }
767 }
768 if !found {
gio721c0042025-04-03 11:56:36 +0400769 if err := m.cnc.RemoveIngressProxy(ocp.From, ocp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400770 return ReleaseResources{}, err
771 }
772 }
773 }
774 for _, ncp := range rendered.ClusterProxies {
775 found := false
776 for _, ocp := range renderedCfg.Out.ClusterProxy {
777 if ocp == ncp {
778 found = true
779 break
780 }
781 }
782 if !found {
gio721c0042025-04-03 11:56:36 +0400783 if err := m.cnc.AddIngressProxy(ncp.From, ncp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400784 return ReleaseResources{}, err
785 }
786 }
787 }
gio94904702024-07-26 16:58:34 +0400788 return ReleaseResources{
789 Release: rendered.Config.Release,
790 RenderedRaw: rendered.Raw,
gio6ce44812025-05-17 07:31:54 +0400791 Access: rendered.Access,
gio94904702024-07-26 16:58:34 +0400792 Helm: extractHelm(rendered.Resources),
793 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400794}
795
796func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400797 m.l.Lock()
798 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400799 if err := m.repo.Pull(); err != nil {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400800 return err
801 }
gio864b4332024-09-05 13:56:47 +0400802 var cfg renderedInstance
giof6ad2982024-08-23 17:42:49 +0400803 if _, err := m.repo.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400804 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof6ad2982024-08-23 17:42:49 +0400805 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giocdfa3722024-06-13 20:10:14 +0400806 if err != nil {
807 return "", err
808 }
gio864b4332024-09-05 13:56:47 +0400809 cfg = renderedCfg
giof6ad2982024-08-23 17:42:49 +0400810 r.RemoveAll(instanceDir)
gio5887caa2024-10-03 15:07:23 +0400811 curr := instanceDir
gio829b1b72024-10-05 21:50:56 +0400812 for {
gio5887caa2024-10-03 15:07:23 +0400813 p := filepath.Dir(curr)
gio829b1b72024-10-05 21:50:56 +0400814 if p == curr {
815 break
816 }
gio5887caa2024-10-03 15:07:23 +0400817 n := filepath.Base(curr)
gio92116ca2024-10-06 13:55:46 +0400818 kustPath := filepath.Join(p, kustomizationFileName)
gio5887caa2024-10-03 15:07:23 +0400819 kust, err := soft.ReadKustomization(r, kustPath)
820 if err != nil {
821 return "", err
822 }
823 kust.RemoveResources(n)
gio829b1b72024-10-05 21:50:56 +0400824 if len(kust.Resources) > 0 || p == m.appDirRoot {
gio5887caa2024-10-03 15:07:23 +0400825 soft.WriteYaml(r, kustPath, kust)
826 break
827 } else {
828 if err := r.RemoveAll(kustPath); err != nil {
829 return "", err
830 }
831 }
gio5887caa2024-10-03 15:07:23 +0400832 curr = p
gio3af43942024-04-16 08:13:50 +0400833 }
gio3af43942024-04-16 08:13:50 +0400834 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400835 }); err != nil {
836 return err
837 }
gio802311e2024-11-04 08:37:34 +0400838 if err := closePorts(cfg.Output.PortForward, cfg.Release.Namespace); err != nil {
giocdfa3722024-06-13 20:10:14 +0400839 return err
840 }
giof6ad2982024-08-23 17:42:49 +0400841 for _, cp := range cfg.Out.ClusterProxy {
gio721c0042025-04-03 11:56:36 +0400842 if err := m.cnc.RemoveIngressProxy(cp.From, cp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400843 return err
844 }
845 }
gio864b4332024-09-05 13:56:47 +0400846 for vmName, vmCfg := range cfg.Out.VM {
847 if vmCfg.VPN.Enabled {
gio92116ca2024-10-06 13:55:46 +0400848 // Not found error is ignored as VM might have not had enough time to boot before uninstalling it.
849 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400850 return err
851 }
852 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
853 return err
854 }
gio92116ca2024-10-06 13:55:46 +0400855 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400856 return err
857 }
858 }
859 }
giocdfa3722024-06-13 20:10:14 +0400860 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400861}
862
giocb34ad22024-07-11 08:01:13 +0400863func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
864 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400865 {
giocdfa3722024-06-13 20:10:14 +0400866 Name: "Public",
867 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
868 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
869 Domain: env.Domain,
870 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
871 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
872 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400873 },
gio7841f4f2024-07-26 19:53:49 +0400874 }
875 if env.PrivateDomain != "" {
876 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400877 Name: "Private",
878 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
879 Domain: env.PrivateDomain,
880 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
881 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
882 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400883 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400884 }
gio7fbd4ad2024-08-27 10:06:39 +0400885 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400886 if err != nil {
887 return nil, err
888 }
889 for _, a := range n {
890 ret = append(ret, Network{
891 Name: a.Input["name"].(string),
892 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
893 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
894 Domain: a.Input["domain"].(string),
895 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
896 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
897 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
898 })
899 }
900 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400901}
gio3cdee592024-04-17 10:15:56 +0400902
giof6ad2982024-08-23 17:42:49 +0400903func (m *AppManager) GetClusters() ([]cluster.State, error) {
904 ret := []cluster.State{
905 {
906 Name: "default",
907 },
908 }
909 files, err := m.repo.ListDir("/clusters")
910 if err != nil {
911 if errors.Is(err, fs.ErrNotExist) {
912 return ret, nil
913 }
914 return nil, err
915 }
916 for _, f := range files {
917 if !f.IsDir() {
918 continue
919 }
920 cfgPath := filepath.Clean(filepath.Join("/clusters", f.Name(), "config.json"))
921 var c cluster.State
922 if err := soft.ReadJson(m.repo, cfgPath, &c); err != nil {
923 if errors.Is(err, fs.ErrNotExist) {
924 continue
925 }
926 return nil, err
927 }
928 ret = append(ret, c)
929 }
930 return ret, nil
931}
932
gio0eaf2712024-04-14 13:08:46 +0400933type installOptions struct {
gio94904702024-07-26 16:58:34 +0400934 NoPull bool
giof8843412024-05-22 16:38:05 +0400935 NoPublish bool
936 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400937 Networks []Network
giof15b9da2024-09-19 06:59:16 +0400938 Clusters []Cluster
giof8843412024-05-22 16:38:05 +0400939 Branch string
940 LG LocalChartGenerator
941 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400942 Force bool
gio9d66f322024-07-06 13:45:10 +0400943 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400944}
945
946type InstallOption func(*installOptions)
947
948func WithConfig(env *EnvConfig) InstallOption {
949 return func(o *installOptions) {
950 o.Env = env
951 }
952}
953
giocb34ad22024-07-11 08:01:13 +0400954func WithNetworks(networks []Network) InstallOption {
955 return func(o *installOptions) {
956 o.Networks = networks
957 }
958}
959
gio23bdc1b2024-07-11 16:07:47 +0400960func WithNoNetworks() InstallOption {
961 return WithNetworks([]Network{})
962}
963
giof15b9da2024-09-19 06:59:16 +0400964func WithClusters(clusters []Cluster) InstallOption {
965 return func(o *installOptions) {
966 o.Clusters = clusters
967 }
968}
969
gio0eaf2712024-04-14 13:08:46 +0400970func WithBranch(branch string) InstallOption {
971 return func(o *installOptions) {
972 o.Branch = branch
973 }
974}
975
giof71a0832024-06-27 14:45:45 +0400976func WithForce() InstallOption {
977 return func(o *installOptions) {
978 o.Force = true
979 }
980}
981
giof8843412024-05-22 16:38:05 +0400982func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
983 return func(o *installOptions) {
984 o.LG = lg
985 }
986}
987
988func WithFetchContainerImages() InstallOption {
989 return func(o *installOptions) {
990 o.FetchContainerImages = true
991 }
992}
993
994func WithNoPublish() InstallOption {
995 return func(o *installOptions) {
996 o.NoPublish = true
997 }
998}
999
gio94904702024-07-26 16:58:34 +04001000func WithNoPull() InstallOption {
1001 return func(o *installOptions) {
1002 o.NoPull = true
1003 }
1004}
1005
gio9d66f322024-07-06 13:45:10 +04001006func WithNoLock() InstallOption {
1007 return func(o *installOptions) {
1008 o.NoLock = true
1009 }
1010}
1011
giof8843412024-05-22 16:38:05 +04001012// InfraAppmanager
1013
1014type InfraAppManager struct {
1015 repoIO soft.RepoIO
1016 nsc NamespaceCreator
1017 hf HelmFetcher
1018 lg LocalChartGenerator
1019}
1020
1021func NewInfraAppManager(
1022 repoIO soft.RepoIO,
1023 nsc NamespaceCreator,
1024 hf HelmFetcher,
1025 lg LocalChartGenerator,
1026) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +04001027 return &InfraAppManager{
1028 repoIO,
giof8843412024-05-22 16:38:05 +04001029 nsc,
1030 hf,
1031 lg,
gio3cdee592024-04-17 10:15:56 +04001032 }, nil
1033}
1034
1035func (m *InfraAppManager) Config() (InfraConfig, error) {
1036 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +04001037 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +04001038 return InfraConfig{}, err
1039 } else {
1040 return cfg, nil
1041 }
1042}
1043
gioe72b54f2024-04-22 10:44:41 +04001044func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
1045 var cfg InfraAppInstanceConfig
1046 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
1047 return InfraAppInstanceConfig{}, err
1048 } else {
1049 return cfg, nil
1050 }
1051}
1052
1053func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +04001054 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", kustomizationFileName))
gioe72b54f2024-04-22 10:44:41 +04001055 if err != nil {
1056 return InfraAppInstanceConfig{}, err
1057 }
1058 for _, app := range kust.Resources {
1059 if app == id {
1060 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
1061 if err != nil {
1062 return InfraAppInstanceConfig{}, err
1063 }
1064 cfg.Id = id
1065 return cfg, nil
1066 }
1067 }
1068 return InfraAppInstanceConfig{}, nil
1069}
1070
gio778577f2024-04-29 09:44:38 +04001071func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +04001072 appDir = filepath.Clean(appDir)
1073 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001074 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001075 }
giof8843412024-05-22 16:38:05 +04001076 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +04001077 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001078 }
1079 infra, err := m.Config()
1080 if err != nil {
gio778577f2024-04-29 09:44:38 +04001081 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001082 }
1083 release := Release{
1084 Namespace: namespace,
1085 RepoAddr: m.repoIO.FullAddress(),
1086 AppDir: appDir,
1087 }
gio7841f4f2024-07-26 19:53:49 +04001088 networks := m.CreateNetworks(infra)
1089 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +04001090 if err != nil {
1091 return ReleaseResources{}, err
1092 }
giof71a0832024-06-27 14:45:45 +04001093 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
1094 if err != nil {
giof8843412024-05-22 16:38:05 +04001095 return ReleaseResources{}, err
1096 }
giof71a0832024-06-27 14:45:45 +04001097 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +04001098 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +04001099 if err != nil {
gio778577f2024-04-29 09:44:38 +04001100 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001101 }
gio94904702024-07-26 16:58:34 +04001102 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
1103 return ReleaseResources{}, err
1104 }
1105 return ReleaseResources{
1106 Release: rendered.Config.Release,
1107 RenderedRaw: rendered.Raw,
1108 Helm: extractHelm(rendered.Resources),
1109 }, nil
gioe72b54f2024-04-22 10:44:41 +04001110}
1111
giof8843412024-05-22 16:38:05 +04001112// TODO(gio): take app configuration from the repo
1113func (m *InfraAppManager) Update(
1114 instanceId string,
1115 values map[string]any,
1116 opts ...InstallOption,
1117) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +04001118 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001119 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001120 }
gio7841f4f2024-07-26 19:53:49 +04001121 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +04001122 if err != nil {
gio778577f2024-04-29 09:44:38 +04001123 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001124 }
1125 instanceDir := filepath.Join("/infrastructure", instanceId)
gio63a1a822025-04-23 12:59:40 +04001126 appCfg, err := GetCueAppData(m.repoIO, instanceDir, nil)
giof8843412024-05-22 16:38:05 +04001127 if err != nil {
1128 return ReleaseResources{}, err
1129 }
1130 app, err := NewCueInfraApp(appCfg)
1131 if err != nil {
1132 return ReleaseResources{}, err
1133 }
gioe72b54f2024-04-22 10:44:41 +04001134 instanceConfigPath := filepath.Join(instanceDir, "config.json")
1135 config, err := m.appConfig(instanceConfigPath)
1136 if err != nil {
gio778577f2024-04-29 09:44:38 +04001137 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001138 }
giocdfa3722024-06-13 20:10:14 +04001139 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +04001140 if err != nil {
1141 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001142 }
gio7841f4f2024-07-26 19:53:49 +04001143 networks := m.CreateNetworks(infra)
1144 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +04001145 if err != nil {
gio778577f2024-04-29 09:44:38 +04001146 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001147 }
gio94904702024-07-26 16:58:34 +04001148 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
1149 return ReleaseResources{}, err
1150 }
1151 return ReleaseResources{
1152 Release: rendered.Config.Release,
1153 RenderedRaw: rendered.Raw,
1154 Helm: extractHelm(rendered.Resources),
1155 }, nil
gio3cdee592024-04-17 10:15:56 +04001156}
giof8843412024-05-22 16:38:05 +04001157
gio7841f4f2024-07-26 19:53:49 +04001158func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
1159 return []InfraNetwork{
1160 {
1161 Name: "Public",
1162 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
1163 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
1164 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
1165 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
1166 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
1167 },
1168 }
1169}
1170
giof8843412024-05-22 16:38:05 +04001171func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
1172 ret := make(map[string]string)
1173 for name, chart := range charts.Git {
1174 chartRoot := filepath.Join(root, name)
1175 ret[name] = chartRoot
1176 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
1177 return nil, err
1178 }
1179 }
1180 return ret, nil
1181}
1182
1183func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
1184 ret := make(map[string]helmv2.HelmChartTemplateSpec)
1185 for name, path := range charts {
1186 ret[name] = g.Generate(path)
1187 }
1188 return ret
1189}
1190
1191func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
1192 for _, img := range imgs {
1193 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
1194 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
1195 "skopeo",
1196 "--insecure-policy",
1197 "copy",
1198 "--dest-tls-verify=false", // TODO(gio): enable
1199 "--multi-arch=all",
1200 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
1201 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
1202 }); err != nil {
1203 return err
1204 }
1205 }
1206 return nil
1207}
1208
1209type renderedInstance struct {
gio802311e2024-11-04 08:37:34 +04001210 Release Release `json:"release"`
giof8843412024-05-22 16:38:05 +04001211 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
gio864b4332024-09-05 13:56:47 +04001212 Out outRendered `json:"out"`
gio802311e2024-11-04 08:37:34 +04001213 Output outputRendered `json:"output"`
1214}
1215
1216type outputRendered struct {
1217 PortForward []PortForward `json:"openPort"`
gio864b4332024-09-05 13:56:47 +04001218}
1219
1220type outRendered struct {
giof6ad2982024-08-23 17:42:49 +04001221 ClusterProxy map[string]ClusterProxy
1222 VM map[string]vmRendered `json:"vm"`
gio864b4332024-09-05 13:56:47 +04001223}
1224
1225type vmRendered struct {
1226 Username string `json:"username"`
1227 VPN struct {
1228 Enabled bool `json:"enabled"`
1229 AuthKey string `json:"authKey"`
1230 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +04001231}
1232
giocdfa3722024-06-13 20:10:14 +04001233func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +04001234 r, err := fs.Reader(path)
1235 if err != nil {
giocdfa3722024-06-13 20:10:14 +04001236 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001237 }
1238 defer r.Close()
1239 var cfg renderedInstance
1240 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +04001241 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001242 }
giocdfa3722024-06-13 20:10:14 +04001243 return cfg, nil
giof8843412024-05-22 16:38:05 +04001244}
gioefa0ed42024-06-13 12:31:43 +04001245
1246func findPortFields(scm Schema) []string {
1247 switch scm.Kind() {
1248 case KindBoolean:
1249 return []string{}
1250 case KindInt:
1251 return []string{}
1252 case KindString:
1253 return []string{}
1254 case KindStruct:
1255 ret := []string{}
1256 for _, f := range scm.Fields() {
1257 for _, p := range findPortFields(f.Schema) {
1258 if p == "" {
1259 ret = append(ret, f.Name)
1260 } else {
1261 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1262 }
1263 }
1264 }
1265 return ret
1266 case KindNetwork:
1267 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001268 case KindMultiNetwork:
1269 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001270 case KindAuth:
1271 return []string{}
1272 case KindSSHKey:
1273 return []string{}
1274 case KindNumber:
1275 return []string{}
1276 case KindArrayString:
1277 return []string{}
1278 case KindPort:
1279 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001280 case KindVPNAuthKey:
1281 return []string{}
giof6ad2982024-08-23 17:42:49 +04001282 case KindCluster:
1283 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001284 default:
1285 panic("MUST NOT REACH!")
1286 }
1287}
1288
1289func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1290 for p, r := range ports {
1291 if err := setPortField(values, p, r.Port); err != nil {
1292 return err
1293 }
1294 }
1295 return nil
1296}
1297
1298func setPortField(values map[string]any, field string, port int) error {
1299 f := strings.SplitN(field, ".", 2)
1300 if len(f) == 2 {
1301 var sub map[string]any
1302 if s, ok := values[f[0]]; ok {
1303 sub, ok = s.(map[string]any)
1304 if !ok {
1305 return fmt.Errorf("expected map")
1306 }
1307 } else {
1308 sub = map[string]any{}
1309 values[f[0]] = sub
1310 }
1311 if err := setPortField(sub, f[1], port); err != nil {
1312 return err
1313 }
1314 } else {
1315 values[f[0]] = port
1316 }
1317 return nil
1318}
giof6ad2982024-08-23 17:42:49 +04001319
1320type Cluster struct {
1321 Name string `json:"name"`
1322 Kubeconfig string `json:"kubeconfig"`
1323 IngressClassName string `json:"ingressClassName"`
1324}
1325
1326func ClusterStateToAccessConfig(c cluster.State) Cluster {
1327 return Cluster{
1328 Name: c.Name,
1329 Kubeconfig: c.Kubeconfig,
1330 IngressClassName: c.IngressClassName,
1331 }
1332}
1333
1334func ToAccessConfigs(clusters []cluster.State) []Cluster {
1335 ret := make([]Cluster, 0, len(clusters))
1336 for _, c := range clusters {
1337 ret = append(ret, ClusterStateToAccessConfig(c))
1338 }
1339 return ret
1340}