blob: e6133c1f9d7d50044a3d357b22720a8afc5a4715 [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
gio63a1a822025-04-23 12:59:40 +0400139func GetCueAppData(fs soft.RepoFS, dir string, overrides CueAppData) (CueAppData, error) {
giof8843412024-05-22 16:38:05 +0400140 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 }
gio63a1a822025-04-23 12:59:40 +0400154 for k, v := range overrides {
155 cfg[k] = v
156 }
gio308105e2024-04-19 13:12:13 +0400157 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400158}
159
gio63a1a822025-04-23 12:59:40 +0400160func (m *AppManager) GetInstanceApp(id string, overrides CueAppData) (EnvApp, error) {
161 cfg, err := GetCueAppData(m.repo, filepath.Join(m.appDirRoot, id), overrides)
giof8843412024-05-22 16:38:05 +0400162 if err != nil {
163 return nil, err
164 }
165 return NewCueEnvApp(cfg)
166}
167
gio3af43942024-04-16 08:13:50 +0400168type allocatePortReq struct {
169 Protocol string `json:"protocol"`
170 SourcePort int `json:"sourcePort"`
171 TargetService string `json:"targetService"`
172 TargetPort int `json:"targetPort"`
giocdfa3722024-06-13 20:10:14 +0400173 Secret string `json:"secret,omitempty"`
174}
175
176type removePortReq struct {
177 Protocol string `json:"protocol"`
178 SourcePort int `json:"sourcePort"`
179 TargetService string `json:"targetService"`
180 TargetPort int `json:"targetPort"`
gio3af43942024-04-16 08:13:50 +0400181}
182
gioefa0ed42024-06-13 12:31:43 +0400183type reservePortResp struct {
184 Port int `json:"port"`
185 Secret string `json:"secret"`
186}
187
gio721c0042025-04-03 11:56:36 +0400188type reservePortInfo struct {
189 reserveAddr string
190 RemoteProxy bool `json:"remoteProxy"`
191}
192
193func reservePorts(ports map[string]reservePortInfo) (map[string]reservePortResp, error) {
gioefa0ed42024-06-13 12:31:43 +0400194 ret := map[string]reservePortResp{}
gio721c0042025-04-03 11:56:36 +0400195 for p, cfg := range ports {
196 var buf bytes.Buffer
197 if err := json.NewEncoder(&buf).Encode(cfg); err != nil {
198 return nil, err
199 }
200 resp, err := http.Post(cfg.reserveAddr, "application/json", &buf)
gioefa0ed42024-06-13 12:31:43 +0400201 if err != nil {
202 return nil, err
203 }
204 if resp.StatusCode != http.StatusOK {
205 var e bytes.Buffer
206 io.Copy(&e, resp.Body)
207 return nil, fmt.Errorf("Could not reserve port: %s", e.String())
208 }
209 var r reservePortResp
210 if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
211 return nil, err
212 }
213 ret[p] = r
214 }
215 return ret, nil
216}
217
gio802311e2024-11-04 08:37:34 +0400218func openPorts(ports []PortForward, reservations map[string]reservePortResp, allocators map[string]string, ns string) error {
gio3af43942024-04-16 08:13:50 +0400219 for _, p := range ports {
gio721c0042025-04-03 11:56:36 +0400220 var target string
221 if p.Cluster == "" {
giof4344632025-04-08 20:04:35 +0400222 if p.Service.Namespace == "" {
223 target = fmt.Sprintf("%s/%s", ns, p.Service.Name)
224 } else {
225 target = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
226 }
gio721c0042025-04-03 11:56:36 +0400227 } else {
228 target = p.Service.Name
229 }
gio3af43942024-04-16 08:13:50 +0400230 var buf bytes.Buffer
231 req := allocatePortReq{
232 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400233 SourcePort: p.Port,
gio721c0042025-04-03 11:56:36 +0400234 TargetService: target,
gio802311e2024-11-04 08:37:34 +0400235 TargetPort: p.Service.Port,
gio3af43942024-04-16 08:13:50 +0400236 }
gioefa0ed42024-06-13 12:31:43 +0400237 allocator := ""
238 for n, r := range reservations {
gio802311e2024-11-04 08:37:34 +0400239 if p.Port == r.Port {
gioefa0ed42024-06-13 12:31:43 +0400240 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400241 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400242 break
243 }
244 }
245 if allocator == "" {
gio802311e2024-11-04 08:37:34 +0400246 return fmt.Errorf("Could not find allocator for: %d", p.Port)
gioefa0ed42024-06-13 12:31:43 +0400247 }
giobd7ab0b2024-06-17 12:55:17 +0400248 if err := json.NewEncoder(&buf).Encode(req); err != nil {
249 return err
250 }
gioefa0ed42024-06-13 12:31:43 +0400251 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400252 if err != nil {
253 return err
254 }
255 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400256 var r bytes.Buffer
257 io.Copy(&r, resp.Body)
gio802311e2024-11-04 08:37:34 +0400258 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 +0400259 }
260 }
261 return nil
262}
263
gio802311e2024-11-04 08:37:34 +0400264func closePorts(ports []PortForward, ns string) error {
giocdfa3722024-06-13 20:10:14 +0400265 var retErr error
266 for _, p := range ports {
267 var buf bytes.Buffer
giof4344632025-04-08 20:04:35 +0400268 var fullName string
269 if p.Service.Namespace == "" {
270 fullName = fmt.Sprintf("%s/%s", ns, p.Service.Name)
271 } else {
272 fullName = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
273 }
giocdfa3722024-06-13 20:10:14 +0400274 req := removePortReq{
275 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400276 SourcePort: p.Port,
giof4344632025-04-08 20:04:35 +0400277 TargetService: fullName,
gio802311e2024-11-04 08:37:34 +0400278 TargetPort: p.Service.Port,
giocdfa3722024-06-13 20:10:14 +0400279 }
280 if err := json.NewEncoder(&buf).Encode(req); err != nil {
281 retErr = err
282 continue
283 }
giod78896a2025-04-10 07:42:13 +0400284 resp, err := http.Post(p.Network.DeallocatePortAddr, "application/json", &buf)
giocdfa3722024-06-13 20:10:14 +0400285 if err != nil {
286 retErr = err
287 continue
288 }
289 if resp.StatusCode != http.StatusOK {
gio802311e2024-11-04 08:37:34 +0400290 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.Port, resp.StatusCode)
giocdfa3722024-06-13 20:10:14 +0400291 continue
292 }
293 }
294 return retErr
295}
296
gioe72b54f2024-04-22 10:44:41 +0400297func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400298 for p := filepath.Clean(path); p != "/"; {
299 parent, child := filepath.Split(p)
gio92116ca2024-10-06 13:55:46 +0400300 kustPath := filepath.Join(parent, kustomizationFileName)
gioe72b54f2024-04-22 10:44:41 +0400301 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400302 if err != nil {
303 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400304 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400305 kust = &k
306 } else {
307 return err
308 }
309 }
310 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400311 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400312 return err
313 }
314 p = filepath.Clean(parent)
315 }
316 return nil
317}
318
gio778577f2024-04-29 09:44:38 +0400319type Resource struct {
giob4a3a192024-08-19 09:55:47 +0400320 Name string `json:"name"`
321 Namespace string `json:"namespace"`
322 Info string `json:"info"`
323 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400324}
325
326type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400327 Release Release
328 Helm []Resource
329 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400330}
331
gio3cdee592024-04-17 10:15:56 +0400332// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400333func installApp(
gioe72b54f2024-04-22 10:44:41 +0400334 repo soft.RepoIO,
335 appDir string,
336 name string,
337 config any,
gioe72b54f2024-04-22 10:44:41 +0400338 resources CueAppData,
339 data CueAppData,
giof8843412024-05-22 16:38:05 +0400340 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400341) error {
giof8843412024-05-22 16:38:05 +0400342 var o installOptions
343 for _, i := range opts {
344 i(&o)
345 }
346 dopts := []soft.DoOption{}
347 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400348 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
349 }
gio94904702024-07-26 16:58:34 +0400350 if o.NoPull {
351 dopts = append(dopts, soft.WithNoPull())
352 }
giof8843412024-05-22 16:38:05 +0400353 if o.NoPublish {
354 dopts = append(dopts, soft.WithNoCommit())
355 }
giof71a0832024-06-27 14:45:45 +0400356 if o.Force {
357 dopts = append(dopts, soft.WithForce())
358 }
gio9d66f322024-07-06 13:45:10 +0400359 if o.NoLock {
360 dopts = append(dopts, soft.WithNoLock())
361 }
giob4a3a192024-08-19 09:55:47 +0400362 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
giof6ad2982024-08-23 17:42:49 +0400363 if err := r.RemoveAll(appDir); err != nil {
gio308105e2024-04-19 13:12:13 +0400364 return "", err
365 }
366 resourcesDir := path.Join(appDir, "resources")
367 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400368 return "", err
369 }
gio94904702024-07-26 16:58:34 +0400370 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400371 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400372 return err
gio5e49bb62024-07-20 10:43:19 +0400373 }
gioe72b54f2024-04-22 10:44:41 +0400374 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400375 return err
gio3af43942024-04-16 08:13:50 +0400376 }
gioe72b54f2024-04-22 10:44:41 +0400377 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400378 return err
gio308105e2024-04-19 13:12:13 +0400379 }
gioe72b54f2024-04-22 10:44:41 +0400380 for name, contents := range data {
gio92116ca2024-10-06 13:55:46 +0400381 if name == "config.json" || name == kustomizationFileName || name == "resources" {
gio94904702024-07-26 16:58:34 +0400382 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400383 }
384 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400385 if err != nil {
gio94904702024-07-26 16:58:34 +0400386 return err
gio3af43942024-04-16 08:13:50 +0400387 }
gio308105e2024-04-19 13:12:13 +0400388 defer w.Close()
389 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400390 return err
gio3af43942024-04-16 08:13:50 +0400391 }
392 }
gio94904702024-07-26 16:58:34 +0400393 return nil
394 }(); err != nil {
395 return "", err
gio308105e2024-04-19 13:12:13 +0400396 }
gio94904702024-07-26 16:58:34 +0400397 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400398 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400399 return err
gio308105e2024-04-19 13:12:13 +0400400 }
gioefa0ed42024-06-13 12:31:43 +0400401 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400402 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400403 appKust.AddResources(name)
404 w, err := r.Writer(path.Join(resourcesDir, name))
405 if err != nil {
gio94904702024-07-26 16:58:34 +0400406 return err
gio308105e2024-04-19 13:12:13 +0400407 }
408 defer w.Close()
409 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400410 return err
gio308105e2024-04-19 13:12:13 +0400411 }
412 }
gio92116ca2024-10-06 13:55:46 +0400413 if err := soft.WriteYaml(r, path.Join(resourcesDir, kustomizationFileName), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400414 return err
gio3af43942024-04-16 08:13:50 +0400415 }
gio94904702024-07-26 16:58:34 +0400416 return nil
417 }(); err != nil {
418 return "", err
gio3af43942024-04-16 08:13:50 +0400419 }
gioe72b54f2024-04-22 10:44:41 +0400420 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400421 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400422 return err
gio3af43942024-04-16 08:13:50 +0400423}
424
gio3cdee592024-04-17 10:15:56 +0400425// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400426func (m *AppManager) Install(
427 app EnvApp,
428 instanceId string,
429 appDir string,
430 namespace string,
431 values map[string]any,
432 opts ...InstallOption,
433) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400434 o := &installOptions{}
435 for _, i := range opts {
436 i(o)
437 }
438 if !o.NoLock {
439 m.l.Lock()
440 defer m.l.Unlock()
441 }
gioefa0ed42024-06-13 12:31:43 +0400442 portFields := findPortFields(app.Schema())
443 fakeReservations := map[string]reservePortResp{}
444 for i, f := range portFields {
445 fakeReservations[f] = reservePortResp{Port: i}
446 }
447 if err := setPortFields(values, fakeReservations); err != nil {
448 return ReleaseResources{}, err
449 }
gio3af43942024-04-16 08:13:50 +0400450 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400451 if !o.NoPull {
giof6ad2982024-08-23 17:42:49 +0400452 if err := m.repo.Pull(); err != nil {
gio94904702024-07-26 16:58:34 +0400453 return ReleaseResources{}, err
454 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400455 }
gio94904702024-07-26 16:58:34 +0400456 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400457 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400458 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400459 }
gio0eaf2712024-04-14 13:08:46 +0400460 var env EnvConfig
461 if o.Env != nil {
462 env = *o.Env
463 } else {
464 var err error
465 env, err = m.Config()
466 if err != nil {
467 return ReleaseResources{}, err
468 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400469 }
giocb34ad22024-07-11 08:01:13 +0400470 var networks []Network
471 if o.Networks != nil {
472 networks = o.Networks
473 } else {
474 var err error
475 networks, err = m.CreateNetworks(env)
476 if err != nil {
477 return ReleaseResources{}, err
478 }
479 }
giof15b9da2024-09-19 06:59:16 +0400480 var clusters []Cluster
481 if o.Clusters != nil {
482 clusters = o.Clusters
483 } else {
484 if cls, err := m.GetClusters(); err != nil {
485 return ReleaseResources{}, err
486 } else {
487 clusters = ToAccessConfigs(cls)
488 }
giof6ad2982024-08-23 17:42:49 +0400489 }
giof8843412024-05-22 16:38:05 +0400490 var lg LocalChartGenerator
491 if o.LG != nil {
492 lg = o.LG
493 } else {
494 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
495 }
gio3cdee592024-04-17 10:15:56 +0400496 release := Release{
497 AppInstanceId: instanceId,
498 Namespace: namespace,
giof6ad2982024-08-23 17:42:49 +0400499 RepoAddr: m.repo.FullAddress(),
gio3cdee592024-04-17 10:15:56 +0400500 AppDir: appDir,
501 }
giof15b9da2024-09-19 06:59:16 +0400502 rendered, err := app.Render(release, env, networks, clusters, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400503 if err != nil {
gio778577f2024-04-29 09:44:38 +0400504 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400505 }
gio721c0042025-04-03 11:56:36 +0400506 reservators := map[string]reservePortInfo{}
gioefa0ed42024-06-13 12:31:43 +0400507 allocators := map[string]string{}
508 for _, pf := range rendered.Ports {
gio721c0042025-04-03 11:56:36 +0400509 reservators[portFields[pf.Port]] = reservePortInfo{
giod78896a2025-04-10 07:42:13 +0400510 reserveAddr: pf.Network.ReservePortAddr,
gio721c0042025-04-03 11:56:36 +0400511 RemoteProxy: pf.Cluster != "",
512 }
giod78896a2025-04-10 07:42:13 +0400513 allocators[portFields[pf.Port]] = pf.Network.AllocatePortAddr
gioefa0ed42024-06-13 12:31:43 +0400514 }
515 portReservations, err := reservePorts(reservators)
516 if err != nil {
517 return ReleaseResources{}, err
518 }
519 if err := setPortFields(values, portReservations); err != nil {
520 return ReleaseResources{}, err
521 }
gio7841f4f2024-07-26 19:53:49 +0400522 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400523 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
524 if o.FetchContainerImages {
525 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
526 return ReleaseResources{}, err
527 }
gio0eaf2712024-04-14 13:08:46 +0400528 }
giof6ad2982024-08-23 17:42:49 +0400529 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
giof71a0832024-06-27 14:45:45 +0400530 if err != nil {
giof8843412024-05-22 16:38:05 +0400531 return ReleaseResources{}, err
532 }
giof71a0832024-06-27 14:45:45 +0400533 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400534 if o.FetchContainerImages {
535 release.ImageRegistry = imageRegistry
536 }
giof15b9da2024-09-19 06:59:16 +0400537 rendered, err = app.Render(release, env, networks, clusters, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400538 if err != nil {
539 return ReleaseResources{}, err
540 }
giof6ad2982024-08-23 17:42:49 +0400541 for _, ns := range rendered.Namespaces {
542 if ns.Name == "" {
543 return ReleaseResources{}, fmt.Errorf("namespace name missing")
544 }
545 if ns.Kubeconfig == "" {
546 continue
547 }
548 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
549 if err != nil {
550 return ReleaseResources{}, err
551 }
552 if err := nsc.Create(ns.Name); err != nil {
553 return ReleaseResources{}, err
554 }
555 }
556 if err := installApp(m.repo, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400557 return ReleaseResources{}, err
558 }
gioff2a29a2024-05-01 17:06:42 +0400559 // TODO(gio): add ingress-nginx to release resources
gio802311e2024-11-04 08:37:34 +0400560 if err := openPorts(rendered.Ports, portReservations, allocators, release.Namespace); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400561 return ReleaseResources{}, err
562 }
giof6ad2982024-08-23 17:42:49 +0400563 for _, p := range rendered.ClusterProxies {
gio721c0042025-04-03 11:56:36 +0400564 if err := m.cnc.AddIngressProxy(p.From, p.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400565 return ReleaseResources{}, err
566 }
567 }
gio778577f2024-04-29 09:44:38 +0400568 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400569 Release: rendered.Config.Release,
570 RenderedRaw: rendered.Raw,
571 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400572 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400573}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400574
gio778577f2024-04-29 09:44:38 +0400575type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400576 Metadata struct {
577 Name string `json:"name"`
578 Namespace string `json:"namespace"`
579 Annotations map[string]string `json:"annotations"`
580 } `json:"metadata"`
581 Kind string `json:"kind"`
582 Status struct {
gio778577f2024-04-29 09:44:38 +0400583 Conditions []struct {
584 Type string `json:"type"`
585 Status string `json:"status"`
586 } `json:"conditions"`
587 } `json:"status,omitempty"`
588}
589
590func extractHelm(resources CueAppData) []Resource {
591 ret := make([]Resource, 0, len(resources))
592 for _, contents := range resources {
593 var h helmRelease
594 if err := yaml.Unmarshal(contents, &h); err != nil {
595 panic(err) // TODO(gio): handle
596 }
gio0eaf2712024-04-14 13:08:46 +0400597 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400598 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400599 Name: h.Metadata.Name,
600 Namespace: h.Metadata.Namespace,
601 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
602 Annotations: nil,
giof9f0bee2024-06-11 20:10:05 +0400603 }
604 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400605 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400606 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
607 if ok && len(info) != 0 {
608 res.Info = info
609 }
610 }
611 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400612 }
gio778577f2024-04-29 09:44:38 +0400613 }
614 return ret
615}
616
giof8843412024-05-22 16:38:05 +0400617// TODO(gio): take app configuration from the repo
618func (m *AppManager) Update(
619 instanceId string,
620 values map[string]any,
gio63a1a822025-04-23 12:59:40 +0400621 // TODO(gio): this should not be cue specific
622 overrides CueAppData,
giof8843412024-05-22 16:38:05 +0400623 opts ...InstallOption,
624) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400625 m.l.Lock()
626 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400627 if err := m.repo.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400628 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400629 }
gio3cdee592024-04-17 10:15:56 +0400630 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400631 if err != nil {
gio778577f2024-04-29 09:44:38 +0400632 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400633 }
gio308105e2024-04-19 13:12:13 +0400634 instanceDir := filepath.Join(m.appDirRoot, instanceId)
gio63a1a822025-04-23 12:59:40 +0400635 app, err := m.GetInstanceApp(instanceId, overrides)
giof8843412024-05-22 16:38:05 +0400636 if err != nil {
637 return ReleaseResources{}, err
638 }
gio308105e2024-04-19 13:12:13 +0400639 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400640 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400641 if err != nil {
gio778577f2024-04-29 09:44:38 +0400642 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400643 }
giof6ad2982024-08-23 17:42:49 +0400644 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400645 if err != nil {
646 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400647 }
giocb34ad22024-07-11 08:01:13 +0400648 networks, err := m.CreateNetworks(env)
649 if err != nil {
650 return ReleaseResources{}, err
651 }
giof6ad2982024-08-23 17:42:49 +0400652 clusters, err := m.GetClusters()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400653 if err != nil {
gio778577f2024-04-29 09:44:38 +0400654 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400655 }
gio63a1a822025-04-23 12:59:40 +0400656 rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), merge(config.Input, values), renderedCfg.LocalCharts, m.vpnAPIClient)
giof6ad2982024-08-23 17:42:49 +0400657 if err != nil {
gio94904702024-07-26 16:58:34 +0400658 return ReleaseResources{}, err
659 }
giof6ad2982024-08-23 17:42:49 +0400660 for _, ns := range rendered.Namespaces {
661 if ns.Name == "" {
662 return ReleaseResources{}, fmt.Errorf("namespace name missing")
663 }
664 if ns.Kubeconfig == "" {
665 continue
666 }
667 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
668 if err != nil {
669 return ReleaseResources{}, err
670 }
671 if err := nsc.Create(ns.Name); err != nil {
672 return ReleaseResources{}, err
673 }
674 }
675 if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
676 return ReleaseResources{}, err
677 }
678 for _, ocp := range renderedCfg.Out.ClusterProxy {
679 found := false
680 for _, ncp := range rendered.ClusterProxies {
681 if ocp == ncp {
682 found = true
683 break
684 }
685 }
686 if !found {
gio721c0042025-04-03 11:56:36 +0400687 if err := m.cnc.RemoveIngressProxy(ocp.From, ocp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400688 return ReleaseResources{}, err
689 }
690 }
691 }
692 for _, ncp := range rendered.ClusterProxies {
693 found := false
694 for _, ocp := range renderedCfg.Out.ClusterProxy {
695 if ocp == ncp {
696 found = true
697 break
698 }
699 }
700 if !found {
gio721c0042025-04-03 11:56:36 +0400701 if err := m.cnc.AddIngressProxy(ncp.From, ncp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400702 return ReleaseResources{}, err
703 }
704 }
705 }
gio94904702024-07-26 16:58:34 +0400706 return ReleaseResources{
707 Release: rendered.Config.Release,
708 RenderedRaw: rendered.Raw,
709 Helm: extractHelm(rendered.Resources),
710 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400711}
712
713func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400714 m.l.Lock()
715 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400716 if err := m.repo.Pull(); err != nil {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400717 return err
718 }
gio864b4332024-09-05 13:56:47 +0400719 var cfg renderedInstance
giof6ad2982024-08-23 17:42:49 +0400720 if _, err := m.repo.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400721 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof6ad2982024-08-23 17:42:49 +0400722 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giocdfa3722024-06-13 20:10:14 +0400723 if err != nil {
724 return "", err
725 }
gio864b4332024-09-05 13:56:47 +0400726 cfg = renderedCfg
giof6ad2982024-08-23 17:42:49 +0400727 r.RemoveAll(instanceDir)
gio5887caa2024-10-03 15:07:23 +0400728 curr := instanceDir
gio829b1b72024-10-05 21:50:56 +0400729 for {
gio5887caa2024-10-03 15:07:23 +0400730 p := filepath.Dir(curr)
gio829b1b72024-10-05 21:50:56 +0400731 if p == curr {
732 break
733 }
gio5887caa2024-10-03 15:07:23 +0400734 n := filepath.Base(curr)
gio92116ca2024-10-06 13:55:46 +0400735 kustPath := filepath.Join(p, kustomizationFileName)
gio5887caa2024-10-03 15:07:23 +0400736 kust, err := soft.ReadKustomization(r, kustPath)
737 if err != nil {
738 return "", err
739 }
740 kust.RemoveResources(n)
gio829b1b72024-10-05 21:50:56 +0400741 if len(kust.Resources) > 0 || p == m.appDirRoot {
gio5887caa2024-10-03 15:07:23 +0400742 soft.WriteYaml(r, kustPath, kust)
743 break
744 } else {
745 if err := r.RemoveAll(kustPath); err != nil {
746 return "", err
747 }
748 }
gio5887caa2024-10-03 15:07:23 +0400749 curr = p
gio3af43942024-04-16 08:13:50 +0400750 }
gio3af43942024-04-16 08:13:50 +0400751 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400752 }); err != nil {
753 return err
754 }
gio802311e2024-11-04 08:37:34 +0400755 if err := closePorts(cfg.Output.PortForward, cfg.Release.Namespace); err != nil {
giocdfa3722024-06-13 20:10:14 +0400756 return err
757 }
giof6ad2982024-08-23 17:42:49 +0400758 for _, cp := range cfg.Out.ClusterProxy {
gio721c0042025-04-03 11:56:36 +0400759 if err := m.cnc.RemoveIngressProxy(cp.From, cp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400760 return err
761 }
762 }
gio864b4332024-09-05 13:56:47 +0400763 for vmName, vmCfg := range cfg.Out.VM {
764 if vmCfg.VPN.Enabled {
gio92116ca2024-10-06 13:55:46 +0400765 // Not found error is ignored as VM might have not had enough time to boot before uninstalling it.
766 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400767 return err
768 }
769 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
770 return err
771 }
gio92116ca2024-10-06 13:55:46 +0400772 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400773 return err
774 }
775 }
776 }
giocdfa3722024-06-13 20:10:14 +0400777 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400778}
779
giocb34ad22024-07-11 08:01:13 +0400780func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
781 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400782 {
giocdfa3722024-06-13 20:10:14 +0400783 Name: "Public",
784 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
785 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
786 Domain: env.Domain,
787 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
788 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
789 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400790 },
gio7841f4f2024-07-26 19:53:49 +0400791 }
792 if env.PrivateDomain != "" {
793 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400794 Name: "Private",
795 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
796 Domain: env.PrivateDomain,
797 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
798 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
799 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400800 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400801 }
gio7fbd4ad2024-08-27 10:06:39 +0400802 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400803 if err != nil {
804 return nil, err
805 }
806 for _, a := range n {
807 ret = append(ret, Network{
808 Name: a.Input["name"].(string),
809 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
810 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
811 Domain: a.Input["domain"].(string),
812 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
813 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
814 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
815 })
816 }
817 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400818}
gio3cdee592024-04-17 10:15:56 +0400819
giof6ad2982024-08-23 17:42:49 +0400820func (m *AppManager) GetClusters() ([]cluster.State, error) {
821 ret := []cluster.State{
822 {
823 Name: "default",
824 },
825 }
826 files, err := m.repo.ListDir("/clusters")
827 if err != nil {
828 if errors.Is(err, fs.ErrNotExist) {
829 return ret, nil
830 }
831 return nil, err
832 }
833 for _, f := range files {
834 if !f.IsDir() {
835 continue
836 }
837 cfgPath := filepath.Clean(filepath.Join("/clusters", f.Name(), "config.json"))
838 var c cluster.State
839 if err := soft.ReadJson(m.repo, cfgPath, &c); err != nil {
840 if errors.Is(err, fs.ErrNotExist) {
841 continue
842 }
843 return nil, err
844 }
845 ret = append(ret, c)
846 }
847 return ret, nil
848}
849
gio0eaf2712024-04-14 13:08:46 +0400850type installOptions struct {
gio94904702024-07-26 16:58:34 +0400851 NoPull bool
giof8843412024-05-22 16:38:05 +0400852 NoPublish bool
853 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400854 Networks []Network
giof15b9da2024-09-19 06:59:16 +0400855 Clusters []Cluster
giof8843412024-05-22 16:38:05 +0400856 Branch string
857 LG LocalChartGenerator
858 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400859 Force bool
gio9d66f322024-07-06 13:45:10 +0400860 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400861}
862
863type InstallOption func(*installOptions)
864
865func WithConfig(env *EnvConfig) InstallOption {
866 return func(o *installOptions) {
867 o.Env = env
868 }
869}
870
giocb34ad22024-07-11 08:01:13 +0400871func WithNetworks(networks []Network) InstallOption {
872 return func(o *installOptions) {
873 o.Networks = networks
874 }
875}
876
gio23bdc1b2024-07-11 16:07:47 +0400877func WithNoNetworks() InstallOption {
878 return WithNetworks([]Network{})
879}
880
giof15b9da2024-09-19 06:59:16 +0400881func WithClusters(clusters []Cluster) InstallOption {
882 return func(o *installOptions) {
883 o.Clusters = clusters
884 }
885}
886
gio0eaf2712024-04-14 13:08:46 +0400887func WithBranch(branch string) InstallOption {
888 return func(o *installOptions) {
889 o.Branch = branch
890 }
891}
892
giof71a0832024-06-27 14:45:45 +0400893func WithForce() InstallOption {
894 return func(o *installOptions) {
895 o.Force = true
896 }
897}
898
giof8843412024-05-22 16:38:05 +0400899func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
900 return func(o *installOptions) {
901 o.LG = lg
902 }
903}
904
905func WithFetchContainerImages() InstallOption {
906 return func(o *installOptions) {
907 o.FetchContainerImages = true
908 }
909}
910
911func WithNoPublish() InstallOption {
912 return func(o *installOptions) {
913 o.NoPublish = true
914 }
915}
916
gio94904702024-07-26 16:58:34 +0400917func WithNoPull() InstallOption {
918 return func(o *installOptions) {
919 o.NoPull = true
920 }
921}
922
gio9d66f322024-07-06 13:45:10 +0400923func WithNoLock() InstallOption {
924 return func(o *installOptions) {
925 o.NoLock = true
926 }
927}
928
giof8843412024-05-22 16:38:05 +0400929// InfraAppmanager
930
931type InfraAppManager struct {
932 repoIO soft.RepoIO
933 nsc NamespaceCreator
934 hf HelmFetcher
935 lg LocalChartGenerator
936}
937
938func NewInfraAppManager(
939 repoIO soft.RepoIO,
940 nsc NamespaceCreator,
941 hf HelmFetcher,
942 lg LocalChartGenerator,
943) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400944 return &InfraAppManager{
945 repoIO,
giof8843412024-05-22 16:38:05 +0400946 nsc,
947 hf,
948 lg,
gio3cdee592024-04-17 10:15:56 +0400949 }, nil
950}
951
952func (m *InfraAppManager) Config() (InfraConfig, error) {
953 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400954 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400955 return InfraConfig{}, err
956 } else {
957 return cfg, nil
958 }
959}
960
gioe72b54f2024-04-22 10:44:41 +0400961func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
962 var cfg InfraAppInstanceConfig
963 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
964 return InfraAppInstanceConfig{}, err
965 } else {
966 return cfg, nil
967 }
968}
969
970func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400971 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", kustomizationFileName))
gioe72b54f2024-04-22 10:44:41 +0400972 if err != nil {
973 return InfraAppInstanceConfig{}, err
974 }
975 for _, app := range kust.Resources {
976 if app == id {
977 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
978 if err != nil {
979 return InfraAppInstanceConfig{}, err
980 }
981 cfg.Id = id
982 return cfg, nil
983 }
984 }
985 return InfraAppInstanceConfig{}, nil
986}
987
gio778577f2024-04-29 09:44:38 +0400988func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400989 appDir = filepath.Clean(appDir)
990 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400991 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400992 }
giof8843412024-05-22 16:38:05 +0400993 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400994 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400995 }
996 infra, err := m.Config()
997 if err != nil {
gio778577f2024-04-29 09:44:38 +0400998 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400999 }
1000 release := Release{
1001 Namespace: namespace,
1002 RepoAddr: m.repoIO.FullAddress(),
1003 AppDir: appDir,
1004 }
gio7841f4f2024-07-26 19:53:49 +04001005 networks := m.CreateNetworks(infra)
1006 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +04001007 if err != nil {
1008 return ReleaseResources{}, err
1009 }
giof71a0832024-06-27 14:45:45 +04001010 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
1011 if err != nil {
giof8843412024-05-22 16:38:05 +04001012 return ReleaseResources{}, err
1013 }
giof71a0832024-06-27 14:45:45 +04001014 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +04001015 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +04001016 if err != nil {
gio778577f2024-04-29 09:44:38 +04001017 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001018 }
gio94904702024-07-26 16:58:34 +04001019 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
1020 return ReleaseResources{}, err
1021 }
1022 return ReleaseResources{
1023 Release: rendered.Config.Release,
1024 RenderedRaw: rendered.Raw,
1025 Helm: extractHelm(rendered.Resources),
1026 }, nil
gioe72b54f2024-04-22 10:44:41 +04001027}
1028
giof8843412024-05-22 16:38:05 +04001029// TODO(gio): take app configuration from the repo
1030func (m *InfraAppManager) Update(
1031 instanceId string,
1032 values map[string]any,
1033 opts ...InstallOption,
1034) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +04001035 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001036 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001037 }
gio7841f4f2024-07-26 19:53:49 +04001038 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +04001039 if err != nil {
gio778577f2024-04-29 09:44:38 +04001040 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001041 }
1042 instanceDir := filepath.Join("/infrastructure", instanceId)
gio63a1a822025-04-23 12:59:40 +04001043 appCfg, err := GetCueAppData(m.repoIO, instanceDir, nil)
giof8843412024-05-22 16:38:05 +04001044 if err != nil {
1045 return ReleaseResources{}, err
1046 }
1047 app, err := NewCueInfraApp(appCfg)
1048 if err != nil {
1049 return ReleaseResources{}, err
1050 }
gioe72b54f2024-04-22 10:44:41 +04001051 instanceConfigPath := filepath.Join(instanceDir, "config.json")
1052 config, err := m.appConfig(instanceConfigPath)
1053 if err != nil {
gio778577f2024-04-29 09:44:38 +04001054 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001055 }
giocdfa3722024-06-13 20:10:14 +04001056 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +04001057 if err != nil {
1058 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001059 }
gio7841f4f2024-07-26 19:53:49 +04001060 networks := m.CreateNetworks(infra)
1061 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +04001062 if err != nil {
gio778577f2024-04-29 09:44:38 +04001063 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001064 }
gio94904702024-07-26 16:58:34 +04001065 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
1066 return ReleaseResources{}, err
1067 }
1068 return ReleaseResources{
1069 Release: rendered.Config.Release,
1070 RenderedRaw: rendered.Raw,
1071 Helm: extractHelm(rendered.Resources),
1072 }, nil
gio3cdee592024-04-17 10:15:56 +04001073}
giof8843412024-05-22 16:38:05 +04001074
gio7841f4f2024-07-26 19:53:49 +04001075func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
1076 return []InfraNetwork{
1077 {
1078 Name: "Public",
1079 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
1080 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
1081 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
1082 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
1083 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
1084 },
1085 }
1086}
1087
giof8843412024-05-22 16:38:05 +04001088func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
1089 ret := make(map[string]string)
1090 for name, chart := range charts.Git {
1091 chartRoot := filepath.Join(root, name)
1092 ret[name] = chartRoot
1093 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
1094 return nil, err
1095 }
1096 }
1097 return ret, nil
1098}
1099
1100func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
1101 ret := make(map[string]helmv2.HelmChartTemplateSpec)
1102 for name, path := range charts {
1103 ret[name] = g.Generate(path)
1104 }
1105 return ret
1106}
1107
1108func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
1109 for _, img := range imgs {
1110 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
1111 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
1112 "skopeo",
1113 "--insecure-policy",
1114 "copy",
1115 "--dest-tls-verify=false", // TODO(gio): enable
1116 "--multi-arch=all",
1117 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
1118 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
1119 }); err != nil {
1120 return err
1121 }
1122 }
1123 return nil
1124}
1125
1126type renderedInstance struct {
gio802311e2024-11-04 08:37:34 +04001127 Release Release `json:"release"`
giof8843412024-05-22 16:38:05 +04001128 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
gio864b4332024-09-05 13:56:47 +04001129 Out outRendered `json:"out"`
gio802311e2024-11-04 08:37:34 +04001130 Output outputRendered `json:"output"`
1131}
1132
1133type outputRendered struct {
1134 PortForward []PortForward `json:"openPort"`
gio864b4332024-09-05 13:56:47 +04001135}
1136
1137type outRendered struct {
giof6ad2982024-08-23 17:42:49 +04001138 ClusterProxy map[string]ClusterProxy
1139 VM map[string]vmRendered `json:"vm"`
gio864b4332024-09-05 13:56:47 +04001140}
1141
1142type vmRendered struct {
1143 Username string `json:"username"`
1144 VPN struct {
1145 Enabled bool `json:"enabled"`
1146 AuthKey string `json:"authKey"`
1147 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +04001148}
1149
giocdfa3722024-06-13 20:10:14 +04001150func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +04001151 r, err := fs.Reader(path)
1152 if err != nil {
giocdfa3722024-06-13 20:10:14 +04001153 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001154 }
1155 defer r.Close()
1156 var cfg renderedInstance
1157 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +04001158 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001159 }
giocdfa3722024-06-13 20:10:14 +04001160 return cfg, nil
giof8843412024-05-22 16:38:05 +04001161}
gioefa0ed42024-06-13 12:31:43 +04001162
1163func findPortFields(scm Schema) []string {
1164 switch scm.Kind() {
1165 case KindBoolean:
1166 return []string{}
1167 case KindInt:
1168 return []string{}
1169 case KindString:
1170 return []string{}
1171 case KindStruct:
1172 ret := []string{}
1173 for _, f := range scm.Fields() {
1174 for _, p := range findPortFields(f.Schema) {
1175 if p == "" {
1176 ret = append(ret, f.Name)
1177 } else {
1178 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1179 }
1180 }
1181 }
1182 return ret
1183 case KindNetwork:
1184 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001185 case KindMultiNetwork:
1186 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001187 case KindAuth:
1188 return []string{}
1189 case KindSSHKey:
1190 return []string{}
1191 case KindNumber:
1192 return []string{}
1193 case KindArrayString:
1194 return []string{}
1195 case KindPort:
1196 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001197 case KindVPNAuthKey:
1198 return []string{}
giof6ad2982024-08-23 17:42:49 +04001199 case KindCluster:
1200 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001201 default:
1202 panic("MUST NOT REACH!")
1203 }
1204}
1205
1206func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1207 for p, r := range ports {
1208 if err := setPortField(values, p, r.Port); err != nil {
1209 return err
1210 }
1211 }
1212 return nil
1213}
1214
1215func setPortField(values map[string]any, field string, port int) error {
1216 f := strings.SplitN(field, ".", 2)
1217 if len(f) == 2 {
1218 var sub map[string]any
1219 if s, ok := values[f[0]]; ok {
1220 sub, ok = s.(map[string]any)
1221 if !ok {
1222 return fmt.Errorf("expected map")
1223 }
1224 } else {
1225 sub = map[string]any{}
1226 values[f[0]] = sub
1227 }
1228 if err := setPortField(sub, f[1], port); err != nil {
1229 return err
1230 }
1231 } else {
1232 values[f[0]] = port
1233 }
1234 return nil
1235}
giof6ad2982024-08-23 17:42:49 +04001236
1237type Cluster struct {
1238 Name string `json:"name"`
1239 Kubeconfig string `json:"kubeconfig"`
1240 IngressClassName string `json:"ingressClassName"`
1241}
1242
1243func ClusterStateToAccessConfig(c cluster.State) Cluster {
1244 return Cluster{
1245 Name: c.Name,
1246 Kubeconfig: c.Kubeconfig,
1247 IngressClassName: c.IngressClassName,
1248 }
1249}
1250
1251func ToAccessConfigs(clusters []cluster.State) []Cluster {
1252 ret := make([]Cluster, 0, len(clusters))
1253 for _, c := range clusters {
1254 ret = append(ret, ClusterStateToAccessConfig(c))
1255 }
1256 return ret
1257}