blob: fbee7836f74ac7c5e8e92b1244bc33aa38dfdeff [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
gio7fbd4ad2024-08-27 10:06:39 +040084func (m *AppManager) GetAllInstances() ([]AppInstanceConfig, error) {
giof6ad2982024-08-23 17:42:49 +040085 m.repo.Pull()
gio92116ca2024-10-06 13:55:46 +040086 kust, err := soft.ReadKustomization(m.repo, filepath.Join(m.appDirRoot, kustomizationFileName))
gio3af43942024-04-16 08:13:50 +040087 if err != nil {
giof6ad2982024-08-23 17:42:49 +040088 if errors.Is(err, fs.ErrNotExist) {
89 return nil, nil
90 }
gio3af43942024-04-16 08:13:50 +040091 return nil, err
92 }
gio3cdee592024-04-17 10:15:56 +040093 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040094 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040095 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
96 if err != nil {
97 return nil, err
98 }
99 cfg.Id = app
100 ret = append(ret, cfg)
101 }
102 return ret, nil
103}
104
gio7fbd4ad2024-08-27 10:06:39 +0400105func (m *AppManager) GetAllAppInstances(name string) ([]AppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400106 kust, err := soft.ReadKustomization(m.repo, filepath.Join(m.appDirRoot, kustomizationFileName))
gio308105e2024-04-19 13:12:13 +0400107 if err != nil {
giocb34ad22024-07-11 08:01:13 +0400108 if errors.Is(err, fs.ErrNotExist) {
109 return nil, nil
110 } else {
111 return nil, err
112 }
gio308105e2024-04-19 13:12:13 +0400113 }
114 ret := make([]AppInstanceConfig, 0)
115 for _, app := range kust.Resources {
116 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400117 if err != nil {
118 return nil, err
119 }
120 cfg.Id = app
121 if cfg.AppId == name {
122 ret = append(ret, cfg)
123 }
124 }
125 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400126}
127
gio7fbd4ad2024-08-27 10:06:39 +0400128func (m *AppManager) GetInstance(id string) (*AppInstanceConfig, error) {
129 appDir := filepath.Clean(filepath.Join(m.appDirRoot, id))
130 cfgPath := filepath.Join(appDir, "config.json")
gio7fbd4ad2024-08-27 10:06:39 +0400131 cfg, err := m.appConfig(cfgPath)
gio3af43942024-04-16 08:13:50 +0400132 if err != nil {
gio778577f2024-04-29 09:44:38 +0400133 return nil, err
gio3af43942024-04-16 08:13:50 +0400134 }
gio7fbd4ad2024-08-27 10:06:39 +0400135 cfg.Id = id
136 return &cfg, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400137}
138
giof8843412024-05-22 16:38:05 +0400139func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
140 files, err := fs.ListDir(dir)
141 if err != nil {
142 return nil, err
143 }
144 cfg := CueAppData{}
145 for _, f := range files {
146 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
147 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
148 if err != nil {
149 return nil, err
150 }
151 cfg[f.Name()] = contents
152 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400153 }
gio308105e2024-04-19 13:12:13 +0400154 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400155}
156
giof8843412024-05-22 16:38:05 +0400157func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
giof6ad2982024-08-23 17:42:49 +0400158 cfg, err := GetCueAppData(m.repo, filepath.Join(m.appDirRoot, id))
giof8843412024-05-22 16:38:05 +0400159 if err != nil {
160 return nil, err
161 }
162 return NewCueEnvApp(cfg)
163}
164
gio3af43942024-04-16 08:13:50 +0400165type allocatePortReq struct {
166 Protocol string `json:"protocol"`
167 SourcePort int `json:"sourcePort"`
168 TargetService string `json:"targetService"`
169 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400170 Secret string `json:"secret,omitempty"`
171}
172
173type removePortReq struct {
174 Protocol string `json:"protocol"`
175 SourcePort int `json:"sourcePort"`
176 TargetService string `json:"targetService"`
177 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400178}
179
gioefa0ed42024-06-13 12:31:43 +0400180type reservePortResp struct {
181 Port int `json:"port"`
182 Secret string `json:"secret"`
183}
184
185func reservePorts(ports map[string]string) (map[string]reservePortResp, error) {
186 ret := map[string]reservePortResp{}
187 for p, reserveAddr := range ports {
188 resp, err := http.Post(reserveAddr, "application/json", nil) // TODO(gio): address
189 if err != nil {
190 return nil, err
191 }
192 if resp.StatusCode != http.StatusOK {
193 var e bytes.Buffer
194 io.Copy(&e, resp.Body)
195 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
196 }
197 var r reservePortResp
198 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
199 return nil, err
200 }
201 ret[p] = r
202 }
203 return ret, nil
204}
205
206func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string) error {
gio3af43942024-04-16 08:13:50 +0400207 for _, p := range ports {
208 var buf bytes.Buffer
209 req := allocatePortReq{
210 Protocol: p.Protocol,
211 SourcePort: p.SourcePort,
212 TargetService: p.TargetService,
213 TargetPort: p.TargetPort,
214 }
gioefa0ed42024-06-13 12:31:43 +0400215 allocator := ""
216 for n, r := range reservations {
217 if p.SourcePort == r.Port {
218 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400219 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400220 break
221 }
222 }
223 if allocator == "" {
224 return fmt.Errorf("Could not find allocator for: %d", p.SourcePort)
225 }
giobd7ab0b2024-06-17 12:55:17 +0400226 if err := json.NewEncoder(&buf).Encode(req); err != nil {
227 return err
228 }
gioefa0ed42024-06-13 12:31:43 +0400229 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400230 if err != nil {
231 return err
232 }
233 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400234 var r bytes.Buffer
235 io.Copy(&r, resp.Body)
236 return fmt.Errorf("Could not allocate port %d, status code %d, message: %s", p.SourcePort, resp.StatusCode, r.String())
gio3af43942024-04-16 08:13:50 +0400237 }
238 }
239 return nil
240}
241
giocdfa3722024-06-13 20:10:14 +0400242func closePorts(ports []PortForward) error {
243 var retErr error
244 for _, p := range ports {
245 var buf bytes.Buffer
246 req := removePortReq{
247 Protocol: p.Protocol,
248 SourcePort: p.SourcePort,
249 TargetService: p.TargetService,
250 TargetPort: p.TargetPort,
251 }
252 if err := json.NewEncoder(&buf).Encode(req); err != nil {
253 retErr = err
254 continue
255 }
256 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
257 if err != nil {
258 retErr = err
259 continue
260 }
261 if resp.StatusCode != http.StatusOK {
262 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
263 continue
264 }
265 }
266 return retErr
267}
268
gioe72b54f2024-04-22 10:44:41 +0400269func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400270 for p := filepath.Clean(path); p != "/"; {
271 parent, child := filepath.Split(p)
gio92116ca2024-10-06 13:55:46 +0400272 kustPath := filepath.Join(parent, kustomizationFileName)
gioe72b54f2024-04-22 10:44:41 +0400273 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400274 if err != nil {
275 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400276 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400277 kust = &k
278 } else {
279 return err
280 }
281 }
282 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400283 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400284 return err
285 }
286 p = filepath.Clean(parent)
287 }
288 return nil
289}
290
gio778577f2024-04-29 09:44:38 +0400291type Resource struct {
giob4a3a192024-08-19 09:55:47 +0400292 Name string `json:"name"`
293 Namespace string `json:"namespace"`
294 Info string `json:"info"`
295 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400296}
297
298type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400299 Release Release
300 Helm []Resource
301 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400302}
303
gio3cdee592024-04-17 10:15:56 +0400304// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400305func installApp(
gioe72b54f2024-04-22 10:44:41 +0400306 repo soft.RepoIO,
307 appDir string,
308 name string,
309 config any,
gioe72b54f2024-04-22 10:44:41 +0400310 resources CueAppData,
311 data CueAppData,
giof8843412024-05-22 16:38:05 +0400312 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400313) error {
giof8843412024-05-22 16:38:05 +0400314 var o installOptions
315 for _, i := range opts {
316 i(&o)
317 }
318 dopts := []soft.DoOption{}
319 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400320 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
321 }
gio94904702024-07-26 16:58:34 +0400322 if o.NoPull {
323 dopts = append(dopts, soft.WithNoPull())
324 }
giof8843412024-05-22 16:38:05 +0400325 if o.NoPublish {
326 dopts = append(dopts, soft.WithNoCommit())
327 }
giof71a0832024-06-27 14:45:45 +0400328 if o.Force {
329 dopts = append(dopts, soft.WithForce())
330 }
gio9d66f322024-07-06 13:45:10 +0400331 if o.NoLock {
332 dopts = append(dopts, soft.WithNoLock())
333 }
giob4a3a192024-08-19 09:55:47 +0400334 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
giof6ad2982024-08-23 17:42:49 +0400335 if err := r.RemoveAll(appDir); err != nil {
gio308105e2024-04-19 13:12:13 +0400336 return "", err
337 }
338 resourcesDir := path.Join(appDir, "resources")
339 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400340 return "", err
341 }
gio94904702024-07-26 16:58:34 +0400342 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400343 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400344 return err
gio5e49bb62024-07-20 10:43:19 +0400345 }
gioe72b54f2024-04-22 10:44:41 +0400346 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400347 return err
gio3af43942024-04-16 08:13:50 +0400348 }
gioe72b54f2024-04-22 10:44:41 +0400349 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400350 return err
gio308105e2024-04-19 13:12:13 +0400351 }
gioe72b54f2024-04-22 10:44:41 +0400352 for name, contents := range data {
gio92116ca2024-10-06 13:55:46 +0400353 if name == "config.json" || name == kustomizationFileName || name == "resources" {
gio94904702024-07-26 16:58:34 +0400354 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400355 }
356 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400357 if err != nil {
gio94904702024-07-26 16:58:34 +0400358 return err
gio3af43942024-04-16 08:13:50 +0400359 }
gio308105e2024-04-19 13:12:13 +0400360 defer w.Close()
361 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400362 return err
gio3af43942024-04-16 08:13:50 +0400363 }
364 }
gio94904702024-07-26 16:58:34 +0400365 return nil
366 }(); err != nil {
367 return "", err
gio308105e2024-04-19 13:12:13 +0400368 }
gio94904702024-07-26 16:58:34 +0400369 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400370 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400371 return err
gio308105e2024-04-19 13:12:13 +0400372 }
gioefa0ed42024-06-13 12:31:43 +0400373 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400374 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400375 appKust.AddResources(name)
376 w, err := r.Writer(path.Join(resourcesDir, name))
377 if err != nil {
gio94904702024-07-26 16:58:34 +0400378 return err
gio308105e2024-04-19 13:12:13 +0400379 }
380 defer w.Close()
381 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400382 return err
gio308105e2024-04-19 13:12:13 +0400383 }
384 }
gio92116ca2024-10-06 13:55:46 +0400385 if err := soft.WriteYaml(r, path.Join(resourcesDir, kustomizationFileName), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400386 return err
gio3af43942024-04-16 08:13:50 +0400387 }
gio94904702024-07-26 16:58:34 +0400388 return nil
389 }(); err != nil {
390 return "", err
gio3af43942024-04-16 08:13:50 +0400391 }
gioe72b54f2024-04-22 10:44:41 +0400392 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400393 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400394 return err
gio3af43942024-04-16 08:13:50 +0400395}
396
gio3cdee592024-04-17 10:15:56 +0400397// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400398func (m *AppManager) Install(
399 app EnvApp,
400 instanceId string,
401 appDir string,
402 namespace string,
403 values map[string]any,
404 opts ...InstallOption,
405) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400406 o := &installOptions{}
407 for _, i := range opts {
408 i(o)
409 }
410 if !o.NoLock {
411 m.l.Lock()
412 defer m.l.Unlock()
413 }
gioefa0ed42024-06-13 12:31:43 +0400414 portFields := findPortFields(app.Schema())
415 fakeReservations := map[string]reservePortResp{}
416 for i, f := range portFields {
417 fakeReservations[f] = reservePortResp{Port: i}
418 }
419 if err := setPortFields(values, fakeReservations); err != nil {
420 return ReleaseResources{}, err
421 }
gio3af43942024-04-16 08:13:50 +0400422 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400423 if !o.NoPull {
giof6ad2982024-08-23 17:42:49 +0400424 if err := m.repo.Pull(); err != nil {
gio94904702024-07-26 16:58:34 +0400425 return ReleaseResources{}, err
426 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400427 }
gio94904702024-07-26 16:58:34 +0400428 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400429 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400430 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400431 }
gio0eaf2712024-04-14 13:08:46 +0400432 var env EnvConfig
433 if o.Env != nil {
434 env = *o.Env
435 } else {
436 var err error
437 env, err = m.Config()
438 if err != nil {
439 return ReleaseResources{}, err
440 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400441 }
giocb34ad22024-07-11 08:01:13 +0400442 var networks []Network
443 if o.Networks != nil {
444 networks = o.Networks
445 } else {
446 var err error
447 networks, err = m.CreateNetworks(env)
448 if err != nil {
449 return ReleaseResources{}, err
450 }
451 }
giof15b9da2024-09-19 06:59:16 +0400452 var clusters []Cluster
453 if o.Clusters != nil {
454 clusters = o.Clusters
455 } else {
456 if cls, err := m.GetClusters(); err != nil {
457 return ReleaseResources{}, err
458 } else {
459 clusters = ToAccessConfigs(cls)
460 }
giof6ad2982024-08-23 17:42:49 +0400461 }
giof8843412024-05-22 16:38:05 +0400462 var lg LocalChartGenerator
463 if o.LG != nil {
464 lg = o.LG
465 } else {
466 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
467 }
gio3cdee592024-04-17 10:15:56 +0400468 release := Release{
469 AppInstanceId: instanceId,
470 Namespace: namespace,
giof6ad2982024-08-23 17:42:49 +0400471 RepoAddr: m.repo.FullAddress(),
gio3cdee592024-04-17 10:15:56 +0400472 AppDir: appDir,
473 }
giof15b9da2024-09-19 06:59:16 +0400474 rendered, err := app.Render(release, env, networks, clusters, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400475 if err != nil {
gio778577f2024-04-29 09:44:38 +0400476 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400477 }
gioefa0ed42024-06-13 12:31:43 +0400478 reservators := map[string]string{}
479 allocators := map[string]string{}
480 for _, pf := range rendered.Ports {
481 reservators[portFields[pf.SourcePort]] = pf.ReserveAddr
482 allocators[portFields[pf.SourcePort]] = pf.Allocator
483 }
484 portReservations, err := reservePorts(reservators)
485 if err != nil {
486 return ReleaseResources{}, err
487 }
488 if err := setPortFields(values, portReservations); err != nil {
489 return ReleaseResources{}, err
490 }
gio7841f4f2024-07-26 19:53:49 +0400491 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400492 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
493 if o.FetchContainerImages {
494 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
495 return ReleaseResources{}, err
496 }
gio0eaf2712024-04-14 13:08:46 +0400497 }
giof6ad2982024-08-23 17:42:49 +0400498 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
giof71a0832024-06-27 14:45:45 +0400499 if err != nil {
giof8843412024-05-22 16:38:05 +0400500 return ReleaseResources{}, err
501 }
giof71a0832024-06-27 14:45:45 +0400502 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400503 if o.FetchContainerImages {
504 release.ImageRegistry = imageRegistry
505 }
giof15b9da2024-09-19 06:59:16 +0400506 rendered, err = app.Render(release, env, networks, clusters, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400507 if err != nil {
508 return ReleaseResources{}, err
509 }
giof6ad2982024-08-23 17:42:49 +0400510 for _, ns := range rendered.Namespaces {
511 if ns.Name == "" {
512 return ReleaseResources{}, fmt.Errorf("namespace name missing")
513 }
514 if ns.Kubeconfig == "" {
515 continue
516 }
517 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
518 if err != nil {
519 return ReleaseResources{}, err
520 }
521 if err := nsc.Create(ns.Name); err != nil {
522 return ReleaseResources{}, err
523 }
524 }
525 if err := installApp(m.repo, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400526 return ReleaseResources{}, err
527 }
gioff2a29a2024-05-01 17:06:42 +0400528 // TODO(gio): add ingress-nginx to release resources
gioefa0ed42024-06-13 12:31:43 +0400529 if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400530 return ReleaseResources{}, err
531 }
giof6ad2982024-08-23 17:42:49 +0400532 for _, p := range rendered.ClusterProxies {
533 if err := m.cnc.AddProxy(p.From, p.To); err != nil {
534 return ReleaseResources{}, err
535 }
536 }
gio778577f2024-04-29 09:44:38 +0400537 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400538 Release: rendered.Config.Release,
539 RenderedRaw: rendered.Raw,
540 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400541 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400542}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400543
gio778577f2024-04-29 09:44:38 +0400544type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400545 Metadata struct {
546 Name string `json:"name"`
547 Namespace string `json:"namespace"`
548 Annotations map[string]string `json:"annotations"`
549 } `json:"metadata"`
550 Kind string `json:"kind"`
551 Status struct {
gio778577f2024-04-29 09:44:38 +0400552 Conditions []struct {
553 Type string `json:"type"`
554 Status string `json:"status"`
555 } `json:"conditions"`
556 } `json:"status,omitempty"`
557}
558
559func extractHelm(resources CueAppData) []Resource {
560 ret := make([]Resource, 0, len(resources))
561 for _, contents := range resources {
562 var h helmRelease
563 if err := yaml.Unmarshal(contents, &h); err != nil {
564 panic(err) // TODO(gio): handle
565 }
gio0eaf2712024-04-14 13:08:46 +0400566 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400567 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400568 Name: h.Metadata.Name,
569 Namespace: h.Metadata.Namespace,
570 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
571 Annotations: nil,
giof9f0bee2024-06-11 20:10:05 +0400572 }
573 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400574 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400575 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
576 if ok && len(info) != 0 {
577 res.Info = info
578 }
579 }
580 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400581 }
gio778577f2024-04-29 09:44:38 +0400582 }
583 return ret
584}
585
giof8843412024-05-22 16:38:05 +0400586// TODO(gio): take app configuration from the repo
587func (m *AppManager) Update(
588 instanceId string,
589 values map[string]any,
590 opts ...InstallOption,
591) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400592 m.l.Lock()
593 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400594 if err := m.repo.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400595 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400596 }
gio3cdee592024-04-17 10:15:56 +0400597 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400598 if err != nil {
gio778577f2024-04-29 09:44:38 +0400599 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400600 }
gio308105e2024-04-19 13:12:13 +0400601 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400602 app, err := m.GetInstanceApp(instanceId)
603 if err != nil {
604 return ReleaseResources{}, err
605 }
gio308105e2024-04-19 13:12:13 +0400606 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400607 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400608 if err != nil {
gio778577f2024-04-29 09:44:38 +0400609 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400610 }
giof6ad2982024-08-23 17:42:49 +0400611 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400612 if err != nil {
613 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400614 }
giocb34ad22024-07-11 08:01:13 +0400615 networks, err := m.CreateNetworks(env)
616 if err != nil {
617 return ReleaseResources{}, err
618 }
giof6ad2982024-08-23 17:42:49 +0400619 clusters, err := m.GetClusters()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400620 if err != nil {
gio778577f2024-04-29 09:44:38 +0400621 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400622 }
giof6ad2982024-08-23 17:42:49 +0400623 rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), values, renderedCfg.LocalCharts, m.vpnAPIClient)
624 if err != nil {
gio94904702024-07-26 16:58:34 +0400625 return ReleaseResources{}, err
626 }
giof6ad2982024-08-23 17:42:49 +0400627 for _, ns := range rendered.Namespaces {
628 if ns.Name == "" {
629 return ReleaseResources{}, fmt.Errorf("namespace name missing")
630 }
631 if ns.Kubeconfig == "" {
632 continue
633 }
634 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
635 if err != nil {
636 return ReleaseResources{}, err
637 }
638 if err := nsc.Create(ns.Name); err != nil {
639 return ReleaseResources{}, err
640 }
641 }
642 if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
643 return ReleaseResources{}, err
644 }
645 for _, ocp := range renderedCfg.Out.ClusterProxy {
646 found := false
647 for _, ncp := range rendered.ClusterProxies {
648 if ocp == ncp {
649 found = true
650 break
651 }
652 }
653 if !found {
654 if err := m.cnc.RemoveProxy(ocp.From, ocp.To); err != nil {
655 return ReleaseResources{}, err
656 }
657 }
658 }
659 for _, ncp := range rendered.ClusterProxies {
660 found := false
661 for _, ocp := range renderedCfg.Out.ClusterProxy {
662 if ocp == ncp {
663 found = true
664 break
665 }
666 }
667 if !found {
668 if err := m.cnc.AddProxy(ncp.From, ncp.To); err != nil {
669 return ReleaseResources{}, err
670 }
671 }
672 }
gio94904702024-07-26 16:58:34 +0400673 return ReleaseResources{
674 Release: rendered.Config.Release,
675 RenderedRaw: rendered.Raw,
676 Helm: extractHelm(rendered.Resources),
677 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400678}
679
680func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400681 m.l.Lock()
682 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400683 if err := m.repo.Pull(); err != nil {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400684 return err
685 }
gio864b4332024-09-05 13:56:47 +0400686 var cfg renderedInstance
giof6ad2982024-08-23 17:42:49 +0400687 if _, err := m.repo.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400688 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof6ad2982024-08-23 17:42:49 +0400689 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giocdfa3722024-06-13 20:10:14 +0400690 if err != nil {
691 return "", err
692 }
gio864b4332024-09-05 13:56:47 +0400693 cfg = renderedCfg
giof6ad2982024-08-23 17:42:49 +0400694 r.RemoveAll(instanceDir)
gio5887caa2024-10-03 15:07:23 +0400695 curr := instanceDir
gio829b1b72024-10-05 21:50:56 +0400696 for {
gio5887caa2024-10-03 15:07:23 +0400697 p := filepath.Dir(curr)
gio829b1b72024-10-05 21:50:56 +0400698 if p == curr {
699 break
700 }
gio5887caa2024-10-03 15:07:23 +0400701 n := filepath.Base(curr)
gio92116ca2024-10-06 13:55:46 +0400702 kustPath := filepath.Join(p, kustomizationFileName)
gio5887caa2024-10-03 15:07:23 +0400703 kust, err := soft.ReadKustomization(r, kustPath)
704 if err != nil {
705 return "", err
706 }
707 kust.RemoveResources(n)
gio829b1b72024-10-05 21:50:56 +0400708 if len(kust.Resources) > 0 || p == m.appDirRoot {
gio5887caa2024-10-03 15:07:23 +0400709 soft.WriteYaml(r, kustPath, kust)
710 break
711 } else {
712 if err := r.RemoveAll(kustPath); err != nil {
713 return "", err
714 }
715 }
gio5887caa2024-10-03 15:07:23 +0400716 curr = p
gio3af43942024-04-16 08:13:50 +0400717 }
gio3af43942024-04-16 08:13:50 +0400718 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400719 }); err != nil {
720 return err
721 }
gio864b4332024-09-05 13:56:47 +0400722 if err := closePorts(cfg.PortForward); err != nil {
giocdfa3722024-06-13 20:10:14 +0400723 return err
724 }
giof6ad2982024-08-23 17:42:49 +0400725 for _, cp := range cfg.Out.ClusterProxy {
726 if err := m.cnc.RemoveProxy(cp.From, cp.To); err != nil {
727 return err
728 }
729 }
gio864b4332024-09-05 13:56:47 +0400730 for vmName, vmCfg := range cfg.Out.VM {
731 if vmCfg.VPN.Enabled {
gio92116ca2024-10-06 13:55:46 +0400732 // Not found error is ignored as VM might have not had enough time to boot before uninstalling it.
733 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400734 return err
735 }
736 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
737 return err
738 }
gio92116ca2024-10-06 13:55:46 +0400739 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400740 return err
741 }
742 }
743 }
giocdfa3722024-06-13 20:10:14 +0400744 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400745}
746
giocb34ad22024-07-11 08:01:13 +0400747func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
748 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400749 {
giocdfa3722024-06-13 20:10:14 +0400750 Name: "Public",
751 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
752 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
753 Domain: env.Domain,
754 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
755 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
756 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400757 },
gio7841f4f2024-07-26 19:53:49 +0400758 }
759 if env.PrivateDomain != "" {
760 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400761 Name: "Private",
762 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
763 Domain: env.PrivateDomain,
764 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
765 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
766 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400767 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400768 }
gio7fbd4ad2024-08-27 10:06:39 +0400769 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400770 if err != nil {
771 return nil, err
772 }
773 for _, a := range n {
774 ret = append(ret, Network{
775 Name: a.Input["name"].(string),
776 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
777 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
778 Domain: a.Input["domain"].(string),
779 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
780 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
781 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
782 })
783 }
784 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400785}
gio3cdee592024-04-17 10:15:56 +0400786
giof6ad2982024-08-23 17:42:49 +0400787func (m *AppManager) GetClusters() ([]cluster.State, error) {
788 ret := []cluster.State{
789 {
790 Name: "default",
791 },
792 }
793 files, err := m.repo.ListDir("/clusters")
794 if err != nil {
795 if errors.Is(err, fs.ErrNotExist) {
796 return ret, nil
797 }
798 return nil, err
799 }
800 for _, f := range files {
801 if !f.IsDir() {
802 continue
803 }
804 cfgPath := filepath.Clean(filepath.Join("/clusters", f.Name(), "config.json"))
805 var c cluster.State
806 if err := soft.ReadJson(m.repo, cfgPath, &c); err != nil {
807 if errors.Is(err, fs.ErrNotExist) {
808 continue
809 }
810 return nil, err
811 }
812 ret = append(ret, c)
813 }
814 return ret, nil
815}
816
gio0eaf2712024-04-14 13:08:46 +0400817type installOptions struct {
gio94904702024-07-26 16:58:34 +0400818 NoPull bool
giof8843412024-05-22 16:38:05 +0400819 NoPublish bool
820 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400821 Networks []Network
giof15b9da2024-09-19 06:59:16 +0400822 Clusters []Cluster
giof8843412024-05-22 16:38:05 +0400823 Branch string
824 LG LocalChartGenerator
825 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400826 Force bool
gio9d66f322024-07-06 13:45:10 +0400827 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400828}
829
830type InstallOption func(*installOptions)
831
832func WithConfig(env *EnvConfig) InstallOption {
833 return func(o *installOptions) {
834 o.Env = env
835 }
836}
837
giocb34ad22024-07-11 08:01:13 +0400838func WithNetworks(networks []Network) InstallOption {
839 return func(o *installOptions) {
840 o.Networks = networks
841 }
842}
843
gio23bdc1b2024-07-11 16:07:47 +0400844func WithNoNetworks() InstallOption {
845 return WithNetworks([]Network{})
846}
847
giof15b9da2024-09-19 06:59:16 +0400848func WithClusters(clusters []Cluster) InstallOption {
849 return func(o *installOptions) {
850 o.Clusters = clusters
851 }
852}
853
gio0eaf2712024-04-14 13:08:46 +0400854func WithBranch(branch string) InstallOption {
855 return func(o *installOptions) {
856 o.Branch = branch
857 }
858}
859
giof71a0832024-06-27 14:45:45 +0400860func WithForce() InstallOption {
861 return func(o *installOptions) {
862 o.Force = true
863 }
864}
865
giof8843412024-05-22 16:38:05 +0400866func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
867 return func(o *installOptions) {
868 o.LG = lg
869 }
870}
871
872func WithFetchContainerImages() InstallOption {
873 return func(o *installOptions) {
874 o.FetchContainerImages = true
875 }
876}
877
878func WithNoPublish() InstallOption {
879 return func(o *installOptions) {
880 o.NoPublish = true
881 }
882}
883
gio94904702024-07-26 16:58:34 +0400884func WithNoPull() InstallOption {
885 return func(o *installOptions) {
886 o.NoPull = true
887 }
888}
889
gio9d66f322024-07-06 13:45:10 +0400890func WithNoLock() InstallOption {
891 return func(o *installOptions) {
892 o.NoLock = true
893 }
894}
895
giof8843412024-05-22 16:38:05 +0400896// InfraAppmanager
897
898type InfraAppManager struct {
899 repoIO soft.RepoIO
900 nsc NamespaceCreator
901 hf HelmFetcher
902 lg LocalChartGenerator
903}
904
905func NewInfraAppManager(
906 repoIO soft.RepoIO,
907 nsc NamespaceCreator,
908 hf HelmFetcher,
909 lg LocalChartGenerator,
910) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400911 return &InfraAppManager{
912 repoIO,
giof8843412024-05-22 16:38:05 +0400913 nsc,
914 hf,
915 lg,
gio3cdee592024-04-17 10:15:56 +0400916 }, nil
917}
918
919func (m *InfraAppManager) Config() (InfraConfig, error) {
920 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400921 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400922 return InfraConfig{}, err
923 } else {
924 return cfg, nil
925 }
926}
927
gioe72b54f2024-04-22 10:44:41 +0400928func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
929 var cfg InfraAppInstanceConfig
930 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
931 return InfraAppInstanceConfig{}, err
932 } else {
933 return cfg, nil
934 }
935}
936
937func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400938 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", kustomizationFileName))
gioe72b54f2024-04-22 10:44:41 +0400939 if err != nil {
940 return InfraAppInstanceConfig{}, err
941 }
942 for _, app := range kust.Resources {
943 if app == id {
944 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
945 if err != nil {
946 return InfraAppInstanceConfig{}, err
947 }
948 cfg.Id = id
949 return cfg, nil
950 }
951 }
952 return InfraAppInstanceConfig{}, nil
953}
954
gio778577f2024-04-29 09:44:38 +0400955func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400956 appDir = filepath.Clean(appDir)
957 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400958 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400959 }
giof8843412024-05-22 16:38:05 +0400960 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400961 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400962 }
963 infra, err := m.Config()
964 if err != nil {
gio778577f2024-04-29 09:44:38 +0400965 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400966 }
967 release := Release{
968 Namespace: namespace,
969 RepoAddr: m.repoIO.FullAddress(),
970 AppDir: appDir,
971 }
gio7841f4f2024-07-26 19:53:49 +0400972 networks := m.CreateNetworks(infra)
973 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +0400974 if err != nil {
975 return ReleaseResources{}, err
976 }
giof71a0832024-06-27 14:45:45 +0400977 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
978 if err != nil {
giof8843412024-05-22 16:38:05 +0400979 return ReleaseResources{}, err
980 }
giof71a0832024-06-27 14:45:45 +0400981 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +0400982 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400983 if err != nil {
gio778577f2024-04-29 09:44:38 +0400984 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400985 }
gio94904702024-07-26 16:58:34 +0400986 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
987 return ReleaseResources{}, err
988 }
989 return ReleaseResources{
990 Release: rendered.Config.Release,
991 RenderedRaw: rendered.Raw,
992 Helm: extractHelm(rendered.Resources),
993 }, nil
gioe72b54f2024-04-22 10:44:41 +0400994}
995
giof8843412024-05-22 16:38:05 +0400996// TODO(gio): take app configuration from the repo
997func (m *InfraAppManager) Update(
998 instanceId string,
999 values map[string]any,
1000 opts ...InstallOption,
1001) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +04001002 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001003 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001004 }
gio7841f4f2024-07-26 19:53:49 +04001005 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +04001006 if err != nil {
gio778577f2024-04-29 09:44:38 +04001007 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001008 }
1009 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +04001010 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
1011 if err != nil {
1012 return ReleaseResources{}, err
1013 }
1014 app, err := NewCueInfraApp(appCfg)
1015 if err != nil {
1016 return ReleaseResources{}, err
1017 }
gioe72b54f2024-04-22 10:44:41 +04001018 instanceConfigPath := filepath.Join(instanceDir, "config.json")
1019 config, err := m.appConfig(instanceConfigPath)
1020 if err != nil {
gio778577f2024-04-29 09:44:38 +04001021 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001022 }
giocdfa3722024-06-13 20:10:14 +04001023 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +04001024 if err != nil {
1025 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001026 }
gio7841f4f2024-07-26 19:53:49 +04001027 networks := m.CreateNetworks(infra)
1028 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +04001029 if err != nil {
gio778577f2024-04-29 09:44:38 +04001030 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001031 }
gio94904702024-07-26 16:58:34 +04001032 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
1033 return ReleaseResources{}, err
1034 }
1035 return ReleaseResources{
1036 Release: rendered.Config.Release,
1037 RenderedRaw: rendered.Raw,
1038 Helm: extractHelm(rendered.Resources),
1039 }, nil
gio3cdee592024-04-17 10:15:56 +04001040}
giof8843412024-05-22 16:38:05 +04001041
gio7841f4f2024-07-26 19:53:49 +04001042func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
1043 return []InfraNetwork{
1044 {
1045 Name: "Public",
1046 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
1047 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
1048 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
1049 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
1050 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
1051 },
1052 }
1053}
1054
giof8843412024-05-22 16:38:05 +04001055func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
1056 ret := make(map[string]string)
1057 for name, chart := range charts.Git {
1058 chartRoot := filepath.Join(root, name)
1059 ret[name] = chartRoot
1060 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
1061 return nil, err
1062 }
1063 }
1064 return ret, nil
1065}
1066
1067func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
1068 ret := make(map[string]helmv2.HelmChartTemplateSpec)
1069 for name, path := range charts {
1070 ret[name] = g.Generate(path)
1071 }
1072 return ret
1073}
1074
1075func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
1076 for _, img := range imgs {
1077 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
1078 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
1079 "skopeo",
1080 "--insecure-policy",
1081 "copy",
1082 "--dest-tls-verify=false", // TODO(gio): enable
1083 "--multi-arch=all",
1084 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
1085 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
1086 }); err != nil {
1087 return err
1088 }
1089 }
1090 return nil
1091}
1092
1093type renderedInstance struct {
1094 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
giocdfa3722024-06-13 20:10:14 +04001095 PortForward []PortForward `json:"portForward"`
gio864b4332024-09-05 13:56:47 +04001096 Out outRendered `json:"out"`
1097}
1098
1099type outRendered struct {
giof6ad2982024-08-23 17:42:49 +04001100 ClusterProxy map[string]ClusterProxy
1101 VM map[string]vmRendered `json:"vm"`
gio864b4332024-09-05 13:56:47 +04001102}
1103
1104type vmRendered struct {
1105 Username string `json:"username"`
1106 VPN struct {
1107 Enabled bool `json:"enabled"`
1108 AuthKey string `json:"authKey"`
1109 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +04001110}
1111
giocdfa3722024-06-13 20:10:14 +04001112func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +04001113 r, err := fs.Reader(path)
1114 if err != nil {
giocdfa3722024-06-13 20:10:14 +04001115 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001116 }
1117 defer r.Close()
1118 var cfg renderedInstance
1119 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +04001120 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001121 }
giocdfa3722024-06-13 20:10:14 +04001122 return cfg, nil
giof8843412024-05-22 16:38:05 +04001123}
gioefa0ed42024-06-13 12:31:43 +04001124
1125func findPortFields(scm Schema) []string {
1126 switch scm.Kind() {
1127 case KindBoolean:
1128 return []string{}
1129 case KindInt:
1130 return []string{}
1131 case KindString:
1132 return []string{}
1133 case KindStruct:
1134 ret := []string{}
1135 for _, f := range scm.Fields() {
1136 for _, p := range findPortFields(f.Schema) {
1137 if p == "" {
1138 ret = append(ret, f.Name)
1139 } else {
1140 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1141 }
1142 }
1143 }
1144 return ret
1145 case KindNetwork:
1146 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001147 case KindMultiNetwork:
1148 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001149 case KindAuth:
1150 return []string{}
1151 case KindSSHKey:
1152 return []string{}
1153 case KindNumber:
1154 return []string{}
1155 case KindArrayString:
1156 return []string{}
1157 case KindPort:
1158 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001159 case KindVPNAuthKey:
1160 return []string{}
giof6ad2982024-08-23 17:42:49 +04001161 case KindCluster:
1162 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001163 default:
1164 panic("MUST NOT REACH!")
1165 }
1166}
1167
1168func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1169 for p, r := range ports {
1170 if err := setPortField(values, p, r.Port); err != nil {
1171 return err
1172 }
1173 }
1174 return nil
1175}
1176
1177func setPortField(values map[string]any, field string, port int) error {
1178 f := strings.SplitN(field, ".", 2)
1179 if len(f) == 2 {
1180 var sub map[string]any
1181 if s, ok := values[f[0]]; ok {
1182 sub, ok = s.(map[string]any)
1183 if !ok {
1184 return fmt.Errorf("expected map")
1185 }
1186 } else {
1187 sub = map[string]any{}
1188 values[f[0]] = sub
1189 }
1190 if err := setPortField(sub, f[1], port); err != nil {
1191 return err
1192 }
1193 } else {
1194 values[f[0]] = port
1195 }
1196 return nil
1197}
giof6ad2982024-08-23 17:42:49 +04001198
1199type Cluster struct {
1200 Name string `json:"name"`
1201 Kubeconfig string `json:"kubeconfig"`
1202 IngressClassName string `json:"ingressClassName"`
1203}
1204
1205func ClusterStateToAccessConfig(c cluster.State) Cluster {
1206 return Cluster{
1207 Name: c.Name,
1208 Kubeconfig: c.Kubeconfig,
1209 IngressClassName: c.IngressClassName,
1210 }
1211}
1212
1213func ToAccessConfigs(clusters []cluster.State) []Cluster {
1214 ret := make([]Cluster, 0, len(clusters))
1215 for _, c := range clusters {
1216 ret = append(ret, ClusterStateToAccessConfig(c))
1217 }
1218 return ret
1219}