blob: 3285c19cfd1c485e9abdddd5fc1262152192bce4 [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
gio721c0042025-04-03 11:56:36 +0400185type reservePortInfo struct {
186 reserveAddr string
187 RemoteProxy bool `json:"remoteProxy"`
188}
189
190func reservePorts(ports map[string]reservePortInfo) (map[string]reservePortResp, error) {
gioefa0ed42024-06-13 12:31:43 +0400191 ret := map[string]reservePortResp{}
gio721c0042025-04-03 11:56:36 +0400192 for p, cfg := range ports {
193 var buf bytes.Buffer
194 if err := json.NewEncoder(&buf).Encode(cfg); err != nil {
195 return nil, err
196 }
197 resp, err := http.Post(cfg.reserveAddr, "application/json", &buf)
gioefa0ed42024-06-13 12:31:43 +0400198 if err != nil {
199 return nil, err
200 }
201 if resp.StatusCode != http.StatusOK {
202 var e bytes.Buffer
203 io.Copy(&e, resp.Body)
204 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
205 }
206 var r reservePortResp
207 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
208 return nil, err
209 }
210 ret[p] = r
211 }
212 return ret, nil
213}
214
gio802311e2024-11-04 08:37:34 +0400215func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string, ns string) error {
gio3af43942024-04-16 08:13:50 +0400216 for _, p := range ports {
gio721c0042025-04-03 11:56:36 +0400217 var target string
218 if p.Cluster == "" {
219 target = fmt.Sprintf("%s/%s", ns, p.Service.Name)
220 } else {
221 target = p.Service.Name
222 }
gio3af43942024-04-16 08:13:50 +0400223 var buf bytes.Buffer
224 req := allocatePortReq{
225 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400226 SourcePort: p.Port,
gio721c0042025-04-03 11:56:36 +0400227 TargetService: target,
gio802311e2024-11-04 08:37:34 +0400228 TargetPort: p.Service.Port,
gio3af43942024-04-16 08:13:50 +0400229 }
gioefa0ed42024-06-13 12:31:43 +0400230 allocator := ""
231 for n, r := range reservations {
gio802311e2024-11-04 08:37:34 +0400232 if p.Port == r.Port {
gioefa0ed42024-06-13 12:31:43 +0400233 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400234 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400235 break
236 }
237 }
238 if allocator == "" {
gio802311e2024-11-04 08:37:34 +0400239 return fmt.Errorf("Could not find allocator for: %d", p.Port)
gioefa0ed42024-06-13 12:31:43 +0400240 }
giobd7ab0b2024-06-17 12:55:17 +0400241 if err := json.NewEncoder(&buf).Encode(req); err != nil {
242 return err
243 }
gioefa0ed42024-06-13 12:31:43 +0400244 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400245 if err != nil {
246 return err
247 }
248 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400249 var r bytes.Buffer
250 io.Copy(&r, resp.Body)
gio802311e2024-11-04 08:37:34 +0400251 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 +0400252 }
253 }
254 return nil
255}
256
gio802311e2024-11-04 08:37:34 +0400257func closePorts(ports []PortForward, ns string) error {
giocdfa3722024-06-13 20:10:14 +0400258 var retErr error
259 for _, p := range ports {
260 var buf bytes.Buffer
261 req := removePortReq{
262 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400263 SourcePort: p.Port,
264 TargetService: fmt.Sprintf("%s/%s", ns, p.Service.Name),
265 TargetPort: p.Service.Port,
giocdfa3722024-06-13 20:10:14 +0400266 }
267 if err := json.NewEncoder(&buf).Encode(req); err != nil {
268 retErr = err
269 continue
270 }
271 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
272 if err != nil {
273 retErr = err
274 continue
275 }
276 if resp.StatusCode != http.StatusOK {
gio802311e2024-11-04 08:37:34 +0400277 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.Port, resp.StatusCode)
giocdfa3722024-06-13 20:10:14 +0400278 continue
279 }
280 }
281 return retErr
282}
283
gioe72b54f2024-04-22 10:44:41 +0400284func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400285 for p := filepath.Clean(path); p != "/"; {
286 parent, child := filepath.Split(p)
gio92116ca2024-10-06 13:55:46 +0400287 kustPath := filepath.Join(parent, kustomizationFileName)
gioe72b54f2024-04-22 10:44:41 +0400288 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400289 if err != nil {
290 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400291 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400292 kust = &k
293 } else {
294 return err
295 }
296 }
297 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400298 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400299 return err
300 }
301 p = filepath.Clean(parent)
302 }
303 return nil
304}
305
gio778577f2024-04-29 09:44:38 +0400306type Resource struct {
giob4a3a192024-08-19 09:55:47 +0400307 Name string `json:"name"`
308 Namespace string `json:"namespace"`
309 Info string `json:"info"`
310 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400311}
312
313type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400314 Release Release
315 Helm []Resource
316 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400317}
318
gio3cdee592024-04-17 10:15:56 +0400319// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400320func installApp(
gioe72b54f2024-04-22 10:44:41 +0400321 repo soft.RepoIO,
322 appDir string,
323 name string,
324 config any,
gioe72b54f2024-04-22 10:44:41 +0400325 resources CueAppData,
326 data CueAppData,
giof8843412024-05-22 16:38:05 +0400327 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400328) error {
giof8843412024-05-22 16:38:05 +0400329 var o installOptions
330 for _, i := range opts {
331 i(&o)
332 }
333 dopts := []soft.DoOption{}
334 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400335 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
336 }
gio94904702024-07-26 16:58:34 +0400337 if o.NoPull {
338 dopts = append(dopts, soft.WithNoPull())
339 }
giof8843412024-05-22 16:38:05 +0400340 if o.NoPublish {
341 dopts = append(dopts, soft.WithNoCommit())
342 }
giof71a0832024-06-27 14:45:45 +0400343 if o.Force {
344 dopts = append(dopts, soft.WithForce())
345 }
gio9d66f322024-07-06 13:45:10 +0400346 if o.NoLock {
347 dopts = append(dopts, soft.WithNoLock())
348 }
giob4a3a192024-08-19 09:55:47 +0400349 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
giof6ad2982024-08-23 17:42:49 +0400350 if err := r.RemoveAll(appDir); err != nil {
gio308105e2024-04-19 13:12:13 +0400351 return "", err
352 }
353 resourcesDir := path.Join(appDir, "resources")
354 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400355 return "", err
356 }
gio94904702024-07-26 16:58:34 +0400357 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400358 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400359 return err
gio5e49bb62024-07-20 10:43:19 +0400360 }
gioe72b54f2024-04-22 10:44:41 +0400361 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400362 return err
gio3af43942024-04-16 08:13:50 +0400363 }
gioe72b54f2024-04-22 10:44:41 +0400364 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400365 return err
gio308105e2024-04-19 13:12:13 +0400366 }
gioe72b54f2024-04-22 10:44:41 +0400367 for name, contents := range data {
gio92116ca2024-10-06 13:55:46 +0400368 if name == "config.json" || name == kustomizationFileName || name == "resources" {
gio94904702024-07-26 16:58:34 +0400369 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400370 }
371 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400372 if err != nil {
gio94904702024-07-26 16:58:34 +0400373 return err
gio3af43942024-04-16 08:13:50 +0400374 }
gio308105e2024-04-19 13:12:13 +0400375 defer w.Close()
376 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400377 return err
gio3af43942024-04-16 08:13:50 +0400378 }
379 }
gio94904702024-07-26 16:58:34 +0400380 return nil
381 }(); err != nil {
382 return "", err
gio308105e2024-04-19 13:12:13 +0400383 }
gio94904702024-07-26 16:58:34 +0400384 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400385 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400386 return err
gio308105e2024-04-19 13:12:13 +0400387 }
gioefa0ed42024-06-13 12:31:43 +0400388 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400389 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400390 appKust.AddResources(name)
391 w, err := r.Writer(path.Join(resourcesDir, name))
392 if err != nil {
gio94904702024-07-26 16:58:34 +0400393 return err
gio308105e2024-04-19 13:12:13 +0400394 }
395 defer w.Close()
396 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400397 return err
gio308105e2024-04-19 13:12:13 +0400398 }
399 }
gio92116ca2024-10-06 13:55:46 +0400400 if err := soft.WriteYaml(r, path.Join(resourcesDir, kustomizationFileName), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400401 return err
gio3af43942024-04-16 08:13:50 +0400402 }
gio94904702024-07-26 16:58:34 +0400403 return nil
404 }(); err != nil {
405 return "", err
gio3af43942024-04-16 08:13:50 +0400406 }
gioe72b54f2024-04-22 10:44:41 +0400407 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400408 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400409 return err
gio3af43942024-04-16 08:13:50 +0400410}
411
gio3cdee592024-04-17 10:15:56 +0400412// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400413func (m *AppManager) Install(
414 app EnvApp,
415 instanceId string,
416 appDir string,
417 namespace string,
418 values map[string]any,
419 opts ...InstallOption,
420) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400421 o := &installOptions{}
422 for _, i := range opts {
423 i(o)
424 }
425 if !o.NoLock {
426 m.l.Lock()
427 defer m.l.Unlock()
428 }
gioefa0ed42024-06-13 12:31:43 +0400429 portFields := findPortFields(app.Schema())
430 fakeReservations := map[string]reservePortResp{}
431 for i, f := range portFields {
432 fakeReservations[f] = reservePortResp{Port: i}
433 }
434 if err := setPortFields(values, fakeReservations); err != nil {
435 return ReleaseResources{}, err
436 }
gio3af43942024-04-16 08:13:50 +0400437 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400438 if !o.NoPull {
giof6ad2982024-08-23 17:42:49 +0400439 if err := m.repo.Pull(); err != nil {
gio94904702024-07-26 16:58:34 +0400440 return ReleaseResources{}, err
441 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400442 }
gio94904702024-07-26 16:58:34 +0400443 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400444 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400445 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400446 }
gio0eaf2712024-04-14 13:08:46 +0400447 var env EnvConfig
448 if o.Env != nil {
449 env = *o.Env
450 } else {
451 var err error
452 env, err = m.Config()
453 if err != nil {
454 return ReleaseResources{}, err
455 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400456 }
giocb34ad22024-07-11 08:01:13 +0400457 var networks []Network
458 if o.Networks != nil {
459 networks = o.Networks
460 } else {
461 var err error
462 networks, err = m.CreateNetworks(env)
463 if err != nil {
464 return ReleaseResources{}, err
465 }
466 }
giof15b9da2024-09-19 06:59:16 +0400467 var clusters []Cluster
468 if o.Clusters != nil {
469 clusters = o.Clusters
470 } else {
471 if cls, err := m.GetClusters(); err != nil {
472 return ReleaseResources{}, err
473 } else {
474 clusters = ToAccessConfigs(cls)
475 }
giof6ad2982024-08-23 17:42:49 +0400476 }
giof8843412024-05-22 16:38:05 +0400477 var lg LocalChartGenerator
478 if o.LG != nil {
479 lg = o.LG
480 } else {
481 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
482 }
gio3cdee592024-04-17 10:15:56 +0400483 release := Release{
484 AppInstanceId: instanceId,
485 Namespace: namespace,
giof6ad2982024-08-23 17:42:49 +0400486 RepoAddr: m.repo.FullAddress(),
gio3cdee592024-04-17 10:15:56 +0400487 AppDir: appDir,
488 }
giof15b9da2024-09-19 06:59:16 +0400489 rendered, err := app.Render(release, env, networks, clusters, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400490 if err != nil {
gio778577f2024-04-29 09:44:38 +0400491 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400492 }
gio721c0042025-04-03 11:56:36 +0400493 reservators := map[string]reservePortInfo{}
gioefa0ed42024-06-13 12:31:43 +0400494 allocators := map[string]string{}
495 for _, pf := range rendered.Ports {
gio721c0042025-04-03 11:56:36 +0400496 reservators[portFields[pf.Port]] = reservePortInfo{
497 reserveAddr: pf.ReserveAddr,
498 RemoteProxy: pf.Cluster != "",
499 }
gio802311e2024-11-04 08:37:34 +0400500 allocators[portFields[pf.Port]] = pf.Allocator
gioefa0ed42024-06-13 12:31:43 +0400501 }
502 portReservations, err := reservePorts(reservators)
503 if err != nil {
504 return ReleaseResources{}, err
505 }
506 if err := setPortFields(values, portReservations); err != nil {
507 return ReleaseResources{}, err
508 }
gio7841f4f2024-07-26 19:53:49 +0400509 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400510 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
511 if o.FetchContainerImages {
512 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
513 return ReleaseResources{}, err
514 }
gio0eaf2712024-04-14 13:08:46 +0400515 }
giof6ad2982024-08-23 17:42:49 +0400516 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
giof71a0832024-06-27 14:45:45 +0400517 if err != nil {
giof8843412024-05-22 16:38:05 +0400518 return ReleaseResources{}, err
519 }
giof71a0832024-06-27 14:45:45 +0400520 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400521 if o.FetchContainerImages {
522 release.ImageRegistry = imageRegistry
523 }
giof15b9da2024-09-19 06:59:16 +0400524 rendered, err = app.Render(release, env, networks, clusters, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400525 if err != nil {
526 return ReleaseResources{}, err
527 }
giof6ad2982024-08-23 17:42:49 +0400528 for _, ns := range rendered.Namespaces {
529 if ns.Name == "" {
530 return ReleaseResources{}, fmt.Errorf("namespace name missing")
531 }
532 if ns.Kubeconfig == "" {
533 continue
534 }
535 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
536 if err != nil {
537 return ReleaseResources{}, err
538 }
539 if err := nsc.Create(ns.Name); err != nil {
540 return ReleaseResources{}, err
541 }
542 }
543 if err := installApp(m.repo, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400544 return ReleaseResources{}, err
545 }
gioff2a29a2024-05-01 17:06:42 +0400546 // TODO(gio): add ingress-nginx to release resources
gio802311e2024-11-04 08:37:34 +0400547 if err := openPorts(rendered.Ports, portReservations, allocators, release.Namespace); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400548 return ReleaseResources{}, err
549 }
giof6ad2982024-08-23 17:42:49 +0400550 for _, p := range rendered.ClusterProxies {
gio721c0042025-04-03 11:56:36 +0400551 if err := m.cnc.AddIngressProxy(p.From, p.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400552 return ReleaseResources{}, err
553 }
554 }
gio778577f2024-04-29 09:44:38 +0400555 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400556 Release: rendered.Config.Release,
557 RenderedRaw: rendered.Raw,
558 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400559 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400560}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400561
gio778577f2024-04-29 09:44:38 +0400562type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400563 Metadata struct {
564 Name string `json:"name"`
565 Namespace string `json:"namespace"`
566 Annotations map[string]string `json:"annotations"`
567 } `json:"metadata"`
568 Kind string `json:"kind"`
569 Status struct {
gio778577f2024-04-29 09:44:38 +0400570 Conditions []struct {
571 Type string `json:"type"`
572 Status string `json:"status"`
573 } `json:"conditions"`
574 } `json:"status,omitempty"`
575}
576
577func extractHelm(resources CueAppData) []Resource {
578 ret := make([]Resource, 0, len(resources))
579 for _, contents := range resources {
580 var h helmRelease
581 if err := yaml.Unmarshal(contents, &h); err != nil {
582 panic(err) // TODO(gio): handle
583 }
gio0eaf2712024-04-14 13:08:46 +0400584 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400585 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400586 Name: h.Metadata.Name,
587 Namespace: h.Metadata.Namespace,
588 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
589 Annotations: nil,
giof9f0bee2024-06-11 20:10:05 +0400590 }
591 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400592 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400593 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
594 if ok && len(info) != 0 {
595 res.Info = info
596 }
597 }
598 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400599 }
gio778577f2024-04-29 09:44:38 +0400600 }
601 return ret
602}
603
giof8843412024-05-22 16:38:05 +0400604// TODO(gio): take app configuration from the repo
605func (m *AppManager) Update(
606 instanceId string,
607 values map[string]any,
608 opts ...InstallOption,
609) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400610 m.l.Lock()
611 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400612 if err := m.repo.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400613 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400614 }
gio3cdee592024-04-17 10:15:56 +0400615 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400616 if err != nil {
gio778577f2024-04-29 09:44:38 +0400617 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400618 }
gio308105e2024-04-19 13:12:13 +0400619 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400620 app, err := m.GetInstanceApp(instanceId)
621 if err != nil {
622 return ReleaseResources{}, err
623 }
gio308105e2024-04-19 13:12:13 +0400624 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400625 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400626 if err != nil {
gio778577f2024-04-29 09:44:38 +0400627 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400628 }
giof6ad2982024-08-23 17:42:49 +0400629 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400630 if err != nil {
631 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400632 }
giocb34ad22024-07-11 08:01:13 +0400633 networks, err := m.CreateNetworks(env)
634 if err != nil {
635 return ReleaseResources{}, err
636 }
giof6ad2982024-08-23 17:42:49 +0400637 clusters, err := m.GetClusters()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400638 if err != nil {
gio778577f2024-04-29 09:44:38 +0400639 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400640 }
giof6ad2982024-08-23 17:42:49 +0400641 rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), values, renderedCfg.LocalCharts, m.vpnAPIClient)
642 if err != nil {
gio94904702024-07-26 16:58:34 +0400643 return ReleaseResources{}, err
644 }
giof6ad2982024-08-23 17:42:49 +0400645 for _, ns := range rendered.Namespaces {
646 if ns.Name == "" {
647 return ReleaseResources{}, fmt.Errorf("namespace name missing")
648 }
649 if ns.Kubeconfig == "" {
650 continue
651 }
652 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
653 if err != nil {
654 return ReleaseResources{}, err
655 }
656 if err := nsc.Create(ns.Name); err != nil {
657 return ReleaseResources{}, err
658 }
659 }
660 if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
661 return ReleaseResources{}, err
662 }
663 for _, ocp := range renderedCfg.Out.ClusterProxy {
664 found := false
665 for _, ncp := range rendered.ClusterProxies {
666 if ocp == ncp {
667 found = true
668 break
669 }
670 }
671 if !found {
gio721c0042025-04-03 11:56:36 +0400672 if err := m.cnc.RemoveIngressProxy(ocp.From, ocp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400673 return ReleaseResources{}, err
674 }
675 }
676 }
677 for _, ncp := range rendered.ClusterProxies {
678 found := false
679 for _, ocp := range renderedCfg.Out.ClusterProxy {
680 if ocp == ncp {
681 found = true
682 break
683 }
684 }
685 if !found {
gio721c0042025-04-03 11:56:36 +0400686 if err := m.cnc.AddIngressProxy(ncp.From, ncp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400687 return ReleaseResources{}, err
688 }
689 }
690 }
gio94904702024-07-26 16:58:34 +0400691 return ReleaseResources{
692 Release: rendered.Config.Release,
693 RenderedRaw: rendered.Raw,
694 Helm: extractHelm(rendered.Resources),
695 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400696}
697
698func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400699 m.l.Lock()
700 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400701 if err := m.repo.Pull(); err != nil {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400702 return err
703 }
gio864b4332024-09-05 13:56:47 +0400704 var cfg renderedInstance
giof6ad2982024-08-23 17:42:49 +0400705 if _, err := m.repo.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400706 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof6ad2982024-08-23 17:42:49 +0400707 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giocdfa3722024-06-13 20:10:14 +0400708 if err != nil {
709 return "", err
710 }
gio864b4332024-09-05 13:56:47 +0400711 cfg = renderedCfg
giof6ad2982024-08-23 17:42:49 +0400712 r.RemoveAll(instanceDir)
gio5887caa2024-10-03 15:07:23 +0400713 curr := instanceDir
gio829b1b72024-10-05 21:50:56 +0400714 for {
gio5887caa2024-10-03 15:07:23 +0400715 p := filepath.Dir(curr)
gio829b1b72024-10-05 21:50:56 +0400716 if p == curr {
717 break
718 }
gio5887caa2024-10-03 15:07:23 +0400719 n := filepath.Base(curr)
gio92116ca2024-10-06 13:55:46 +0400720 kustPath := filepath.Join(p, kustomizationFileName)
gio5887caa2024-10-03 15:07:23 +0400721 kust, err := soft.ReadKustomization(r, kustPath)
722 if err != nil {
723 return "", err
724 }
725 kust.RemoveResources(n)
gio829b1b72024-10-05 21:50:56 +0400726 if len(kust.Resources) > 0 || p == m.appDirRoot {
gio5887caa2024-10-03 15:07:23 +0400727 soft.WriteYaml(r, kustPath, kust)
728 break
729 } else {
730 if err := r.RemoveAll(kustPath); err != nil {
731 return "", err
732 }
733 }
gio5887caa2024-10-03 15:07:23 +0400734 curr = p
gio3af43942024-04-16 08:13:50 +0400735 }
gio3af43942024-04-16 08:13:50 +0400736 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400737 }); err != nil {
738 return err
739 }
gio802311e2024-11-04 08:37:34 +0400740 if err := closePorts(cfg.Output.PortForward, cfg.Release.Namespace); err != nil {
giocdfa3722024-06-13 20:10:14 +0400741 return err
742 }
giof6ad2982024-08-23 17:42:49 +0400743 for _, cp := range cfg.Out.ClusterProxy {
gio721c0042025-04-03 11:56:36 +0400744 if err := m.cnc.RemoveIngressProxy(cp.From, cp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400745 return err
746 }
747 }
gio864b4332024-09-05 13:56:47 +0400748 for vmName, vmCfg := range cfg.Out.VM {
749 if vmCfg.VPN.Enabled {
gio92116ca2024-10-06 13:55:46 +0400750 // Not found error is ignored as VM might have not had enough time to boot before uninstalling it.
751 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400752 return err
753 }
754 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
755 return err
756 }
gio92116ca2024-10-06 13:55:46 +0400757 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400758 return err
759 }
760 }
761 }
giocdfa3722024-06-13 20:10:14 +0400762 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400763}
764
giocb34ad22024-07-11 08:01:13 +0400765func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
766 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400767 {
giocdfa3722024-06-13 20:10:14 +0400768 Name: "Public",
769 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
770 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
771 Domain: env.Domain,
772 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
773 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
774 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400775 },
gio7841f4f2024-07-26 19:53:49 +0400776 }
777 if env.PrivateDomain != "" {
778 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400779 Name: "Private",
780 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
781 Domain: env.PrivateDomain,
782 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
783 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
784 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400785 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400786 }
gio7fbd4ad2024-08-27 10:06:39 +0400787 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400788 if err != nil {
789 return nil, err
790 }
791 for _, a := range n {
792 ret = append(ret, Network{
793 Name: a.Input["name"].(string),
794 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
795 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
796 Domain: a.Input["domain"].(string),
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),
800 })
801 }
802 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400803}
gio3cdee592024-04-17 10:15:56 +0400804
giof6ad2982024-08-23 17:42:49 +0400805func (m *AppManager) GetClusters() ([]cluster.State, error) {
806 ret := []cluster.State{
807 {
808 Name: "default",
809 },
810 }
811 files, err := m.repo.ListDir("/clusters")
812 if err != nil {
813 if errors.Is(err, fs.ErrNotExist) {
814 return ret, nil
815 }
816 return nil, err
817 }
818 for _, f := range files {
819 if !f.IsDir() {
820 continue
821 }
822 cfgPath := filepath.Clean(filepath.Join("/clusters", f.Name(), "config.json"))
823 var c cluster.State
824 if err := soft.ReadJson(m.repo, cfgPath, &c); err != nil {
825 if errors.Is(err, fs.ErrNotExist) {
826 continue
827 }
828 return nil, err
829 }
830 ret = append(ret, c)
831 }
832 return ret, nil
833}
834
gio0eaf2712024-04-14 13:08:46 +0400835type installOptions struct {
gio94904702024-07-26 16:58:34 +0400836 NoPull bool
giof8843412024-05-22 16:38:05 +0400837 NoPublish bool
838 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400839 Networks []Network
giof15b9da2024-09-19 06:59:16 +0400840 Clusters []Cluster
giof8843412024-05-22 16:38:05 +0400841 Branch string
842 LG LocalChartGenerator
843 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400844 Force bool
gio9d66f322024-07-06 13:45:10 +0400845 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400846}
847
848type InstallOption func(*installOptions)
849
850func WithConfig(env *EnvConfig) InstallOption {
851 return func(o *installOptions) {
852 o.Env = env
853 }
854}
855
giocb34ad22024-07-11 08:01:13 +0400856func WithNetworks(networks []Network) InstallOption {
857 return func(o *installOptions) {
858 o.Networks = networks
859 }
860}
861
gio23bdc1b2024-07-11 16:07:47 +0400862func WithNoNetworks() InstallOption {
863 return WithNetworks([]Network{})
864}
865
giof15b9da2024-09-19 06:59:16 +0400866func WithClusters(clusters []Cluster) InstallOption {
867 return func(o *installOptions) {
868 o.Clusters = clusters
869 }
870}
871
gio0eaf2712024-04-14 13:08:46 +0400872func WithBranch(branch string) InstallOption {
873 return func(o *installOptions) {
874 o.Branch = branch
875 }
876}
877
giof71a0832024-06-27 14:45:45 +0400878func WithForce() InstallOption {
879 return func(o *installOptions) {
880 o.Force = true
881 }
882}
883
giof8843412024-05-22 16:38:05 +0400884func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
885 return func(o *installOptions) {
886 o.LG = lg
887 }
888}
889
890func WithFetchContainerImages() InstallOption {
891 return func(o *installOptions) {
892 o.FetchContainerImages = true
893 }
894}
895
896func WithNoPublish() InstallOption {
897 return func(o *installOptions) {
898 o.NoPublish = true
899 }
900}
901
gio94904702024-07-26 16:58:34 +0400902func WithNoPull() InstallOption {
903 return func(o *installOptions) {
904 o.NoPull = true
905 }
906}
907
gio9d66f322024-07-06 13:45:10 +0400908func WithNoLock() InstallOption {
909 return func(o *installOptions) {
910 o.NoLock = true
911 }
912}
913
giof8843412024-05-22 16:38:05 +0400914// InfraAppmanager
915
916type InfraAppManager struct {
917 repoIO soft.RepoIO
918 nsc NamespaceCreator
919 hf HelmFetcher
920 lg LocalChartGenerator
921}
922
923func NewInfraAppManager(
924 repoIO soft.RepoIO,
925 nsc NamespaceCreator,
926 hf HelmFetcher,
927 lg LocalChartGenerator,
928) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400929 return &InfraAppManager{
930 repoIO,
giof8843412024-05-22 16:38:05 +0400931 nsc,
932 hf,
933 lg,
gio3cdee592024-04-17 10:15:56 +0400934 }, nil
935}
936
937func (m *InfraAppManager) Config() (InfraConfig, error) {
938 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400939 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400940 return InfraConfig{}, err
941 } else {
942 return cfg, nil
943 }
944}
945
gioe72b54f2024-04-22 10:44:41 +0400946func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
947 var cfg InfraAppInstanceConfig
948 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
949 return InfraAppInstanceConfig{}, err
950 } else {
951 return cfg, nil
952 }
953}
954
955func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400956 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", kustomizationFileName))
gioe72b54f2024-04-22 10:44:41 +0400957 if err != nil {
958 return InfraAppInstanceConfig{}, err
959 }
960 for _, app := range kust.Resources {
961 if app == id {
962 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
963 if err != nil {
964 return InfraAppInstanceConfig{}, err
965 }
966 cfg.Id = id
967 return cfg, nil
968 }
969 }
970 return InfraAppInstanceConfig{}, nil
971}
972
gio778577f2024-04-29 09:44:38 +0400973func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400974 appDir = filepath.Clean(appDir)
975 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400976 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400977 }
giof8843412024-05-22 16:38:05 +0400978 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400979 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400980 }
981 infra, err := m.Config()
982 if err != nil {
gio778577f2024-04-29 09:44:38 +0400983 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400984 }
985 release := Release{
986 Namespace: namespace,
987 RepoAddr: m.repoIO.FullAddress(),
988 AppDir: appDir,
989 }
gio7841f4f2024-07-26 19:53:49 +0400990 networks := m.CreateNetworks(infra)
991 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +0400992 if err != nil {
993 return ReleaseResources{}, err
994 }
giof71a0832024-06-27 14:45:45 +0400995 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
996 if err != nil {
giof8843412024-05-22 16:38:05 +0400997 return ReleaseResources{}, err
998 }
giof71a0832024-06-27 14:45:45 +0400999 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +04001000 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +04001001 if err != nil {
gio778577f2024-04-29 09:44:38 +04001002 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001003 }
gio94904702024-07-26 16:58:34 +04001004 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
1005 return ReleaseResources{}, err
1006 }
1007 return ReleaseResources{
1008 Release: rendered.Config.Release,
1009 RenderedRaw: rendered.Raw,
1010 Helm: extractHelm(rendered.Resources),
1011 }, nil
gioe72b54f2024-04-22 10:44:41 +04001012}
1013
giof8843412024-05-22 16:38:05 +04001014// TODO(gio): take app configuration from the repo
1015func (m *InfraAppManager) Update(
1016 instanceId string,
1017 values map[string]any,
1018 opts ...InstallOption,
1019) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +04001020 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001021 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001022 }
gio7841f4f2024-07-26 19:53:49 +04001023 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +04001024 if err != nil {
gio778577f2024-04-29 09:44:38 +04001025 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001026 }
1027 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +04001028 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
1029 if err != nil {
1030 return ReleaseResources{}, err
1031 }
1032 app, err := NewCueInfraApp(appCfg)
1033 if err != nil {
1034 return ReleaseResources{}, err
1035 }
gioe72b54f2024-04-22 10:44:41 +04001036 instanceConfigPath := filepath.Join(instanceDir, "config.json")
1037 config, err := m.appConfig(instanceConfigPath)
1038 if err != nil {
gio778577f2024-04-29 09:44:38 +04001039 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001040 }
giocdfa3722024-06-13 20:10:14 +04001041 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +04001042 if err != nil {
1043 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001044 }
gio7841f4f2024-07-26 19:53:49 +04001045 networks := m.CreateNetworks(infra)
1046 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +04001047 if err != nil {
gio778577f2024-04-29 09:44:38 +04001048 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001049 }
gio94904702024-07-26 16:58:34 +04001050 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
1051 return ReleaseResources{}, err
1052 }
1053 return ReleaseResources{
1054 Release: rendered.Config.Release,
1055 RenderedRaw: rendered.Raw,
1056 Helm: extractHelm(rendered.Resources),
1057 }, nil
gio3cdee592024-04-17 10:15:56 +04001058}
giof8843412024-05-22 16:38:05 +04001059
gio7841f4f2024-07-26 19:53:49 +04001060func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
1061 return []InfraNetwork{
1062 {
1063 Name: "Public",
1064 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
1065 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
1066 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
1067 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
1068 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
1069 },
1070 }
1071}
1072
giof8843412024-05-22 16:38:05 +04001073func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
1074 ret := make(map[string]string)
1075 for name, chart := range charts.Git {
1076 chartRoot := filepath.Join(root, name)
1077 ret[name] = chartRoot
1078 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
1079 return nil, err
1080 }
1081 }
1082 return ret, nil
1083}
1084
1085func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
1086 ret := make(map[string]helmv2.HelmChartTemplateSpec)
1087 for name, path := range charts {
1088 ret[name] = g.Generate(path)
1089 }
1090 return ret
1091}
1092
1093func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
1094 for _, img := range imgs {
1095 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
1096 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
1097 "skopeo",
1098 "--insecure-policy",
1099 "copy",
1100 "--dest-tls-verify=false", // TODO(gio): enable
1101 "--multi-arch=all",
1102 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
1103 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
1104 }); err != nil {
1105 return err
1106 }
1107 }
1108 return nil
1109}
1110
1111type renderedInstance struct {
gio802311e2024-11-04 08:37:34 +04001112 Release Release `json:"release"`
giof8843412024-05-22 16:38:05 +04001113 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
gio864b4332024-09-05 13:56:47 +04001114 Out outRendered `json:"out"`
gio802311e2024-11-04 08:37:34 +04001115 Output outputRendered `json:"output"`
1116}
1117
1118type outputRendered struct {
1119 PortForward []PortForward `json:"openPort"`
gio864b4332024-09-05 13:56:47 +04001120}
1121
1122type outRendered struct {
giof6ad2982024-08-23 17:42:49 +04001123 ClusterProxy map[string]ClusterProxy
1124 VM map[string]vmRendered `json:"vm"`
gio864b4332024-09-05 13:56:47 +04001125}
1126
1127type vmRendered struct {
1128 Username string `json:"username"`
1129 VPN struct {
1130 Enabled bool `json:"enabled"`
1131 AuthKey string `json:"authKey"`
1132 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +04001133}
1134
giocdfa3722024-06-13 20:10:14 +04001135func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +04001136 r, err := fs.Reader(path)
1137 if err != nil {
giocdfa3722024-06-13 20:10:14 +04001138 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001139 }
1140 defer r.Close()
1141 var cfg renderedInstance
1142 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +04001143 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001144 }
giocdfa3722024-06-13 20:10:14 +04001145 return cfg, nil
giof8843412024-05-22 16:38:05 +04001146}
gioefa0ed42024-06-13 12:31:43 +04001147
1148func findPortFields(scm Schema) []string {
1149 switch scm.Kind() {
1150 case KindBoolean:
1151 return []string{}
1152 case KindInt:
1153 return []string{}
1154 case KindString:
1155 return []string{}
1156 case KindStruct:
1157 ret := []string{}
1158 for _, f := range scm.Fields() {
1159 for _, p := range findPortFields(f.Schema) {
1160 if p == "" {
1161 ret = append(ret, f.Name)
1162 } else {
1163 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1164 }
1165 }
1166 }
1167 return ret
1168 case KindNetwork:
1169 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001170 case KindMultiNetwork:
1171 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001172 case KindAuth:
1173 return []string{}
1174 case KindSSHKey:
1175 return []string{}
1176 case KindNumber:
1177 return []string{}
1178 case KindArrayString:
1179 return []string{}
1180 case KindPort:
1181 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001182 case KindVPNAuthKey:
1183 return []string{}
giof6ad2982024-08-23 17:42:49 +04001184 case KindCluster:
1185 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001186 default:
1187 panic("MUST NOT REACH!")
1188 }
1189}
1190
1191func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1192 for p, r := range ports {
1193 if err := setPortField(values, p, r.Port); err != nil {
1194 return err
1195 }
1196 }
1197 return nil
1198}
1199
1200func setPortField(values map[string]any, field string, port int) error {
1201 f := strings.SplitN(field, ".", 2)
1202 if len(f) == 2 {
1203 var sub map[string]any
1204 if s, ok := values[f[0]]; ok {
1205 sub, ok = s.(map[string]any)
1206 if !ok {
1207 return fmt.Errorf("expected map")
1208 }
1209 } else {
1210 sub = map[string]any{}
1211 values[f[0]] = sub
1212 }
1213 if err := setPortField(sub, f[1], port); err != nil {
1214 return err
1215 }
1216 } else {
1217 values[f[0]] = port
1218 }
1219 return nil
1220}
giof6ad2982024-08-23 17:42:49 +04001221
1222type Cluster struct {
1223 Name string `json:"name"`
1224 Kubeconfig string `json:"kubeconfig"`
1225 IngressClassName string `json:"ingressClassName"`
1226}
1227
1228func ClusterStateToAccessConfig(c cluster.State) Cluster {
1229 return Cluster{
1230 Name: c.Name,
1231 Kubeconfig: c.Kubeconfig,
1232 IngressClassName: c.IngressClassName,
1233 }
1234}
1235
1236func ToAccessConfigs(clusters []cluster.State) []Cluster {
1237 ret := make([]Cluster, 0, len(clusters))
1238 for _, c := range clusters {
1239 ret = append(ret, ClusterStateToAccessConfig(c))
1240 }
1241 return ret
1242}