blob: eee48410bcdc4cdec7f82aeb5c57fb19a5b0347b [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 {
gio3af43942024-04-16 08:13:50 +0400225 for _, p := range ports {
gio721c0042025-04-03 11:56:36 +0400226 var target string
227 if p.Cluster == "" {
giof4344632025-04-08 20:04:35 +0400228 if p.Service.Namespace == "" {
229 target = fmt.Sprintf("%s/%s", ns, p.Service.Name)
230 } else {
231 target = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
232 }
gio721c0042025-04-03 11:56:36 +0400233 } else {
234 target = p.Service.Name
235 }
gio3af43942024-04-16 08:13:50 +0400236 var buf bytes.Buffer
237 req := allocatePortReq{
238 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400239 SourcePort: p.Port,
gio721c0042025-04-03 11:56:36 +0400240 TargetService: target,
gio802311e2024-11-04 08:37:34 +0400241 TargetPort: p.Service.Port,
gio3af43942024-04-16 08:13:50 +0400242 }
gioefa0ed42024-06-13 12:31:43 +0400243 allocator := ""
244 for n, r := range reservations {
gio802311e2024-11-04 08:37:34 +0400245 if p.Port == r.Port {
gioefa0ed42024-06-13 12:31:43 +0400246 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400247 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400248 break
249 }
250 }
251 if allocator == "" {
gio802311e2024-11-04 08:37:34 +0400252 return fmt.Errorf("Could not find allocator for: %d", p.Port)
gioefa0ed42024-06-13 12:31:43 +0400253 }
giobd7ab0b2024-06-17 12:55:17 +0400254 if err := json.NewEncoder(&buf).Encode(req); err != nil {
255 return err
256 }
gioefa0ed42024-06-13 12:31:43 +0400257 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400258 if err != nil {
259 return err
260 }
261 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400262 var r bytes.Buffer
263 io.Copy(&r, resp.Body)
gio802311e2024-11-04 08:37:34 +0400264 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 +0400265 }
266 }
267 return nil
268}
269
gio802311e2024-11-04 08:37:34 +0400270func closePorts(ports []PortForward, ns string) error {
giocdfa3722024-06-13 20:10:14 +0400271 var retErr error
272 for _, p := range ports {
273 var buf bytes.Buffer
giof4344632025-04-08 20:04:35 +0400274 var fullName string
275 if p.Service.Namespace == "" {
276 fullName = fmt.Sprintf("%s/%s", ns, p.Service.Name)
277 } else {
278 fullName = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
279 }
giocdfa3722024-06-13 20:10:14 +0400280 req := removePortReq{
281 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400282 SourcePort: p.Port,
giof4344632025-04-08 20:04:35 +0400283 TargetService: fullName,
gio802311e2024-11-04 08:37:34 +0400284 TargetPort: p.Service.Port,
giocdfa3722024-06-13 20:10:14 +0400285 }
286 if err := json.NewEncoder(&buf).Encode(req); err != nil {
287 retErr = err
288 continue
289 }
giod78896a2025-04-10 07:42:13 +0400290 resp, err := http.Post(p.Network.DeallocatePortAddr, "application/json", &buf)
giocdfa3722024-06-13 20:10:14 +0400291 if err != nil {
292 retErr = err
293 continue
294 }
295 if resp.StatusCode != http.StatusOK {
gio802311e2024-11-04 08:37:34 +0400296 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.Port, resp.StatusCode)
giocdfa3722024-06-13 20:10:14 +0400297 continue
298 }
299 }
300 return retErr
301}
302
gioe72b54f2024-04-22 10:44:41 +0400303func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400304 for p := filepath.Clean(path); p != "/"; {
305 parent, child := filepath.Split(p)
gio92116ca2024-10-06 13:55:46 +0400306 kustPath := filepath.Join(parent, kustomizationFileName)
gioe72b54f2024-04-22 10:44:41 +0400307 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400308 if err != nil {
309 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400310 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400311 kust = &k
312 } else {
313 return err
314 }
315 }
316 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400317 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400318 return err
319 }
320 p = filepath.Clean(parent)
321 }
322 return nil
323}
324
gio778577f2024-04-29 09:44:38 +0400325type Resource struct {
giof8acc612025-04-26 08:20:55 +0400326 Id string `json:"id"`
giob4a3a192024-08-19 09:55:47 +0400327 Name string `json:"name"`
328 Namespace string `json:"namespace"`
329 Info string `json:"info"`
330 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400331}
332
333type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400334 Release Release
335 Helm []Resource
gio6ce44812025-05-17 07:31:54 +0400336 Access []Access
gio94904702024-07-26 16:58:34 +0400337 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400338}
339
gio3cdee592024-04-17 10:15:56 +0400340// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400341func installApp(
gioe72b54f2024-04-22 10:44:41 +0400342 repo soft.RepoIO,
343 appDir string,
344 name string,
345 config any,
gioe72b54f2024-04-22 10:44:41 +0400346 resources CueAppData,
347 data CueAppData,
giof8843412024-05-22 16:38:05 +0400348 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400349) error {
giof8843412024-05-22 16:38:05 +0400350 var o installOptions
351 for _, i := range opts {
352 i(&o)
353 }
354 dopts := []soft.DoOption{}
355 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400356 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
357 }
gio94904702024-07-26 16:58:34 +0400358 if o.NoPull {
359 dopts = append(dopts, soft.WithNoPull())
360 }
giof8843412024-05-22 16:38:05 +0400361 if o.NoPublish {
362 dopts = append(dopts, soft.WithNoCommit())
363 }
giof71a0832024-06-27 14:45:45 +0400364 if o.Force {
365 dopts = append(dopts, soft.WithForce())
366 }
gio9d66f322024-07-06 13:45:10 +0400367 if o.NoLock {
368 dopts = append(dopts, soft.WithNoLock())
369 }
giob4a3a192024-08-19 09:55:47 +0400370 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
giof6ad2982024-08-23 17:42:49 +0400371 if err := r.RemoveAll(appDir); err != nil {
gio308105e2024-04-19 13:12:13 +0400372 return "", err
373 }
374 resourcesDir := path.Join(appDir, "resources")
375 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400376 return "", err
377 }
gio94904702024-07-26 16:58:34 +0400378 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400379 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400380 return err
gio5e49bb62024-07-20 10:43:19 +0400381 }
gioe72b54f2024-04-22 10:44:41 +0400382 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400383 return err
gio3af43942024-04-16 08:13:50 +0400384 }
gioe72b54f2024-04-22 10:44:41 +0400385 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400386 return err
gio308105e2024-04-19 13:12:13 +0400387 }
gioe72b54f2024-04-22 10:44:41 +0400388 for name, contents := range data {
gio92116ca2024-10-06 13:55:46 +0400389 if name == "config.json" || name == kustomizationFileName || name == "resources" {
gio94904702024-07-26 16:58:34 +0400390 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400391 }
392 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400393 if err != nil {
gio94904702024-07-26 16:58:34 +0400394 return err
gio3af43942024-04-16 08:13:50 +0400395 }
gio308105e2024-04-19 13:12:13 +0400396 defer w.Close()
397 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400398 return err
gio3af43942024-04-16 08:13:50 +0400399 }
400 }
gio94904702024-07-26 16:58:34 +0400401 return nil
402 }(); err != nil {
403 return "", err
gio308105e2024-04-19 13:12:13 +0400404 }
gio94904702024-07-26 16:58:34 +0400405 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400406 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400407 return err
gio308105e2024-04-19 13:12:13 +0400408 }
gioefa0ed42024-06-13 12:31:43 +0400409 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400410 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400411 appKust.AddResources(name)
412 w, err := r.Writer(path.Join(resourcesDir, name))
413 if err != nil {
gio94904702024-07-26 16:58:34 +0400414 return err
gio308105e2024-04-19 13:12:13 +0400415 }
416 defer w.Close()
417 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400418 return err
gio308105e2024-04-19 13:12:13 +0400419 }
420 }
gio92116ca2024-10-06 13:55:46 +0400421 if err := soft.WriteYaml(r, path.Join(resourcesDir, kustomizationFileName), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400422 return err
gio3af43942024-04-16 08:13:50 +0400423 }
gio94904702024-07-26 16:58:34 +0400424 return nil
425 }(); err != nil {
426 return "", err
gio3af43942024-04-16 08:13:50 +0400427 }
gioe72b54f2024-04-22 10:44:41 +0400428 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400429 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400430 return err
gio3af43942024-04-16 08:13:50 +0400431}
432
gio3cdee592024-04-17 10:15:56 +0400433// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400434func (m *AppManager) Install(
435 app EnvApp,
436 instanceId string,
437 appDir string,
438 namespace string,
439 values map[string]any,
440 opts ...InstallOption,
441) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400442 o := &installOptions{}
443 for _, i := range opts {
444 i(o)
445 }
446 if !o.NoLock {
447 m.l.Lock()
448 defer m.l.Unlock()
449 }
gioefa0ed42024-06-13 12:31:43 +0400450 portFields := findPortFields(app.Schema())
451 fakeReservations := map[string]reservePortResp{}
452 for i, f := range portFields {
453 fakeReservations[f] = reservePortResp{Port: i}
454 }
455 if err := setPortFields(values, fakeReservations); err != nil {
456 return ReleaseResources{}, err
457 }
gio3af43942024-04-16 08:13:50 +0400458 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400459 if !o.NoPull {
giof6ad2982024-08-23 17:42:49 +0400460 if err := m.repo.Pull(); err != nil {
gio94904702024-07-26 16:58:34 +0400461 return ReleaseResources{}, err
462 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400463 }
gio94904702024-07-26 16:58:34 +0400464 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400465 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400466 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400467 }
gio0eaf2712024-04-14 13:08:46 +0400468 var env EnvConfig
469 if o.Env != nil {
470 env = *o.Env
471 } else {
472 var err error
473 env, err = m.Config()
474 if err != nil {
475 return ReleaseResources{}, err
476 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400477 }
giocb34ad22024-07-11 08:01:13 +0400478 var networks []Network
479 if o.Networks != nil {
480 networks = o.Networks
481 } else {
482 var err error
483 networks, err = m.CreateNetworks(env)
484 if err != nil {
485 return ReleaseResources{}, err
486 }
487 }
giof15b9da2024-09-19 06:59:16 +0400488 var clusters []Cluster
489 if o.Clusters != nil {
490 clusters = o.Clusters
491 } else {
492 if cls, err := m.GetClusters(); err != nil {
493 return ReleaseResources{}, err
494 } else {
495 clusters = ToAccessConfigs(cls)
496 }
giof6ad2982024-08-23 17:42:49 +0400497 }
giof8843412024-05-22 16:38:05 +0400498 var lg LocalChartGenerator
499 if o.LG != nil {
500 lg = o.LG
501 } else {
502 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
503 }
gio3cdee592024-04-17 10:15:56 +0400504 release := Release{
505 AppInstanceId: instanceId,
506 Namespace: namespace,
giof6ad2982024-08-23 17:42:49 +0400507 RepoAddr: m.repo.FullAddress(),
gio3cdee592024-04-17 10:15:56 +0400508 AppDir: appDir,
509 }
giof15b9da2024-09-19 06:59:16 +0400510 rendered, err := app.Render(release, env, networks, clusters, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400511 if err != nil {
gio778577f2024-04-29 09:44:38 +0400512 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400513 }
gio721c0042025-04-03 11:56:36 +0400514 reservators := map[string]reservePortInfo{}
gioefa0ed42024-06-13 12:31:43 +0400515 allocators := map[string]string{}
516 for _, pf := range rendered.Ports {
gio721c0042025-04-03 11:56:36 +0400517 reservators[portFields[pf.Port]] = reservePortInfo{
giod78896a2025-04-10 07:42:13 +0400518 reserveAddr: pf.Network.ReservePortAddr,
gio721c0042025-04-03 11:56:36 +0400519 RemoteProxy: pf.Cluster != "",
520 }
giod78896a2025-04-10 07:42:13 +0400521 allocators[portFields[pf.Port]] = pf.Network.AllocatePortAddr
gioefa0ed42024-06-13 12:31:43 +0400522 }
523 portReservations, err := reservePorts(reservators)
524 if err != nil {
525 return ReleaseResources{}, err
526 }
527 if err := setPortFields(values, portReservations); err != nil {
528 return ReleaseResources{}, err
529 }
gio7841f4f2024-07-26 19:53:49 +0400530 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400531 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
532 if o.FetchContainerImages {
533 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
534 return ReleaseResources{}, err
535 }
gio0eaf2712024-04-14 13:08:46 +0400536 }
giof6ad2982024-08-23 17:42:49 +0400537 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
giof71a0832024-06-27 14:45:45 +0400538 if err != nil {
giof8843412024-05-22 16:38:05 +0400539 return ReleaseResources{}, err
540 }
giof71a0832024-06-27 14:45:45 +0400541 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400542 if o.FetchContainerImages {
543 release.ImageRegistry = imageRegistry
544 }
giof15b9da2024-09-19 06:59:16 +0400545 rendered, err = app.Render(release, env, networks, clusters, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400546 if err != nil {
547 return ReleaseResources{}, err
548 }
giof6ad2982024-08-23 17:42:49 +0400549 for _, ns := range rendered.Namespaces {
550 if ns.Name == "" {
551 return ReleaseResources{}, fmt.Errorf("namespace name missing")
552 }
553 if ns.Kubeconfig == "" {
554 continue
555 }
556 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
557 if err != nil {
558 return ReleaseResources{}, err
559 }
560 if err := nsc.Create(ns.Name); err != nil {
561 return ReleaseResources{}, err
562 }
563 }
564 if err := installApp(m.repo, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400565 return ReleaseResources{}, err
566 }
gioff2a29a2024-05-01 17:06:42 +0400567 // TODO(gio): add ingress-nginx to release resources
gio802311e2024-11-04 08:37:34 +0400568 if err := openPorts(rendered.Ports, portReservations, allocators, release.Namespace); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400569 return ReleaseResources{}, err
570 }
giof6ad2982024-08-23 17:42:49 +0400571 for _, p := range rendered.ClusterProxies {
gio721c0042025-04-03 11:56:36 +0400572 if err := m.cnc.AddIngressProxy(p.From, p.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400573 return ReleaseResources{}, err
574 }
575 }
gio778577f2024-04-29 09:44:38 +0400576 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400577 Release: rendered.Config.Release,
578 RenderedRaw: rendered.Raw,
gio6ce44812025-05-17 07:31:54 +0400579 Access: rendered.Access,
gio94904702024-07-26 16:58:34 +0400580 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400581 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400582}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400583
gio778577f2024-04-29 09:44:38 +0400584type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400585 Metadata struct {
586 Name string `json:"name"`
587 Namespace string `json:"namespace"`
588 Annotations map[string]string `json:"annotations"`
589 } `json:"metadata"`
590 Kind string `json:"kind"`
591 Status struct {
gio778577f2024-04-29 09:44:38 +0400592 Conditions []struct {
593 Type string `json:"type"`
594 Status string `json:"status"`
595 } `json:"conditions"`
596 } `json:"status,omitempty"`
597}
598
599func extractHelm(resources CueAppData) []Resource {
600 ret := make([]Resource, 0, len(resources))
601 for _, contents := range resources {
602 var h helmRelease
603 if err := yaml.Unmarshal(contents, &h); err != nil {
604 panic(err) // TODO(gio): handle
605 }
gio0eaf2712024-04-14 13:08:46 +0400606 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400607 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400608 Name: h.Metadata.Name,
609 Namespace: h.Metadata.Namespace,
610 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
giof8acc612025-04-26 08:20:55 +0400611 Annotations: h.Metadata.Annotations,
giof9f0bee2024-06-11 20:10:05 +0400612 }
613 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400614 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400615 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
616 if ok && len(info) != 0 {
617 res.Info = info
618 }
giof8acc612025-04-26 08:20:55 +0400619 id, ok := h.Metadata.Annotations["dodo.cloud/id"]
620 if ok && len(id) != 0 {
621 res.Id = id
622 }
giof9f0bee2024-06-11 20:10:05 +0400623 }
624 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400625 }
gio778577f2024-04-29 09:44:38 +0400626 }
627 return ret
628}
629
giof8843412024-05-22 16:38:05 +0400630// TODO(gio): take app configuration from the repo
631func (m *AppManager) Update(
632 instanceId string,
633 values map[string]any,
gio63a1a822025-04-23 12:59:40 +0400634 // TODO(gio): this should not be cue specific
635 overrides CueAppData,
giof8843412024-05-22 16:38:05 +0400636 opts ...InstallOption,
637) (ReleaseResources, error) {
giodbabb102025-06-18 14:22:53 +0400638 o := &installOptions{}
639 for _, i := range opts {
640 i(o)
641 }
642 if !o.NoLock {
643 m.l.Lock()
644 defer m.l.Unlock()
645 }
gio838bcb82025-05-15 19:39:04 +0400646 if values == nil {
647 values = map[string]any{}
648 }
giof6ad2982024-08-23 17:42:49 +0400649 if err := m.repo.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400650 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400651 }
gio3cdee592024-04-17 10:15:56 +0400652 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400653 if err != nil {
gio778577f2024-04-29 09:44:38 +0400654 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400655 }
gio308105e2024-04-19 13:12:13 +0400656 instanceDir := filepath.Join(m.appDirRoot, instanceId)
gio838bcb82025-05-15 19:39:04 +0400657 oldApp, err := m.GetInstanceApp(instanceId, nil)
giof8843412024-05-22 16:38:05 +0400658 if err != nil {
659 return ReleaseResources{}, err
660 }
gio838bcb82025-05-15 19:39:04 +0400661 newApp, err := m.GetInstanceApp(instanceId, overrides)
662 if err != nil {
663 return ReleaseResources{}, err
664 }
665 oldPorts := findPortFields(oldApp.Schema())
666 newPorts := findPortFields(newApp.Schema())
667 portFields := []string{}
668 for _, np := range newPorts {
669 if !slices.Contains(oldPorts, np) {
670 portFields = append(portFields, np)
671 }
672 }
673 fakeReservations := map[string]reservePortResp{}
674 for i, f := range portFields {
675 fakeReservations[f] = reservePortResp{Port: i}
676 }
677 if err := setPortFields(values, fakeReservations); err != nil {
678 return ReleaseResources{}, err
679 }
gio308105e2024-04-19 13:12:13 +0400680 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400681 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400682 if err != nil {
gio778577f2024-04-29 09:44:38 +0400683 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400684 }
giof6ad2982024-08-23 17:42:49 +0400685 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400686 if err != nil {
687 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400688 }
giocb34ad22024-07-11 08:01:13 +0400689 networks, err := m.CreateNetworks(env)
690 if err != nil {
691 return ReleaseResources{}, err
692 }
giof6ad2982024-08-23 17:42:49 +0400693 clusters, err := m.GetClusters()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400694 if err != nil {
gio778577f2024-04-29 09:44:38 +0400695 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400696 }
giodbabb102025-06-18 14:22:53 +0400697 var lg LocalChartGenerator
698 if o.LG != nil {
699 lg = o.LG
700 } else {
701 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
702 }
703 rendered, err := newApp.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), nil, m.vpnAPIClient)
gio838bcb82025-05-15 19:39:04 +0400704 if err != nil {
705 return ReleaseResources{}, err
706 }
707 reservators := map[string]reservePortInfo{}
708 allocators := map[string]string{}
709 for _, pf := range rendered.Ports {
710 found := false
711 for _, fr := range fakeReservations {
712 if fr.Port == pf.Port {
713 found = true
714 }
715 }
716 if !found {
717 continue
718 }
719 reservators[portFields[pf.Port]] = reservePortInfo{
720 reserveAddr: pf.Network.ReservePortAddr,
721 RemoteProxy: pf.Cluster != "",
722 }
723 allocators[portFields[pf.Port]] = pf.Network.AllocatePortAddr
724 }
725 portReservations, err := reservePorts(reservators)
726 if err != nil {
727 return ReleaseResources{}, err
728 }
729 if err := setPortFields(values, portReservations); err != nil {
730 return ReleaseResources{}, err
731 }
giodbabb102025-06-18 14:22:53 +0400732 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
733 if err != nil {
734 return ReleaseResources{}, err
735 }
736 localCharts := generateLocalCharts(lg, charts)
737 rendered, err = newApp.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), localCharts, m.vpnAPIClient)
giof6ad2982024-08-23 17:42:49 +0400738 if err != nil {
gio94904702024-07-26 16:58:34 +0400739 return ReleaseResources{}, err
740 }
giof6ad2982024-08-23 17:42:49 +0400741 for _, ns := range rendered.Namespaces {
742 if ns.Name == "" {
743 return ReleaseResources{}, fmt.Errorf("namespace name missing")
744 }
745 if ns.Kubeconfig == "" {
746 continue
747 }
748 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
749 if err != nil {
750 return ReleaseResources{}, err
751 }
752 if err := nsc.Create(ns.Name); err != nil {
753 return ReleaseResources{}, err
754 }
755 }
756 if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
757 return ReleaseResources{}, err
758 }
gio838bcb82025-05-15 19:39:04 +0400759 toOpen := []PortForward{}
760 for _, op := range rendered.Ports {
761 found := false
762 for _, rp := range portReservations {
763 if rp.Port == op.Port {
764 found = true
765 break
766 }
767 }
giob58cd052025-05-19 16:33:08 +0400768 if found {
gio838bcb82025-05-15 19:39:04 +0400769 toOpen = append(toOpen, op)
770 }
771 }
772 if err := openPorts(toOpen, portReservations, allocators, config.Release.Namespace); err != nil {
773 return ReleaseResources{}, err
774 }
giof6ad2982024-08-23 17:42:49 +0400775 for _, ocp := range renderedCfg.Out.ClusterProxy {
776 found := false
777 for _, ncp := range rendered.ClusterProxies {
778 if ocp == ncp {
779 found = true
780 break
781 }
782 }
783 if !found {
gio721c0042025-04-03 11:56:36 +0400784 if err := m.cnc.RemoveIngressProxy(ocp.From, ocp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400785 return ReleaseResources{}, err
786 }
787 }
788 }
789 for _, ncp := range rendered.ClusterProxies {
790 found := false
791 for _, ocp := range renderedCfg.Out.ClusterProxy {
792 if ocp == ncp {
793 found = true
794 break
795 }
796 }
797 if !found {
gio721c0042025-04-03 11:56:36 +0400798 if err := m.cnc.AddIngressProxy(ncp.From, ncp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400799 return ReleaseResources{}, err
800 }
801 }
802 }
gio94904702024-07-26 16:58:34 +0400803 return ReleaseResources{
804 Release: rendered.Config.Release,
805 RenderedRaw: rendered.Raw,
gio6ce44812025-05-17 07:31:54 +0400806 Access: rendered.Access,
gio94904702024-07-26 16:58:34 +0400807 Helm: extractHelm(rendered.Resources),
808 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400809}
810
811func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400812 m.l.Lock()
813 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400814 if err := m.repo.Pull(); err != nil {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400815 return err
816 }
gio864b4332024-09-05 13:56:47 +0400817 var cfg renderedInstance
giof6ad2982024-08-23 17:42:49 +0400818 if _, err := m.repo.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400819 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof6ad2982024-08-23 17:42:49 +0400820 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giocdfa3722024-06-13 20:10:14 +0400821 if err != nil {
822 return "", err
823 }
gio864b4332024-09-05 13:56:47 +0400824 cfg = renderedCfg
giof6ad2982024-08-23 17:42:49 +0400825 r.RemoveAll(instanceDir)
gio5887caa2024-10-03 15:07:23 +0400826 curr := instanceDir
gio829b1b72024-10-05 21:50:56 +0400827 for {
gio5887caa2024-10-03 15:07:23 +0400828 p := filepath.Dir(curr)
gio829b1b72024-10-05 21:50:56 +0400829 if p == curr {
830 break
831 }
gio5887caa2024-10-03 15:07:23 +0400832 n := filepath.Base(curr)
gio92116ca2024-10-06 13:55:46 +0400833 kustPath := filepath.Join(p, kustomizationFileName)
gio5887caa2024-10-03 15:07:23 +0400834 kust, err := soft.ReadKustomization(r, kustPath)
835 if err != nil {
836 return "", err
837 }
838 kust.RemoveResources(n)
gio829b1b72024-10-05 21:50:56 +0400839 if len(kust.Resources) > 0 || p == m.appDirRoot {
gio5887caa2024-10-03 15:07:23 +0400840 soft.WriteYaml(r, kustPath, kust)
841 break
842 } else {
843 if err := r.RemoveAll(kustPath); err != nil {
844 return "", err
845 }
846 }
gio5887caa2024-10-03 15:07:23 +0400847 curr = p
gio3af43942024-04-16 08:13:50 +0400848 }
gio3af43942024-04-16 08:13:50 +0400849 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400850 }); err != nil {
851 return err
852 }
gio802311e2024-11-04 08:37:34 +0400853 if err := closePorts(cfg.Output.PortForward, cfg.Release.Namespace); err != nil {
giocdfa3722024-06-13 20:10:14 +0400854 return err
855 }
giof6ad2982024-08-23 17:42:49 +0400856 for _, cp := range cfg.Out.ClusterProxy {
gio721c0042025-04-03 11:56:36 +0400857 if err := m.cnc.RemoveIngressProxy(cp.From, cp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400858 return err
859 }
860 }
gio864b4332024-09-05 13:56:47 +0400861 for vmName, vmCfg := range cfg.Out.VM {
862 if vmCfg.VPN.Enabled {
gio92116ca2024-10-06 13:55:46 +0400863 // Not found error is ignored as VM might have not had enough time to boot before uninstalling it.
864 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400865 return err
866 }
867 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
868 return err
869 }
gio92116ca2024-10-06 13:55:46 +0400870 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400871 return err
872 }
873 }
874 }
giocdfa3722024-06-13 20:10:14 +0400875 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400876}
877
giocb34ad22024-07-11 08:01:13 +0400878func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
879 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400880 {
giocdfa3722024-06-13 20:10:14 +0400881 Name: "Public",
882 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
883 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
884 Domain: env.Domain,
885 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
886 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
887 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400888 },
gio7841f4f2024-07-26 19:53:49 +0400889 }
890 if env.PrivateDomain != "" {
891 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400892 Name: "Private",
893 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
894 Domain: env.PrivateDomain,
895 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
896 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
897 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400898 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400899 }
gio7fbd4ad2024-08-27 10:06:39 +0400900 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400901 if err != nil {
902 return nil, err
903 }
904 for _, a := range n {
905 ret = append(ret, Network{
906 Name: a.Input["name"].(string),
907 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
908 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
909 Domain: a.Input["domain"].(string),
910 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
911 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
912 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
913 })
914 }
915 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400916}
gio3cdee592024-04-17 10:15:56 +0400917
giof6ad2982024-08-23 17:42:49 +0400918func (m *AppManager) GetClusters() ([]cluster.State, error) {
919 ret := []cluster.State{
920 {
921 Name: "default",
922 },
923 }
924 files, err := m.repo.ListDir("/clusters")
925 if err != nil {
926 if errors.Is(err, fs.ErrNotExist) {
927 return ret, nil
928 }
929 return nil, err
930 }
931 for _, f := range files {
932 if !f.IsDir() {
933 continue
934 }
935 cfgPath := filepath.Clean(filepath.Join("/clusters", f.Name(), "config.json"))
936 var c cluster.State
937 if err := soft.ReadJson(m.repo, cfgPath, &c); err != nil {
938 if errors.Is(err, fs.ErrNotExist) {
939 continue
940 }
941 return nil, err
942 }
943 ret = append(ret, c)
944 }
945 return ret, nil
946}
947
gio0eaf2712024-04-14 13:08:46 +0400948type installOptions struct {
gio94904702024-07-26 16:58:34 +0400949 NoPull bool
giof8843412024-05-22 16:38:05 +0400950 NoPublish bool
951 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400952 Networks []Network
giof15b9da2024-09-19 06:59:16 +0400953 Clusters []Cluster
giof8843412024-05-22 16:38:05 +0400954 Branch string
955 LG LocalChartGenerator
956 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400957 Force bool
gio9d66f322024-07-06 13:45:10 +0400958 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400959}
960
961type InstallOption func(*installOptions)
962
963func WithConfig(env *EnvConfig) InstallOption {
964 return func(o *installOptions) {
965 o.Env = env
966 }
967}
968
giocb34ad22024-07-11 08:01:13 +0400969func WithNetworks(networks []Network) InstallOption {
970 return func(o *installOptions) {
971 o.Networks = networks
972 }
973}
974
gio23bdc1b2024-07-11 16:07:47 +0400975func WithNoNetworks() InstallOption {
976 return WithNetworks([]Network{})
977}
978
giof15b9da2024-09-19 06:59:16 +0400979func WithClusters(clusters []Cluster) InstallOption {
980 return func(o *installOptions) {
981 o.Clusters = clusters
982 }
983}
984
gio0eaf2712024-04-14 13:08:46 +0400985func WithBranch(branch string) InstallOption {
986 return func(o *installOptions) {
987 o.Branch = branch
988 }
989}
990
giof71a0832024-06-27 14:45:45 +0400991func WithForce() InstallOption {
992 return func(o *installOptions) {
993 o.Force = true
994 }
995}
996
giof8843412024-05-22 16:38:05 +0400997func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
998 return func(o *installOptions) {
999 o.LG = lg
1000 }
1001}
1002
1003func WithFetchContainerImages() InstallOption {
1004 return func(o *installOptions) {
1005 o.FetchContainerImages = true
1006 }
1007}
1008
1009func WithNoPublish() InstallOption {
1010 return func(o *installOptions) {
1011 o.NoPublish = true
1012 }
1013}
1014
gio94904702024-07-26 16:58:34 +04001015func WithNoPull() InstallOption {
1016 return func(o *installOptions) {
1017 o.NoPull = true
1018 }
1019}
1020
gio9d66f322024-07-06 13:45:10 +04001021func WithNoLock() InstallOption {
1022 return func(o *installOptions) {
1023 o.NoLock = true
1024 }
1025}
1026
giof8843412024-05-22 16:38:05 +04001027// InfraAppmanager
1028
1029type InfraAppManager struct {
1030 repoIO soft.RepoIO
1031 nsc NamespaceCreator
1032 hf HelmFetcher
1033 lg LocalChartGenerator
1034}
1035
1036func NewInfraAppManager(
1037 repoIO soft.RepoIO,
1038 nsc NamespaceCreator,
1039 hf HelmFetcher,
1040 lg LocalChartGenerator,
1041) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +04001042 return &InfraAppManager{
1043 repoIO,
giof8843412024-05-22 16:38:05 +04001044 nsc,
1045 hf,
1046 lg,
gio3cdee592024-04-17 10:15:56 +04001047 }, nil
1048}
1049
1050func (m *InfraAppManager) Config() (InfraConfig, error) {
1051 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +04001052 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +04001053 return InfraConfig{}, err
1054 } else {
1055 return cfg, nil
1056 }
1057}
1058
gioe72b54f2024-04-22 10:44:41 +04001059func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
1060 var cfg InfraAppInstanceConfig
1061 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
1062 return InfraAppInstanceConfig{}, err
1063 } else {
1064 return cfg, nil
1065 }
1066}
1067
1068func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +04001069 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", kustomizationFileName))
gioe72b54f2024-04-22 10:44:41 +04001070 if err != nil {
1071 return InfraAppInstanceConfig{}, err
1072 }
1073 for _, app := range kust.Resources {
1074 if app == id {
1075 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
1076 if err != nil {
1077 return InfraAppInstanceConfig{}, err
1078 }
1079 cfg.Id = id
1080 return cfg, nil
1081 }
1082 }
1083 return InfraAppInstanceConfig{}, nil
1084}
1085
gio778577f2024-04-29 09:44:38 +04001086func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +04001087 appDir = filepath.Clean(appDir)
1088 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001089 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001090 }
giof8843412024-05-22 16:38:05 +04001091 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +04001092 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001093 }
1094 infra, err := m.Config()
1095 if err != nil {
gio778577f2024-04-29 09:44:38 +04001096 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001097 }
1098 release := Release{
1099 Namespace: namespace,
1100 RepoAddr: m.repoIO.FullAddress(),
1101 AppDir: appDir,
1102 }
gio7841f4f2024-07-26 19:53:49 +04001103 networks := m.CreateNetworks(infra)
1104 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +04001105 if err != nil {
1106 return ReleaseResources{}, err
1107 }
giof71a0832024-06-27 14:45:45 +04001108 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
1109 if err != nil {
giof8843412024-05-22 16:38:05 +04001110 return ReleaseResources{}, err
1111 }
giof71a0832024-06-27 14:45:45 +04001112 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +04001113 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +04001114 if err != nil {
gio778577f2024-04-29 09:44:38 +04001115 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001116 }
gio94904702024-07-26 16:58:34 +04001117 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
1118 return ReleaseResources{}, err
1119 }
1120 return ReleaseResources{
1121 Release: rendered.Config.Release,
1122 RenderedRaw: rendered.Raw,
1123 Helm: extractHelm(rendered.Resources),
1124 }, nil
gioe72b54f2024-04-22 10:44:41 +04001125}
1126
giof8843412024-05-22 16:38:05 +04001127// TODO(gio): take app configuration from the repo
1128func (m *InfraAppManager) Update(
1129 instanceId string,
1130 values map[string]any,
1131 opts ...InstallOption,
1132) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +04001133 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001134 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001135 }
gio7841f4f2024-07-26 19:53:49 +04001136 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +04001137 if err != nil {
gio778577f2024-04-29 09:44:38 +04001138 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001139 }
1140 instanceDir := filepath.Join("/infrastructure", instanceId)
gio63a1a822025-04-23 12:59:40 +04001141 appCfg, err := GetCueAppData(m.repoIO, instanceDir, nil)
giof8843412024-05-22 16:38:05 +04001142 if err != nil {
1143 return ReleaseResources{}, err
1144 }
1145 app, err := NewCueInfraApp(appCfg)
1146 if err != nil {
1147 return ReleaseResources{}, err
1148 }
gioe72b54f2024-04-22 10:44:41 +04001149 instanceConfigPath := filepath.Join(instanceDir, "config.json")
1150 config, err := m.appConfig(instanceConfigPath)
1151 if err != nil {
gio778577f2024-04-29 09:44:38 +04001152 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001153 }
giocdfa3722024-06-13 20:10:14 +04001154 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +04001155 if err != nil {
1156 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001157 }
gio7841f4f2024-07-26 19:53:49 +04001158 networks := m.CreateNetworks(infra)
1159 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +04001160 if err != nil {
gio778577f2024-04-29 09:44:38 +04001161 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001162 }
gio94904702024-07-26 16:58:34 +04001163 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
1164 return ReleaseResources{}, err
1165 }
1166 return ReleaseResources{
1167 Release: rendered.Config.Release,
1168 RenderedRaw: rendered.Raw,
1169 Helm: extractHelm(rendered.Resources),
1170 }, nil
gio3cdee592024-04-17 10:15:56 +04001171}
giof8843412024-05-22 16:38:05 +04001172
gio7841f4f2024-07-26 19:53:49 +04001173func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
1174 return []InfraNetwork{
1175 {
1176 Name: "Public",
1177 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
1178 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
1179 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
1180 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
1181 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
1182 },
1183 }
1184}
1185
giof8843412024-05-22 16:38:05 +04001186func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
1187 ret := make(map[string]string)
1188 for name, chart := range charts.Git {
1189 chartRoot := filepath.Join(root, name)
1190 ret[name] = chartRoot
1191 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
1192 return nil, err
1193 }
1194 }
1195 return ret, nil
1196}
1197
1198func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
1199 ret := make(map[string]helmv2.HelmChartTemplateSpec)
1200 for name, path := range charts {
1201 ret[name] = g.Generate(path)
1202 }
1203 return ret
1204}
1205
1206func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
1207 for _, img := range imgs {
1208 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
1209 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
1210 "skopeo",
1211 "--insecure-policy",
1212 "copy",
1213 "--dest-tls-verify=false", // TODO(gio): enable
1214 "--multi-arch=all",
1215 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
1216 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
1217 }); err != nil {
1218 return err
1219 }
1220 }
1221 return nil
1222}
1223
1224type renderedInstance struct {
gio802311e2024-11-04 08:37:34 +04001225 Release Release `json:"release"`
giof8843412024-05-22 16:38:05 +04001226 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
gio864b4332024-09-05 13:56:47 +04001227 Out outRendered `json:"out"`
gio802311e2024-11-04 08:37:34 +04001228 Output outputRendered `json:"output"`
1229}
1230
1231type outputRendered struct {
1232 PortForward []PortForward `json:"openPort"`
gio864b4332024-09-05 13:56:47 +04001233}
1234
1235type outRendered struct {
giof6ad2982024-08-23 17:42:49 +04001236 ClusterProxy map[string]ClusterProxy
1237 VM map[string]vmRendered `json:"vm"`
gio864b4332024-09-05 13:56:47 +04001238}
1239
1240type vmRendered struct {
1241 Username string `json:"username"`
1242 VPN struct {
1243 Enabled bool `json:"enabled"`
1244 AuthKey string `json:"authKey"`
1245 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +04001246}
1247
giocdfa3722024-06-13 20:10:14 +04001248func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +04001249 r, err := fs.Reader(path)
1250 if err != nil {
giocdfa3722024-06-13 20:10:14 +04001251 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001252 }
1253 defer r.Close()
1254 var cfg renderedInstance
1255 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +04001256 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001257 }
giocdfa3722024-06-13 20:10:14 +04001258 return cfg, nil
giof8843412024-05-22 16:38:05 +04001259}
gioefa0ed42024-06-13 12:31:43 +04001260
1261func findPortFields(scm Schema) []string {
1262 switch scm.Kind() {
1263 case KindBoolean:
1264 return []string{}
1265 case KindInt:
1266 return []string{}
1267 case KindString:
1268 return []string{}
1269 case KindStruct:
1270 ret := []string{}
1271 for _, f := range scm.Fields() {
1272 for _, p := range findPortFields(f.Schema) {
1273 if p == "" {
1274 ret = append(ret, f.Name)
1275 } else {
1276 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1277 }
1278 }
1279 }
1280 return ret
1281 case KindNetwork:
1282 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001283 case KindMultiNetwork:
1284 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001285 case KindAuth:
1286 return []string{}
1287 case KindSSHKey:
1288 return []string{}
1289 case KindNumber:
1290 return []string{}
1291 case KindArrayString:
1292 return []string{}
1293 case KindPort:
1294 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001295 case KindVPNAuthKey:
1296 return []string{}
giof6ad2982024-08-23 17:42:49 +04001297 case KindCluster:
1298 return []string{}
gio6481c902025-05-20 16:16:30 +04001299 case KindPassword:
1300 return []string{}
gioe65d9a92025-06-19 09:02:32 +04001301 case KindSketchSessionId:
1302 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001303 default:
1304 panic("MUST NOT REACH!")
1305 }
1306}
1307
1308func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1309 for p, r := range ports {
1310 if err := setPortField(values, p, r.Port); err != nil {
1311 return err
1312 }
1313 }
1314 return nil
1315}
1316
1317func setPortField(values map[string]any, field string, port int) error {
1318 f := strings.SplitN(field, ".", 2)
1319 if len(f) == 2 {
1320 var sub map[string]any
1321 if s, ok := values[f[0]]; ok {
1322 sub, ok = s.(map[string]any)
1323 if !ok {
1324 return fmt.Errorf("expected map")
1325 }
1326 } else {
1327 sub = map[string]any{}
1328 values[f[0]] = sub
1329 }
1330 if err := setPortField(sub, f[1], port); err != nil {
1331 return err
1332 }
1333 } else {
1334 values[f[0]] = port
1335 }
1336 return nil
1337}
giof6ad2982024-08-23 17:42:49 +04001338
1339type Cluster struct {
1340 Name string `json:"name"`
1341 Kubeconfig string `json:"kubeconfig"`
1342 IngressClassName string `json:"ingressClassName"`
1343}
1344
1345func ClusterStateToAccessConfig(c cluster.State) Cluster {
1346 return Cluster{
1347 Name: c.Name,
1348 Kubeconfig: c.Kubeconfig,
1349 IngressClassName: c.IngressClassName,
1350 }
1351}
1352
1353func ToAccessConfigs(clusters []cluster.State) []Cluster {
1354 ret := make([]Cluster, 0, len(clusters))
1355 for _, c := range clusters {
1356 ret = append(ret, ClusterStateToAccessConfig(c))
1357 }
1358 return ret
1359}