blob: 7995a413c3404987e1e5043db20930e9639e9bb7 [file] [log] [blame]
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +04001package installer
2
3import (
gio3af43942024-04-16 08:13:50 +04004 "bytes"
5 "encoding/json"
6 "errors"
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +04007 "fmt"
gioefa0ed42024-06-13 12:31:43 +04008 "io"
gio3af43942024-04-16 08:13:50 +04009 "io/fs"
gio3af43942024-04-16 08:13:50 +040010 "net/http"
11 "path"
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +040012 "path/filepath"
giof8843412024-05-22 16:38:05 +040013 "strings"
gio69731e82024-08-01 14:15:55 +040014 "sync"
gioe72b54f2024-04-22 10:44:41 +040015
giof6ad2982024-08-23 17:42:49 +040016 "github.com/giolekva/pcloud/core/installer/cluster"
gioefa0ed42024-06-13 12:31:43 +040017 gio "github.com/giolekva/pcloud/core/installer/io"
giof6ad2982024-08-23 17:42:49 +040018 "github.com/giolekva/pcloud/core/installer/kube"
gioe72b54f2024-04-22 10:44:41 +040019 "github.com/giolekva/pcloud/core/installer/soft"
gio778577f2024-04-29 09:44:38 +040020
giof8843412024-05-22 16:38:05 +040021 helmv2 "github.com/fluxcd/helm-controller/api/v2"
gio778577f2024-04-29 09:44:38 +040022 "sigs.k8s.io/yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040023)
24
gio5e49bb62024-07-20 10:43:19 +040025const (
26 configFileName = "config.yaml"
27 kustomizationFileName = "kustomization.yaml"
28 gitIgnoreFileName = ".gitignore"
29 includeEverything = "!*"
30)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040031
gio778577f2024-04-29 09:44:38 +040032var ErrorNotFound = errors.New("not found")
33
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040034type AppManager struct {
gio864b4332024-09-05 13:56:47 +040035 l sync.Locker
giof6ad2982024-08-23 17:42:49 +040036 repo soft.RepoIO
gio864b4332024-09-05 13:56:47 +040037 nsc NamespaceCreator
38 jc JobCreator
39 hf HelmFetcher
40 vpnAPIClient VPNAPIClient
giof6ad2982024-08-23 17:42:49 +040041 cnc ClusterNetworkConfigurator
gio864b4332024-09-05 13:56:47 +040042 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040043}
44
giof8843412024-05-22 16:38:05 +040045func NewAppManager(
giof6ad2982024-08-23 17:42:49 +040046 repo soft.RepoIO,
giof8843412024-05-22 16:38:05 +040047 nsc NamespaceCreator,
48 jc JobCreator,
49 hf HelmFetcher,
gio864b4332024-09-05 13:56:47 +040050 vpnKeyGen VPNAPIClient,
giof6ad2982024-08-23 17:42:49 +040051 cnc ClusterNetworkConfigurator,
giof8843412024-05-22 16:38:05 +040052 appDirRoot string,
53) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040054 return &AppManager{
gio69731e82024-08-01 14:15:55 +040055 &sync.Mutex{},
giof6ad2982024-08-23 17:42:49 +040056 repo,
giof8843412024-05-22 16:38:05 +040057 nsc,
58 jc,
59 hf,
gio36b23b32024-08-25 12:20:54 +040060 vpnKeyGen,
giof6ad2982024-08-23 17:42:49 +040061 cnc,
gio308105e2024-04-19 13:12:13 +040062 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040063 }, nil
64}
65
gioe72b54f2024-04-22 10:44:41 +040066func (m *AppManager) Config() (EnvConfig, error) {
67 var cfg EnvConfig
giof6ad2982024-08-23 17:42:49 +040068 if err := soft.ReadYaml(m.repo, configFileName, &cfg); err != nil {
gioe72b54f2024-04-22 10:44:41 +040069 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040070 } else {
71 return cfg, nil
72 }
73}
74
gio3cdee592024-04-17 10:15:56 +040075func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
76 var cfg AppInstanceConfig
giof6ad2982024-08-23 17:42:49 +040077 if err := soft.ReadJson(m.repo, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040078 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040079 } else {
80 return cfg, nil
81 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040082}
83
giof8acc612025-04-26 08:20:55 +040084func (m *AppManager) AppRendered(instanceId string) ([]byte, error) {
85 path := filepath.Join(m.appDirRoot, instanceId, "rendered.json")
86 return soft.ReadFile(m.repo, path)
87}
88
gio7fbd4ad2024-08-27 10:06:39 +040089func (m *AppManager) GetAllInstances() ([]AppInstanceConfig, error) {
giof6ad2982024-08-23 17:42:49 +040090 m.repo.Pull()
gio92116ca2024-10-06 13:55:46 +040091 kust, err := soft.ReadKustomization(m.repo, filepath.Join(m.appDirRoot, kustomizationFileName))
gio3af43942024-04-16 08:13:50 +040092 if err != nil {
giof6ad2982024-08-23 17:42:49 +040093 if errors.Is(err, fs.ErrNotExist) {
94 return nil, nil
95 }
gio3af43942024-04-16 08:13:50 +040096 return nil, err
97 }
gio3cdee592024-04-17 10:15:56 +040098 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040099 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +0400100 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
101 if err != nil {
102 return nil, err
103 }
104 cfg.Id = app
105 ret = append(ret, cfg)
106 }
107 return ret, nil
108}
109
gio7fbd4ad2024-08-27 10:06:39 +0400110func (m *AppManager) GetAllAppInstances(name string) ([]AppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400111 kust, err := soft.ReadKustomization(m.repo, filepath.Join(m.appDirRoot, kustomizationFileName))
gio308105e2024-04-19 13:12:13 +0400112 if err != nil {
giocb34ad22024-07-11 08:01:13 +0400113 if errors.Is(err, fs.ErrNotExist) {
114 return nil, nil
115 } else {
116 return nil, err
117 }
gio308105e2024-04-19 13:12:13 +0400118 }
119 ret := make([]AppInstanceConfig, 0)
120 for _, app := range kust.Resources {
121 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400122 if err != nil {
123 return nil, err
124 }
125 cfg.Id = app
126 if cfg.AppId == name {
127 ret = append(ret, cfg)
128 }
129 }
130 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400131}
132
gio7fbd4ad2024-08-27 10:06:39 +0400133func (m *AppManager) GetInstance(id string) (*AppInstanceConfig, error) {
134 appDir := filepath.Clean(filepath.Join(m.appDirRoot, id))
135 cfgPath := filepath.Join(appDir, "config.json")
gio7fbd4ad2024-08-27 10:06:39 +0400136 cfg, err := m.appConfig(cfgPath)
gio3af43942024-04-16 08:13:50 +0400137 if err != nil {
gio778577f2024-04-29 09:44:38 +0400138 return nil, err
gio3af43942024-04-16 08:13:50 +0400139 }
gio7fbd4ad2024-08-27 10:06:39 +0400140 cfg.Id = id
141 return &cfg, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400142}
143
gio63a1a822025-04-23 12:59:40 +0400144func GetCueAppData(fs soft.RepoFS, dir string, overrides CueAppData) (CueAppData, error) {
giof8843412024-05-22 16:38:05 +0400145 files, err := fs.ListDir(dir)
146 if err != nil {
147 return nil, err
148 }
149 cfg := CueAppData{}
150 for _, f := range files {
151 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
152 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
153 if err != nil {
154 return nil, err
155 }
156 cfg[f.Name()] = contents
157 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400158 }
gio63a1a822025-04-23 12:59:40 +0400159 for k, v := range overrides {
160 cfg[k] = v
161 }
gio308105e2024-04-19 13:12:13 +0400162 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400163}
164
gio63a1a822025-04-23 12:59:40 +0400165func (m *AppManager) GetInstanceApp(id string, overrides CueAppData) (EnvApp, error) {
166 cfg, err := GetCueAppData(m.repo, filepath.Join(m.appDirRoot, id), overrides)
giof8843412024-05-22 16:38:05 +0400167 if err != nil {
168 return nil, err
169 }
170 return NewCueEnvApp(cfg)
171}
172
gio3af43942024-04-16 08:13:50 +0400173type allocatePortReq struct {
174 Protocol string `json:"protocol"`
175 SourcePort int `json:"sourcePort"`
176 TargetService string `json:"targetService"`
177 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400178 Secret string `json:"secret,omitempty"`
179}
180
181type removePortReq struct {
182 Protocol string `json:"protocol"`
183 SourcePort int `json:"sourcePort"`
184 TargetService string `json:"targetService"`
185 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400186}
187
gioefa0ed42024-06-13 12:31:43 +0400188type reservePortResp struct {
189 Port int `json:"port"`
190 Secret string `json:"secret"`
191}
192
gio721c0042025-04-03 11:56:36 +0400193type reservePortInfo struct {
194 reserveAddr string
195 RemoteProxy bool `json:"remoteProxy"`
196}
197
198func reservePorts(ports map[string]reservePortInfo) (map[string]reservePortResp, error) {
gioefa0ed42024-06-13 12:31:43 +0400199 ret := map[string]reservePortResp{}
gio721c0042025-04-03 11:56:36 +0400200 for p, cfg := range ports {
201 var buf bytes.Buffer
202 if err := json.NewEncoder(&buf).Encode(cfg); err != nil {
203 return nil, err
204 }
205 resp, err := http.Post(cfg.reserveAddr, "application/json", &buf)
gioefa0ed42024-06-13 12:31:43 +0400206 if err != nil {
207 return nil, err
208 }
209 if resp.StatusCode != http.StatusOK {
210 var e bytes.Buffer
211 io.Copy(&e, resp.Body)
212 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
213 }
214 var r reservePortResp
215 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
216 return nil, err
217 }
218 ret[p] = r
219 }
220 return ret, nil
221}
222
gio802311e2024-11-04 08:37:34 +0400223func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string, ns string) error {
gio3af43942024-04-16 08:13:50 +0400224 for _, p := range ports {
gio721c0042025-04-03 11:56:36 +0400225 var target string
226 if p.Cluster == "" {
giof4344632025-04-08 20:04:35 +0400227 if p.Service.Namespace == "" {
228 target = fmt.Sprintf("%s/%s", ns, p.Service.Name)
229 } else {
230 target = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
231 }
gio721c0042025-04-03 11:56:36 +0400232 } else {
233 target = p.Service.Name
234 }
gio3af43942024-04-16 08:13:50 +0400235 var buf bytes.Buffer
236 req := allocatePortReq{
237 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400238 SourcePort: p.Port,
gio721c0042025-04-03 11:56:36 +0400239 TargetService: target,
gio802311e2024-11-04 08:37:34 +0400240 TargetPort: p.Service.Port,
gio3af43942024-04-16 08:13:50 +0400241 }
gioefa0ed42024-06-13 12:31:43 +0400242 allocator := ""
243 for n, r := range reservations {
gio802311e2024-11-04 08:37:34 +0400244 if p.Port == r.Port {
gioefa0ed42024-06-13 12:31:43 +0400245 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400246 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400247 break
248 }
249 }
250 if allocator == "" {
gio802311e2024-11-04 08:37:34 +0400251 return fmt.Errorf("Could not find allocator for: %d", p.Port)
gioefa0ed42024-06-13 12:31:43 +0400252 }
giobd7ab0b2024-06-17 12:55:17 +0400253 if err := json.NewEncoder(&buf).Encode(req); err != nil {
254 return err
255 }
gioefa0ed42024-06-13 12:31:43 +0400256 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400257 if err != nil {
258 return err
259 }
260 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400261 var r bytes.Buffer
262 io.Copy(&r, resp.Body)
gio802311e2024-11-04 08:37:34 +0400263 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 +0400264 }
265 }
266 return nil
267}
268
gio802311e2024-11-04 08:37:34 +0400269func closePorts(ports []PortForward, ns string) error {
giocdfa3722024-06-13 20:10:14 +0400270 var retErr error
271 for _, p := range ports {
272 var buf bytes.Buffer
giof4344632025-04-08 20:04:35 +0400273 var fullName string
274 if p.Service.Namespace == "" {
275 fullName = fmt.Sprintf("%s/%s", ns, p.Service.Name)
276 } else {
277 fullName = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
278 }
giocdfa3722024-06-13 20:10:14 +0400279 req := removePortReq{
280 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400281 SourcePort: p.Port,
giof4344632025-04-08 20:04:35 +0400282 TargetService: fullName,
gio802311e2024-11-04 08:37:34 +0400283 TargetPort: p.Service.Port,
giocdfa3722024-06-13 20:10:14 +0400284 }
285 if err := json.NewEncoder(&buf).Encode(req); err != nil {
286 retErr = err
287 continue
288 }
giod78896a2025-04-10 07:42:13 +0400289 resp, err := http.Post(p.Network.DeallocatePortAddr, "application/json", &buf)
giocdfa3722024-06-13 20:10:14 +0400290 if err != nil {
291 retErr = err
292 continue
293 }
294 if resp.StatusCode != http.StatusOK {
gio802311e2024-11-04 08:37:34 +0400295 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.Port, resp.StatusCode)
giocdfa3722024-06-13 20:10:14 +0400296 continue
297 }
298 }
299 return retErr
300}
301
gioe72b54f2024-04-22 10:44:41 +0400302func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400303 for p := filepath.Clean(path); p != "/"; {
304 parent, child := filepath.Split(p)
gio92116ca2024-10-06 13:55:46 +0400305 kustPath := filepath.Join(parent, kustomizationFileName)
gioe72b54f2024-04-22 10:44:41 +0400306 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400307 if err != nil {
308 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400309 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400310 kust = &k
311 } else {
312 return err
313 }
314 }
315 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400316 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400317 return err
318 }
319 p = filepath.Clean(parent)
320 }
321 return nil
322}
323
gio778577f2024-04-29 09:44:38 +0400324type Resource struct {
giof8acc612025-04-26 08:20:55 +0400325 Id string `json:"id"`
giob4a3a192024-08-19 09:55:47 +0400326 Name string `json:"name"`
327 Namespace string `json:"namespace"`
328 Info string `json:"info"`
329 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400330}
331
332type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400333 Release Release
334 Helm []Resource
335 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400336}
337
gio3cdee592024-04-17 10:15:56 +0400338// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400339func installApp(
gioe72b54f2024-04-22 10:44:41 +0400340 repo soft.RepoIO,
341 appDir string,
342 name string,
343 config any,
gioe72b54f2024-04-22 10:44:41 +0400344 resources CueAppData,
345 data CueAppData,
giof8843412024-05-22 16:38:05 +0400346 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400347) error {
giof8843412024-05-22 16:38:05 +0400348 var o installOptions
349 for _, i := range opts {
350 i(&o)
351 }
352 dopts := []soft.DoOption{}
353 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400354 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
355 }
gio94904702024-07-26 16:58:34 +0400356 if o.NoPull {
357 dopts = append(dopts, soft.WithNoPull())
358 }
giof8843412024-05-22 16:38:05 +0400359 if o.NoPublish {
360 dopts = append(dopts, soft.WithNoCommit())
361 }
giof71a0832024-06-27 14:45:45 +0400362 if o.Force {
363 dopts = append(dopts, soft.WithForce())
364 }
gio9d66f322024-07-06 13:45:10 +0400365 if o.NoLock {
366 dopts = append(dopts, soft.WithNoLock())
367 }
giob4a3a192024-08-19 09:55:47 +0400368 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
giof6ad2982024-08-23 17:42:49 +0400369 if err := r.RemoveAll(appDir); err != nil {
gio308105e2024-04-19 13:12:13 +0400370 return "", err
371 }
372 resourcesDir := path.Join(appDir, "resources")
373 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400374 return "", err
375 }
gio94904702024-07-26 16:58:34 +0400376 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400377 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400378 return err
gio5e49bb62024-07-20 10:43:19 +0400379 }
gioe72b54f2024-04-22 10:44:41 +0400380 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400381 return err
gio3af43942024-04-16 08:13:50 +0400382 }
gioe72b54f2024-04-22 10:44:41 +0400383 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400384 return err
gio308105e2024-04-19 13:12:13 +0400385 }
gioe72b54f2024-04-22 10:44:41 +0400386 for name, contents := range data {
gio92116ca2024-10-06 13:55:46 +0400387 if name == "config.json" || name == kustomizationFileName || name == "resources" {
gio94904702024-07-26 16:58:34 +0400388 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400389 }
390 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400391 if err != nil {
gio94904702024-07-26 16:58:34 +0400392 return err
gio3af43942024-04-16 08:13:50 +0400393 }
gio308105e2024-04-19 13:12:13 +0400394 defer w.Close()
395 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400396 return err
gio3af43942024-04-16 08:13:50 +0400397 }
398 }
gio94904702024-07-26 16:58:34 +0400399 return nil
400 }(); err != nil {
401 return "", err
gio308105e2024-04-19 13:12:13 +0400402 }
gio94904702024-07-26 16:58:34 +0400403 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400404 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400405 return err
gio308105e2024-04-19 13:12:13 +0400406 }
gioefa0ed42024-06-13 12:31:43 +0400407 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400408 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400409 appKust.AddResources(name)
410 w, err := r.Writer(path.Join(resourcesDir, name))
411 if err != nil {
gio94904702024-07-26 16:58:34 +0400412 return err
gio308105e2024-04-19 13:12:13 +0400413 }
414 defer w.Close()
415 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400416 return err
gio308105e2024-04-19 13:12:13 +0400417 }
418 }
gio92116ca2024-10-06 13:55:46 +0400419 if err := soft.WriteYaml(r, path.Join(resourcesDir, kustomizationFileName), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400420 return err
gio3af43942024-04-16 08:13:50 +0400421 }
gio94904702024-07-26 16:58:34 +0400422 return nil
423 }(); err != nil {
424 return "", err
gio3af43942024-04-16 08:13:50 +0400425 }
gioe72b54f2024-04-22 10:44:41 +0400426 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400427 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400428 return err
gio3af43942024-04-16 08:13:50 +0400429}
430
gio3cdee592024-04-17 10:15:56 +0400431// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400432func (m *AppManager) Install(
433 app EnvApp,
434 instanceId string,
435 appDir string,
436 namespace string,
437 values map[string]any,
438 opts ...InstallOption,
439) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400440 o := &installOptions{}
441 for _, i := range opts {
442 i(o)
443 }
444 if !o.NoLock {
445 m.l.Lock()
446 defer m.l.Unlock()
447 }
gioefa0ed42024-06-13 12:31:43 +0400448 portFields := findPortFields(app.Schema())
449 fakeReservations := map[string]reservePortResp{}
450 for i, f := range portFields {
451 fakeReservations[f] = reservePortResp{Port: i}
452 }
453 if err := setPortFields(values, fakeReservations); err != nil {
454 return ReleaseResources{}, err
455 }
gio3af43942024-04-16 08:13:50 +0400456 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400457 if !o.NoPull {
giof6ad2982024-08-23 17:42:49 +0400458 if err := m.repo.Pull(); err != nil {
gio94904702024-07-26 16:58:34 +0400459 return ReleaseResources{}, err
460 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400461 }
gio94904702024-07-26 16:58:34 +0400462 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400463 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400464 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400465 }
gio0eaf2712024-04-14 13:08:46 +0400466 var env EnvConfig
467 if o.Env != nil {
468 env = *o.Env
469 } else {
470 var err error
471 env, err = m.Config()
472 if err != nil {
473 return ReleaseResources{}, err
474 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400475 }
giocb34ad22024-07-11 08:01:13 +0400476 var networks []Network
477 if o.Networks != nil {
478 networks = o.Networks
479 } else {
480 var err error
481 networks, err = m.CreateNetworks(env)
482 if err != nil {
483 return ReleaseResources{}, err
484 }
485 }
giof15b9da2024-09-19 06:59:16 +0400486 var clusters []Cluster
487 if o.Clusters != nil {
488 clusters = o.Clusters
489 } else {
490 if cls, err := m.GetClusters(); err != nil {
491 return ReleaseResources{}, err
492 } else {
493 clusters = ToAccessConfigs(cls)
494 }
giof6ad2982024-08-23 17:42:49 +0400495 }
giof8843412024-05-22 16:38:05 +0400496 var lg LocalChartGenerator
497 if o.LG != nil {
498 lg = o.LG
499 } else {
500 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
501 }
gio3cdee592024-04-17 10:15:56 +0400502 release := Release{
503 AppInstanceId: instanceId,
504 Namespace: namespace,
giof6ad2982024-08-23 17:42:49 +0400505 RepoAddr: m.repo.FullAddress(),
gio3cdee592024-04-17 10:15:56 +0400506 AppDir: appDir,
507 }
giof15b9da2024-09-19 06:59:16 +0400508 rendered, err := app.Render(release, env, networks, clusters, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400509 if err != nil {
gio778577f2024-04-29 09:44:38 +0400510 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400511 }
gio721c0042025-04-03 11:56:36 +0400512 reservators := map[string]reservePortInfo{}
gioefa0ed42024-06-13 12:31:43 +0400513 allocators := map[string]string{}
514 for _, pf := range rendered.Ports {
gio721c0042025-04-03 11:56:36 +0400515 reservators[portFields[pf.Port]] = reservePortInfo{
giod78896a2025-04-10 07:42:13 +0400516 reserveAddr: pf.Network.ReservePortAddr,
gio721c0042025-04-03 11:56:36 +0400517 RemoteProxy: pf.Cluster != "",
518 }
giod78896a2025-04-10 07:42:13 +0400519 allocators[portFields[pf.Port]] = pf.Network.AllocatePortAddr
gioefa0ed42024-06-13 12:31:43 +0400520 }
521 portReservations, err := reservePorts(reservators)
522 if err != nil {
523 return ReleaseResources{}, err
524 }
525 if err := setPortFields(values, portReservations); err != nil {
526 return ReleaseResources{}, err
527 }
gio7841f4f2024-07-26 19:53:49 +0400528 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400529 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
530 if o.FetchContainerImages {
531 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
532 return ReleaseResources{}, err
533 }
gio0eaf2712024-04-14 13:08:46 +0400534 }
giof6ad2982024-08-23 17:42:49 +0400535 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
giof71a0832024-06-27 14:45:45 +0400536 if err != nil {
giof8843412024-05-22 16:38:05 +0400537 return ReleaseResources{}, err
538 }
giof71a0832024-06-27 14:45:45 +0400539 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400540 if o.FetchContainerImages {
541 release.ImageRegistry = imageRegistry
542 }
giof15b9da2024-09-19 06:59:16 +0400543 rendered, err = app.Render(release, env, networks, clusters, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400544 if err != nil {
545 return ReleaseResources{}, err
546 }
giof6ad2982024-08-23 17:42:49 +0400547 for _, ns := range rendered.Namespaces {
548 if ns.Name == "" {
549 return ReleaseResources{}, fmt.Errorf("namespace name missing")
550 }
551 if ns.Kubeconfig == "" {
552 continue
553 }
554 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
555 if err != nil {
556 return ReleaseResources{}, err
557 }
558 if err := nsc.Create(ns.Name); err != nil {
559 return ReleaseResources{}, err
560 }
561 }
562 if err := installApp(m.repo, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400563 return ReleaseResources{}, err
564 }
gioff2a29a2024-05-01 17:06:42 +0400565 // TODO(gio): add ingress-nginx to release resources
gio802311e2024-11-04 08:37:34 +0400566 if err := openPorts(rendered.Ports, portReservations, allocators, release.Namespace); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400567 return ReleaseResources{}, err
568 }
giof6ad2982024-08-23 17:42:49 +0400569 for _, p := range rendered.ClusterProxies {
gio721c0042025-04-03 11:56:36 +0400570 if err := m.cnc.AddIngressProxy(p.From, p.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400571 return ReleaseResources{}, err
572 }
573 }
gio778577f2024-04-29 09:44:38 +0400574 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400575 Release: rendered.Config.Release,
576 RenderedRaw: rendered.Raw,
577 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400578 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400579}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400580
gio778577f2024-04-29 09:44:38 +0400581type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400582 Metadata struct {
583 Name string `json:"name"`
584 Namespace string `json:"namespace"`
585 Annotations map[string]string `json:"annotations"`
586 } `json:"metadata"`
587 Kind string `json:"kind"`
588 Status struct {
gio778577f2024-04-29 09:44:38 +0400589 Conditions []struct {
590 Type string `json:"type"`
591 Status string `json:"status"`
592 } `json:"conditions"`
593 } `json:"status,omitempty"`
594}
595
596func extractHelm(resources CueAppData) []Resource {
597 ret := make([]Resource, 0, len(resources))
598 for _, contents := range resources {
599 var h helmRelease
600 if err := yaml.Unmarshal(contents, &h); err != nil {
601 panic(err) // TODO(gio): handle
602 }
gio0eaf2712024-04-14 13:08:46 +0400603 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400604 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400605 Name: h.Metadata.Name,
606 Namespace: h.Metadata.Namespace,
607 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
giof8acc612025-04-26 08:20:55 +0400608 Annotations: h.Metadata.Annotations,
giof9f0bee2024-06-11 20:10:05 +0400609 }
610 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400611 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400612 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
613 if ok && len(info) != 0 {
614 res.Info = info
615 }
giof8acc612025-04-26 08:20:55 +0400616 id, ok := h.Metadata.Annotations["dodo.cloud/id"]
617 if ok && len(id) != 0 {
618 res.Id = id
619 }
giof9f0bee2024-06-11 20:10:05 +0400620 }
621 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400622 }
gio778577f2024-04-29 09:44:38 +0400623 }
624 return ret
625}
626
giof8843412024-05-22 16:38:05 +0400627// TODO(gio): take app configuration from the repo
628func (m *AppManager) Update(
629 instanceId string,
630 values map[string]any,
gio63a1a822025-04-23 12:59:40 +0400631 // TODO(gio): this should not be cue specific
632 overrides CueAppData,
giof8843412024-05-22 16:38:05 +0400633 opts ...InstallOption,
634) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400635 m.l.Lock()
636 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400637 if err := m.repo.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400638 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400639 }
gio3cdee592024-04-17 10:15:56 +0400640 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400641 if err != nil {
gio778577f2024-04-29 09:44:38 +0400642 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400643 }
gio308105e2024-04-19 13:12:13 +0400644 instanceDir := filepath.Join(m.appDirRoot, instanceId)
gio63a1a822025-04-23 12:59:40 +0400645 app, err := m.GetInstanceApp(instanceId, overrides)
giof8843412024-05-22 16:38:05 +0400646 if err != nil {
647 return ReleaseResources{}, err
648 }
gio308105e2024-04-19 13:12:13 +0400649 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400650 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400651 if err != nil {
gio778577f2024-04-29 09:44:38 +0400652 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400653 }
giof6ad2982024-08-23 17:42:49 +0400654 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400655 if err != nil {
656 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400657 }
giocb34ad22024-07-11 08:01:13 +0400658 networks, err := m.CreateNetworks(env)
659 if err != nil {
660 return ReleaseResources{}, err
661 }
giof6ad2982024-08-23 17:42:49 +0400662 clusters, err := m.GetClusters()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400663 if err != nil {
gio778577f2024-04-29 09:44:38 +0400664 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400665 }
gio63a1a822025-04-23 12:59:40 +0400666 rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
giof6ad2982024-08-23 17:42:49 +0400667 if err != nil {
gio94904702024-07-26 16:58:34 +0400668 return ReleaseResources{}, err
669 }
giof6ad2982024-08-23 17:42:49 +0400670 for _, ns := range rendered.Namespaces {
671 if ns.Name == "" {
672 return ReleaseResources{}, fmt.Errorf("namespace name missing")
673 }
674 if ns.Kubeconfig == "" {
675 continue
676 }
677 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
678 if err != nil {
679 return ReleaseResources{}, err
680 }
681 if err := nsc.Create(ns.Name); err != nil {
682 return ReleaseResources{}, err
683 }
684 }
685 if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
686 return ReleaseResources{}, err
687 }
688 for _, ocp := range renderedCfg.Out.ClusterProxy {
689 found := false
690 for _, ncp := range rendered.ClusterProxies {
691 if ocp == ncp {
692 found = true
693 break
694 }
695 }
696 if !found {
gio721c0042025-04-03 11:56:36 +0400697 if err := m.cnc.RemoveIngressProxy(ocp.From, ocp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400698 return ReleaseResources{}, err
699 }
700 }
701 }
702 for _, ncp := range rendered.ClusterProxies {
703 found := false
704 for _, ocp := range renderedCfg.Out.ClusterProxy {
705 if ocp == ncp {
706 found = true
707 break
708 }
709 }
710 if !found {
gio721c0042025-04-03 11:56:36 +0400711 if err := m.cnc.AddIngressProxy(ncp.From, ncp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400712 return ReleaseResources{}, err
713 }
714 }
715 }
gio94904702024-07-26 16:58:34 +0400716 return ReleaseResources{
717 Release: rendered.Config.Release,
718 RenderedRaw: rendered.Raw,
719 Helm: extractHelm(rendered.Resources),
720 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400721}
722
723func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400724 m.l.Lock()
725 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400726 if err := m.repo.Pull(); err != nil {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400727 return err
728 }
gio864b4332024-09-05 13:56:47 +0400729 var cfg renderedInstance
giof6ad2982024-08-23 17:42:49 +0400730 if _, err := m.repo.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400731 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof6ad2982024-08-23 17:42:49 +0400732 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giocdfa3722024-06-13 20:10:14 +0400733 if err != nil {
734 return "", err
735 }
gio864b4332024-09-05 13:56:47 +0400736 cfg = renderedCfg
giof6ad2982024-08-23 17:42:49 +0400737 r.RemoveAll(instanceDir)
gio5887caa2024-10-03 15:07:23 +0400738 curr := instanceDir
gio829b1b72024-10-05 21:50:56 +0400739 for {
gio5887caa2024-10-03 15:07:23 +0400740 p := filepath.Dir(curr)
gio829b1b72024-10-05 21:50:56 +0400741 if p == curr {
742 break
743 }
gio5887caa2024-10-03 15:07:23 +0400744 n := filepath.Base(curr)
gio92116ca2024-10-06 13:55:46 +0400745 kustPath := filepath.Join(p, kustomizationFileName)
gio5887caa2024-10-03 15:07:23 +0400746 kust, err := soft.ReadKustomization(r, kustPath)
747 if err != nil {
748 return "", err
749 }
750 kust.RemoveResources(n)
gio829b1b72024-10-05 21:50:56 +0400751 if len(kust.Resources) > 0 || p == m.appDirRoot {
gio5887caa2024-10-03 15:07:23 +0400752 soft.WriteYaml(r, kustPath, kust)
753 break
754 } else {
755 if err := r.RemoveAll(kustPath); err != nil {
756 return "", err
757 }
758 }
gio5887caa2024-10-03 15:07:23 +0400759 curr = p
gio3af43942024-04-16 08:13:50 +0400760 }
gio3af43942024-04-16 08:13:50 +0400761 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400762 }); err != nil {
763 return err
764 }
gio802311e2024-11-04 08:37:34 +0400765 if err := closePorts(cfg.Output.PortForward, cfg.Release.Namespace); err != nil {
giocdfa3722024-06-13 20:10:14 +0400766 return err
767 }
giof6ad2982024-08-23 17:42:49 +0400768 for _, cp := range cfg.Out.ClusterProxy {
gio721c0042025-04-03 11:56:36 +0400769 if err := m.cnc.RemoveIngressProxy(cp.From, cp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400770 return err
771 }
772 }
gio864b4332024-09-05 13:56:47 +0400773 for vmName, vmCfg := range cfg.Out.VM {
774 if vmCfg.VPN.Enabled {
gio92116ca2024-10-06 13:55:46 +0400775 // Not found error is ignored as VM might have not had enough time to boot before uninstalling it.
776 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400777 return err
778 }
779 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
780 return err
781 }
gio92116ca2024-10-06 13:55:46 +0400782 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400783 return err
784 }
785 }
786 }
giocdfa3722024-06-13 20:10:14 +0400787 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400788}
789
giocb34ad22024-07-11 08:01:13 +0400790func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
791 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400792 {
giocdfa3722024-06-13 20:10:14 +0400793 Name: "Public",
794 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
795 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
796 Domain: env.Domain,
797 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
798 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
799 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400800 },
gio7841f4f2024-07-26 19:53:49 +0400801 }
802 if env.PrivateDomain != "" {
803 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400804 Name: "Private",
805 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
806 Domain: env.PrivateDomain,
807 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
808 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
809 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400810 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400811 }
gio7fbd4ad2024-08-27 10:06:39 +0400812 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400813 if err != nil {
814 return nil, err
815 }
816 for _, a := range n {
817 ret = append(ret, Network{
818 Name: a.Input["name"].(string),
819 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
820 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
821 Domain: a.Input["domain"].(string),
822 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
823 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
824 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
825 })
826 }
827 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400828}
gio3cdee592024-04-17 10:15:56 +0400829
giof6ad2982024-08-23 17:42:49 +0400830func (m *AppManager) GetClusters() ([]cluster.State, error) {
831 ret := []cluster.State{
832 {
833 Name: "default",
834 },
835 }
836 files, err := m.repo.ListDir("/clusters")
837 if err != nil {
838 if errors.Is(err, fs.ErrNotExist) {
839 return ret, nil
840 }
841 return nil, err
842 }
843 for _, f := range files {
844 if !f.IsDir() {
845 continue
846 }
847 cfgPath := filepath.Clean(filepath.Join("/clusters", f.Name(), "config.json"))
848 var c cluster.State
849 if err := soft.ReadJson(m.repo, cfgPath, &c); err != nil {
850 if errors.Is(err, fs.ErrNotExist) {
851 continue
852 }
853 return nil, err
854 }
855 ret = append(ret, c)
856 }
857 return ret, nil
858}
859
gio0eaf2712024-04-14 13:08:46 +0400860type installOptions struct {
gio94904702024-07-26 16:58:34 +0400861 NoPull bool
giof8843412024-05-22 16:38:05 +0400862 NoPublish bool
863 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400864 Networks []Network
giof15b9da2024-09-19 06:59:16 +0400865 Clusters []Cluster
giof8843412024-05-22 16:38:05 +0400866 Branch string
867 LG LocalChartGenerator
868 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400869 Force bool
gio9d66f322024-07-06 13:45:10 +0400870 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400871}
872
873type InstallOption func(*installOptions)
874
875func WithConfig(env *EnvConfig) InstallOption {
876 return func(o *installOptions) {
877 o.Env = env
878 }
879}
880
giocb34ad22024-07-11 08:01:13 +0400881func WithNetworks(networks []Network) InstallOption {
882 return func(o *installOptions) {
883 o.Networks = networks
884 }
885}
886
gio23bdc1b2024-07-11 16:07:47 +0400887func WithNoNetworks() InstallOption {
888 return WithNetworks([]Network{})
889}
890
giof15b9da2024-09-19 06:59:16 +0400891func WithClusters(clusters []Cluster) InstallOption {
892 return func(o *installOptions) {
893 o.Clusters = clusters
894 }
895}
896
gio0eaf2712024-04-14 13:08:46 +0400897func WithBranch(branch string) InstallOption {
898 return func(o *installOptions) {
899 o.Branch = branch
900 }
901}
902
giof71a0832024-06-27 14:45:45 +0400903func WithForce() InstallOption {
904 return func(o *installOptions) {
905 o.Force = true
906 }
907}
908
giof8843412024-05-22 16:38:05 +0400909func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
910 return func(o *installOptions) {
911 o.LG = lg
912 }
913}
914
915func WithFetchContainerImages() InstallOption {
916 return func(o *installOptions) {
917 o.FetchContainerImages = true
918 }
919}
920
921func WithNoPublish() InstallOption {
922 return func(o *installOptions) {
923 o.NoPublish = true
924 }
925}
926
gio94904702024-07-26 16:58:34 +0400927func WithNoPull() InstallOption {
928 return func(o *installOptions) {
929 o.NoPull = true
930 }
931}
932
gio9d66f322024-07-06 13:45:10 +0400933func WithNoLock() InstallOption {
934 return func(o *installOptions) {
935 o.NoLock = true
936 }
937}
938
giof8843412024-05-22 16:38:05 +0400939// InfraAppmanager
940
941type InfraAppManager struct {
942 repoIO soft.RepoIO
943 nsc NamespaceCreator
944 hf HelmFetcher
945 lg LocalChartGenerator
946}
947
948func NewInfraAppManager(
949 repoIO soft.RepoIO,
950 nsc NamespaceCreator,
951 hf HelmFetcher,
952 lg LocalChartGenerator,
953) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400954 return &InfraAppManager{
955 repoIO,
giof8843412024-05-22 16:38:05 +0400956 nsc,
957 hf,
958 lg,
gio3cdee592024-04-17 10:15:56 +0400959 }, nil
960}
961
962func (m *InfraAppManager) Config() (InfraConfig, error) {
963 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400964 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400965 return InfraConfig{}, err
966 } else {
967 return cfg, nil
968 }
969}
970
gioe72b54f2024-04-22 10:44:41 +0400971func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
972 var cfg InfraAppInstanceConfig
973 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
974 return InfraAppInstanceConfig{}, err
975 } else {
976 return cfg, nil
977 }
978}
979
980func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400981 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", kustomizationFileName))
gioe72b54f2024-04-22 10:44:41 +0400982 if err != nil {
983 return InfraAppInstanceConfig{}, err
984 }
985 for _, app := range kust.Resources {
986 if app == id {
987 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
988 if err != nil {
989 return InfraAppInstanceConfig{}, err
990 }
991 cfg.Id = id
992 return cfg, nil
993 }
994 }
995 return InfraAppInstanceConfig{}, nil
996}
997
gio778577f2024-04-29 09:44:38 +0400998func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400999 appDir = filepath.Clean(appDir)
1000 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001001 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001002 }
giof8843412024-05-22 16:38:05 +04001003 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +04001004 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001005 }
1006 infra, err := m.Config()
1007 if err != nil {
gio778577f2024-04-29 09:44:38 +04001008 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001009 }
1010 release := Release{
1011 Namespace: namespace,
1012 RepoAddr: m.repoIO.FullAddress(),
1013 AppDir: appDir,
1014 }
gio7841f4f2024-07-26 19:53:49 +04001015 networks := m.CreateNetworks(infra)
1016 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +04001017 if err != nil {
1018 return ReleaseResources{}, err
1019 }
giof71a0832024-06-27 14:45:45 +04001020 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
1021 if err != nil {
giof8843412024-05-22 16:38:05 +04001022 return ReleaseResources{}, err
1023 }
giof71a0832024-06-27 14:45:45 +04001024 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +04001025 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +04001026 if err != nil {
gio778577f2024-04-29 09:44:38 +04001027 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001028 }
gio94904702024-07-26 16:58:34 +04001029 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
1030 return ReleaseResources{}, err
1031 }
1032 return ReleaseResources{
1033 Release: rendered.Config.Release,
1034 RenderedRaw: rendered.Raw,
1035 Helm: extractHelm(rendered.Resources),
1036 }, nil
gioe72b54f2024-04-22 10:44:41 +04001037}
1038
giof8843412024-05-22 16:38:05 +04001039// TODO(gio): take app configuration from the repo
1040func (m *InfraAppManager) Update(
1041 instanceId string,
1042 values map[string]any,
1043 opts ...InstallOption,
1044) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +04001045 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001046 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001047 }
gio7841f4f2024-07-26 19:53:49 +04001048 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +04001049 if err != nil {
gio778577f2024-04-29 09:44:38 +04001050 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001051 }
1052 instanceDir := filepath.Join("/infrastructure", instanceId)
gio63a1a822025-04-23 12:59:40 +04001053 appCfg, err := GetCueAppData(m.repoIO, instanceDir, nil)
giof8843412024-05-22 16:38:05 +04001054 if err != nil {
1055 return ReleaseResources{}, err
1056 }
1057 app, err := NewCueInfraApp(appCfg)
1058 if err != nil {
1059 return ReleaseResources{}, err
1060 }
gioe72b54f2024-04-22 10:44:41 +04001061 instanceConfigPath := filepath.Join(instanceDir, "config.json")
1062 config, err := m.appConfig(instanceConfigPath)
1063 if err != nil {
gio778577f2024-04-29 09:44:38 +04001064 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001065 }
giocdfa3722024-06-13 20:10:14 +04001066 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +04001067 if err != nil {
1068 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001069 }
gio7841f4f2024-07-26 19:53:49 +04001070 networks := m.CreateNetworks(infra)
1071 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +04001072 if err != nil {
gio778577f2024-04-29 09:44:38 +04001073 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001074 }
gio94904702024-07-26 16:58:34 +04001075 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
1076 return ReleaseResources{}, err
1077 }
1078 return ReleaseResources{
1079 Release: rendered.Config.Release,
1080 RenderedRaw: rendered.Raw,
1081 Helm: extractHelm(rendered.Resources),
1082 }, nil
gio3cdee592024-04-17 10:15:56 +04001083}
giof8843412024-05-22 16:38:05 +04001084
gio7841f4f2024-07-26 19:53:49 +04001085func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
1086 return []InfraNetwork{
1087 {
1088 Name: "Public",
1089 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
1090 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
1091 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
1092 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
1093 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
1094 },
1095 }
1096}
1097
giof8843412024-05-22 16:38:05 +04001098func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
1099 ret := make(map[string]string)
1100 for name, chart := range charts.Git {
1101 chartRoot := filepath.Join(root, name)
1102 ret[name] = chartRoot
1103 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
1104 return nil, err
1105 }
1106 }
1107 return ret, nil
1108}
1109
1110func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
1111 ret := make(map[string]helmv2.HelmChartTemplateSpec)
1112 for name, path := range charts {
1113 ret[name] = g.Generate(path)
1114 }
1115 return ret
1116}
1117
1118func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
1119 for _, img := range imgs {
1120 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
1121 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
1122 "skopeo",
1123 "--insecure-policy",
1124 "copy",
1125 "--dest-tls-verify=false", // TODO(gio): enable
1126 "--multi-arch=all",
1127 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
1128 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
1129 }); err != nil {
1130 return err
1131 }
1132 }
1133 return nil
1134}
1135
1136type renderedInstance struct {
gio802311e2024-11-04 08:37:34 +04001137 Release Release `json:"release"`
giof8843412024-05-22 16:38:05 +04001138 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
gio864b4332024-09-05 13:56:47 +04001139 Out outRendered `json:"out"`
gio802311e2024-11-04 08:37:34 +04001140 Output outputRendered `json:"output"`
1141}
1142
1143type outputRendered struct {
1144 PortForward []PortForward `json:"openPort"`
gio864b4332024-09-05 13:56:47 +04001145}
1146
1147type outRendered struct {
giof6ad2982024-08-23 17:42:49 +04001148 ClusterProxy map[string]ClusterProxy
1149 VM map[string]vmRendered `json:"vm"`
gio864b4332024-09-05 13:56:47 +04001150}
1151
1152type vmRendered struct {
1153 Username string `json:"username"`
1154 VPN struct {
1155 Enabled bool `json:"enabled"`
1156 AuthKey string `json:"authKey"`
1157 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +04001158}
1159
giocdfa3722024-06-13 20:10:14 +04001160func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +04001161 r, err := fs.Reader(path)
1162 if err != nil {
giocdfa3722024-06-13 20:10:14 +04001163 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001164 }
1165 defer r.Close()
1166 var cfg renderedInstance
1167 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +04001168 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001169 }
giocdfa3722024-06-13 20:10:14 +04001170 return cfg, nil
giof8843412024-05-22 16:38:05 +04001171}
gioefa0ed42024-06-13 12:31:43 +04001172
1173func findPortFields(scm Schema) []string {
1174 switch scm.Kind() {
1175 case KindBoolean:
1176 return []string{}
1177 case KindInt:
1178 return []string{}
1179 case KindString:
1180 return []string{}
1181 case KindStruct:
1182 ret := []string{}
1183 for _, f := range scm.Fields() {
1184 for _, p := range findPortFields(f.Schema) {
1185 if p == "" {
1186 ret = append(ret, f.Name)
1187 } else {
1188 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1189 }
1190 }
1191 }
1192 return ret
1193 case KindNetwork:
1194 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001195 case KindMultiNetwork:
1196 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001197 case KindAuth:
1198 return []string{}
1199 case KindSSHKey:
1200 return []string{}
1201 case KindNumber:
1202 return []string{}
1203 case KindArrayString:
1204 return []string{}
1205 case KindPort:
1206 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001207 case KindVPNAuthKey:
1208 return []string{}
giof6ad2982024-08-23 17:42:49 +04001209 case KindCluster:
1210 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001211 default:
1212 panic("MUST NOT REACH!")
1213 }
1214}
1215
1216func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1217 for p, r := range ports {
1218 if err := setPortField(values, p, r.Port); err != nil {
1219 return err
1220 }
1221 }
1222 return nil
1223}
1224
1225func setPortField(values map[string]any, field string, port int) error {
1226 f := strings.SplitN(field, ".", 2)
1227 if len(f) == 2 {
1228 var sub map[string]any
1229 if s, ok := values[f[0]]; ok {
1230 sub, ok = s.(map[string]any)
1231 if !ok {
1232 return fmt.Errorf("expected map")
1233 }
1234 } else {
1235 sub = map[string]any{}
1236 values[f[0]] = sub
1237 }
1238 if err := setPortField(sub, f[1], port); err != nil {
1239 return err
1240 }
1241 } else {
1242 values[f[0]] = port
1243 }
1244 return nil
1245}
giof6ad2982024-08-23 17:42:49 +04001246
1247type Cluster struct {
1248 Name string `json:"name"`
1249 Kubeconfig string `json:"kubeconfig"`
1250 IngressClassName string `json:"ingressClassName"`
1251}
1252
1253func ClusterStateToAccessConfig(c cluster.State) Cluster {
1254 return Cluster{
1255 Name: c.Name,
1256 Kubeconfig: c.Kubeconfig,
1257 IngressClassName: c.IngressClassName,
1258 }
1259}
1260
1261func ToAccessConfigs(clusters []cluster.State) []Cluster {
1262 ret := make([]Cluster, 0, len(clusters))
1263 for _, c := range clusters {
1264 ret = append(ret, ClusterStateToAccessConfig(c))
1265 }
1266 return ret
1267}