blob: 0ec6605546123792f52bd45e96340d4c7fc3e92f [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 == "" {
giof4344632025-04-08 20:04:35 +0400219 if p.Service.Namespace == "" {
220 target = fmt.Sprintf("%s/%s", ns, p.Service.Name)
221 } else {
222 target = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
223 }
gio721c0042025-04-03 11:56:36 +0400224 } else {
225 target = p.Service.Name
226 }
gio3af43942024-04-16 08:13:50 +0400227 var buf bytes.Buffer
228 req := allocatePortReq{
229 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400230 SourcePort: p.Port,
gio721c0042025-04-03 11:56:36 +0400231 TargetService: target,
gio802311e2024-11-04 08:37:34 +0400232 TargetPort: p.Service.Port,
gio3af43942024-04-16 08:13:50 +0400233 }
gioefa0ed42024-06-13 12:31:43 +0400234 allocator := ""
235 for n, r := range reservations {
gio802311e2024-11-04 08:37:34 +0400236 if p.Port == r.Port {
gioefa0ed42024-06-13 12:31:43 +0400237 allocator = allocators[n]
giobd7ab0b2024-06-17 12:55:17 +0400238 req.Secret = r.Secret
gioefa0ed42024-06-13 12:31:43 +0400239 break
240 }
241 }
242 if allocator == "" {
gio802311e2024-11-04 08:37:34 +0400243 return fmt.Errorf("Could not find allocator for: %d", p.Port)
gioefa0ed42024-06-13 12:31:43 +0400244 }
giobd7ab0b2024-06-17 12:55:17 +0400245 if err := json.NewEncoder(&buf).Encode(req); err != nil {
246 return err
247 }
gioefa0ed42024-06-13 12:31:43 +0400248 resp, err := http.Post(allocator, "application/json", &buf)
gio3af43942024-04-16 08:13:50 +0400249 if err != nil {
250 return err
251 }
252 if resp.StatusCode != http.StatusOK {
giocdfa3722024-06-13 20:10:14 +0400253 var r bytes.Buffer
254 io.Copy(&r, resp.Body)
gio802311e2024-11-04 08:37:34 +0400255 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 +0400256 }
257 }
258 return nil
259}
260
gio802311e2024-11-04 08:37:34 +0400261func closePorts(ports []PortForward, ns string) error {
giocdfa3722024-06-13 20:10:14 +0400262 var retErr error
263 for _, p := range ports {
264 var buf bytes.Buffer
giof4344632025-04-08 20:04:35 +0400265 var fullName string
266 if p.Service.Namespace == "" {
267 fullName = fmt.Sprintf("%s/%s", ns, p.Service.Name)
268 } else {
269 fullName = fmt.Sprintf("%s/%s", p.Service.Namespace, p.Service.Name)
270 }
giocdfa3722024-06-13 20:10:14 +0400271 req := removePortReq{
272 Protocol: p.Protocol,
gio802311e2024-11-04 08:37:34 +0400273 SourcePort: p.Port,
giof4344632025-04-08 20:04:35 +0400274 TargetService: fullName,
gio802311e2024-11-04 08:37:34 +0400275 TargetPort: p.Service.Port,
giocdfa3722024-06-13 20:10:14 +0400276 }
277 if err := json.NewEncoder(&buf).Encode(req); err != nil {
278 retErr = err
279 continue
280 }
281 resp, err := http.Post(p.RemoveAddr, "application/json", &buf)
282 if err != nil {
283 retErr = err
284 continue
285 }
286 if resp.StatusCode != http.StatusOK {
gio802311e2024-11-04 08:37:34 +0400287 retErr = fmt.Errorf("Could not deallocate port %d, status code: %d", p.Port, resp.StatusCode)
giocdfa3722024-06-13 20:10:14 +0400288 continue
289 }
290 }
291 return retErr
292}
293
gioe72b54f2024-04-22 10:44:41 +0400294func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400295 for p := filepath.Clean(path); p != "/"; {
296 parent, child := filepath.Split(p)
gio92116ca2024-10-06 13:55:46 +0400297 kustPath := filepath.Join(parent, kustomizationFileName)
gioe72b54f2024-04-22 10:44:41 +0400298 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400299 if err != nil {
300 if errors.Is(err, fs.ErrNotExist) {
gioefa0ed42024-06-13 12:31:43 +0400301 k := gio.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400302 kust = &k
303 } else {
304 return err
305 }
306 }
307 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400308 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400309 return err
310 }
311 p = filepath.Clean(parent)
312 }
313 return nil
314}
315
gio778577f2024-04-29 09:44:38 +0400316type Resource struct {
giob4a3a192024-08-19 09:55:47 +0400317 Name string `json:"name"`
318 Namespace string `json:"namespace"`
319 Info string `json:"info"`
320 Annotations map[string]string `json:"annotations"`
gio778577f2024-04-29 09:44:38 +0400321}
322
323type ReleaseResources struct {
gio94904702024-07-26 16:58:34 +0400324 Release Release
325 Helm []Resource
326 RenderedRaw []byte
gio778577f2024-04-29 09:44:38 +0400327}
328
gio3cdee592024-04-17 10:15:56 +0400329// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400330func installApp(
gioe72b54f2024-04-22 10:44:41 +0400331 repo soft.RepoIO,
332 appDir string,
333 name string,
334 config any,
gioe72b54f2024-04-22 10:44:41 +0400335 resources CueAppData,
336 data CueAppData,
giof8843412024-05-22 16:38:05 +0400337 opts ...InstallOption,
gio94904702024-07-26 16:58:34 +0400338) error {
giof8843412024-05-22 16:38:05 +0400339 var o installOptions
340 for _, i := range opts {
341 i(&o)
342 }
343 dopts := []soft.DoOption{}
344 if o.Branch != "" {
giof8843412024-05-22 16:38:05 +0400345 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
346 }
gio94904702024-07-26 16:58:34 +0400347 if o.NoPull {
348 dopts = append(dopts, soft.WithNoPull())
349 }
giof8843412024-05-22 16:38:05 +0400350 if o.NoPublish {
351 dopts = append(dopts, soft.WithNoCommit())
352 }
giof71a0832024-06-27 14:45:45 +0400353 if o.Force {
354 dopts = append(dopts, soft.WithForce())
355 }
gio9d66f322024-07-06 13:45:10 +0400356 if o.NoLock {
357 dopts = append(dopts, soft.WithNoLock())
358 }
giob4a3a192024-08-19 09:55:47 +0400359 _, err := repo.Do(func(r soft.RepoFS) (string, error) {
giof6ad2982024-08-23 17:42:49 +0400360 if err := r.RemoveAll(appDir); err != nil {
gio308105e2024-04-19 13:12:13 +0400361 return "", err
362 }
363 resourcesDir := path.Join(appDir, "resources")
364 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400365 return "", err
366 }
gio94904702024-07-26 16:58:34 +0400367 if err := func() error {
gio5e49bb62024-07-20 10:43:19 +0400368 if err := soft.WriteFile(r, path.Join(appDir, gitIgnoreFileName), includeEverything); err != nil {
gio94904702024-07-26 16:58:34 +0400369 return err
gio5e49bb62024-07-20 10:43:19 +0400370 }
gioe72b54f2024-04-22 10:44:41 +0400371 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio94904702024-07-26 16:58:34 +0400372 return err
gio3af43942024-04-16 08:13:50 +0400373 }
gioe72b54f2024-04-22 10:44:41 +0400374 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio94904702024-07-26 16:58:34 +0400375 return err
gio308105e2024-04-19 13:12:13 +0400376 }
gioe72b54f2024-04-22 10:44:41 +0400377 for name, contents := range data {
gio92116ca2024-10-06 13:55:46 +0400378 if name == "config.json" || name == kustomizationFileName || name == "resources" {
gio94904702024-07-26 16:58:34 +0400379 return fmt.Errorf("%s is forbidden", name)
gio308105e2024-04-19 13:12:13 +0400380 }
381 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400382 if err != nil {
gio94904702024-07-26 16:58:34 +0400383 return err
gio3af43942024-04-16 08:13:50 +0400384 }
gio308105e2024-04-19 13:12:13 +0400385 defer w.Close()
386 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400387 return err
gio3af43942024-04-16 08:13:50 +0400388 }
389 }
gio94904702024-07-26 16:58:34 +0400390 return nil
391 }(); err != nil {
392 return "", err
gio308105e2024-04-19 13:12:13 +0400393 }
gio94904702024-07-26 16:58:34 +0400394 if err := func() error {
gio308105e2024-04-19 13:12:13 +0400395 if err := createKustomizationChain(r, resourcesDir); err != nil {
gio94904702024-07-26 16:58:34 +0400396 return err
gio308105e2024-04-19 13:12:13 +0400397 }
gioefa0ed42024-06-13 12:31:43 +0400398 appKust := gio.NewKustomization()
gioe72b54f2024-04-22 10:44:41 +0400399 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400400 appKust.AddResources(name)
401 w, err := r.Writer(path.Join(resourcesDir, name))
402 if err != nil {
gio94904702024-07-26 16:58:34 +0400403 return err
gio308105e2024-04-19 13:12:13 +0400404 }
405 defer w.Close()
406 if _, err := w.Write(contents); err != nil {
gio94904702024-07-26 16:58:34 +0400407 return err
gio308105e2024-04-19 13:12:13 +0400408 }
409 }
gio92116ca2024-10-06 13:55:46 +0400410 if err := soft.WriteYaml(r, path.Join(resourcesDir, kustomizationFileName), appKust); err != nil {
gio94904702024-07-26 16:58:34 +0400411 return err
gio3af43942024-04-16 08:13:50 +0400412 }
gio94904702024-07-26 16:58:34 +0400413 return nil
414 }(); err != nil {
415 return "", err
gio3af43942024-04-16 08:13:50 +0400416 }
gioe72b54f2024-04-22 10:44:41 +0400417 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400418 }, dopts...)
giob4a3a192024-08-19 09:55:47 +0400419 return err
gio3af43942024-04-16 08:13:50 +0400420}
421
gio3cdee592024-04-17 10:15:56 +0400422// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400423func (m *AppManager) Install(
424 app EnvApp,
425 instanceId string,
426 appDir string,
427 namespace string,
428 values map[string]any,
429 opts ...InstallOption,
430) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400431 o := &installOptions{}
432 for _, i := range opts {
433 i(o)
434 }
435 if !o.NoLock {
436 m.l.Lock()
437 defer m.l.Unlock()
438 }
gioefa0ed42024-06-13 12:31:43 +0400439 portFields := findPortFields(app.Schema())
440 fakeReservations := map[string]reservePortResp{}
441 for i, f := range portFields {
442 fakeReservations[f] = reservePortResp{Port: i}
443 }
444 if err := setPortFields(values, fakeReservations); err != nil {
445 return ReleaseResources{}, err
446 }
gio3af43942024-04-16 08:13:50 +0400447 appDir = filepath.Clean(appDir)
gio94904702024-07-26 16:58:34 +0400448 if !o.NoPull {
giof6ad2982024-08-23 17:42:49 +0400449 if err := m.repo.Pull(); err != nil {
gio94904702024-07-26 16:58:34 +0400450 return ReleaseResources{}, err
451 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400452 }
gio94904702024-07-26 16:58:34 +0400453 opts = append(opts, WithNoPull())
giof8843412024-05-22 16:38:05 +0400454 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400455 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400456 }
gio0eaf2712024-04-14 13:08:46 +0400457 var env EnvConfig
458 if o.Env != nil {
459 env = *o.Env
460 } else {
461 var err error
462 env, err = m.Config()
463 if err != nil {
464 return ReleaseResources{}, err
465 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400466 }
giocb34ad22024-07-11 08:01:13 +0400467 var networks []Network
468 if o.Networks != nil {
469 networks = o.Networks
470 } else {
471 var err error
472 networks, err = m.CreateNetworks(env)
473 if err != nil {
474 return ReleaseResources{}, err
475 }
476 }
giof15b9da2024-09-19 06:59:16 +0400477 var clusters []Cluster
478 if o.Clusters != nil {
479 clusters = o.Clusters
480 } else {
481 if cls, err := m.GetClusters(); err != nil {
482 return ReleaseResources{}, err
483 } else {
484 clusters = ToAccessConfigs(cls)
485 }
giof6ad2982024-08-23 17:42:49 +0400486 }
giof8843412024-05-22 16:38:05 +0400487 var lg LocalChartGenerator
488 if o.LG != nil {
489 lg = o.LG
490 } else {
491 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
492 }
gio3cdee592024-04-17 10:15:56 +0400493 release := Release{
494 AppInstanceId: instanceId,
495 Namespace: namespace,
giof6ad2982024-08-23 17:42:49 +0400496 RepoAddr: m.repo.FullAddress(),
gio3cdee592024-04-17 10:15:56 +0400497 AppDir: appDir,
498 }
giof15b9da2024-09-19 06:59:16 +0400499 rendered, err := app.Render(release, env, networks, clusters, values, nil, m.vpnAPIClient)
gioef01fbb2024-04-12 16:52:59 +0400500 if err != nil {
gio778577f2024-04-29 09:44:38 +0400501 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400502 }
gio721c0042025-04-03 11:56:36 +0400503 reservators := map[string]reservePortInfo{}
gioefa0ed42024-06-13 12:31:43 +0400504 allocators := map[string]string{}
505 for _, pf := range rendered.Ports {
gio721c0042025-04-03 11:56:36 +0400506 reservators[portFields[pf.Port]] = reservePortInfo{
507 reserveAddr: pf.ReserveAddr,
508 RemoteProxy: pf.Cluster != "",
509 }
gio802311e2024-11-04 08:37:34 +0400510 allocators[portFields[pf.Port]] = pf.Allocator
gioefa0ed42024-06-13 12:31:43 +0400511 }
512 portReservations, err := reservePorts(reservators)
513 if err != nil {
514 return ReleaseResources{}, err
515 }
516 if err := setPortFields(values, portReservations); err != nil {
517 return ReleaseResources{}, err
518 }
gio7841f4f2024-07-26 19:53:49 +0400519 // TODO(gio): env might not have private domain
giof8843412024-05-22 16:38:05 +0400520 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
521 if o.FetchContainerImages {
522 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
523 return ReleaseResources{}, err
524 }
gio0eaf2712024-04-14 13:08:46 +0400525 }
giof6ad2982024-08-23 17:42:49 +0400526 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repo, "/helm-charts")
giof71a0832024-06-27 14:45:45 +0400527 if err != nil {
giof8843412024-05-22 16:38:05 +0400528 return ReleaseResources{}, err
529 }
giof71a0832024-06-27 14:45:45 +0400530 localCharts := generateLocalCharts(lg, charts)
giof8843412024-05-22 16:38:05 +0400531 if o.FetchContainerImages {
532 release.ImageRegistry = imageRegistry
533 }
giof15b9da2024-09-19 06:59:16 +0400534 rendered, err = app.Render(release, env, networks, clusters, values, localCharts, m.vpnAPIClient)
giof8843412024-05-22 16:38:05 +0400535 if err != nil {
536 return ReleaseResources{}, err
537 }
giof6ad2982024-08-23 17:42:49 +0400538 for _, ns := range rendered.Namespaces {
539 if ns.Name == "" {
540 return ReleaseResources{}, fmt.Errorf("namespace name missing")
541 }
542 if ns.Kubeconfig == "" {
543 continue
544 }
545 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
546 if err != nil {
547 return ReleaseResources{}, err
548 }
549 if err := nsc.Create(ns.Name); err != nil {
550 return ReleaseResources{}, err
551 }
552 }
553 if err := installApp(m.repo, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400554 return ReleaseResources{}, err
555 }
gioff2a29a2024-05-01 17:06:42 +0400556 // TODO(gio): add ingress-nginx to release resources
gio802311e2024-11-04 08:37:34 +0400557 if err := openPorts(rendered.Ports, portReservations, allocators, release.Namespace); err != nil {
gioff2a29a2024-05-01 17:06:42 +0400558 return ReleaseResources{}, err
559 }
giof6ad2982024-08-23 17:42:49 +0400560 for _, p := range rendered.ClusterProxies {
gio721c0042025-04-03 11:56:36 +0400561 if err := m.cnc.AddIngressProxy(p.From, p.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400562 return ReleaseResources{}, err
563 }
564 }
gio778577f2024-04-29 09:44:38 +0400565 return ReleaseResources{
gio94904702024-07-26 16:58:34 +0400566 Release: rendered.Config.Release,
567 RenderedRaw: rendered.Raw,
568 Helm: extractHelm(rendered.Resources),
gio778577f2024-04-29 09:44:38 +0400569 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400570}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400571
gio778577f2024-04-29 09:44:38 +0400572type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400573 Metadata struct {
574 Name string `json:"name"`
575 Namespace string `json:"namespace"`
576 Annotations map[string]string `json:"annotations"`
577 } `json:"metadata"`
578 Kind string `json:"kind"`
579 Status struct {
gio778577f2024-04-29 09:44:38 +0400580 Conditions []struct {
581 Type string `json:"type"`
582 Status string `json:"status"`
583 } `json:"conditions"`
584 } `json:"status,omitempty"`
585}
586
587func extractHelm(resources CueAppData) []Resource {
588 ret := make([]Resource, 0, len(resources))
589 for _, contents := range resources {
590 var h helmRelease
591 if err := yaml.Unmarshal(contents, &h); err != nil {
592 panic(err) // TODO(gio): handle
593 }
gio0eaf2712024-04-14 13:08:46 +0400594 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400595 res := Resource{
giob4a3a192024-08-19 09:55:47 +0400596 Name: h.Metadata.Name,
597 Namespace: h.Metadata.Namespace,
598 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
599 Annotations: nil,
giof9f0bee2024-06-11 20:10:05 +0400600 }
601 if h.Metadata.Annotations != nil {
giob4a3a192024-08-19 09:55:47 +0400602 res.Annotations = h.Metadata.Annotations
giof9f0bee2024-06-11 20:10:05 +0400603 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
604 if ok && len(info) != 0 {
605 res.Info = info
606 }
607 }
608 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400609 }
gio778577f2024-04-29 09:44:38 +0400610 }
611 return ret
612}
613
giof8843412024-05-22 16:38:05 +0400614// TODO(gio): take app configuration from the repo
615func (m *AppManager) Update(
616 instanceId string,
617 values map[string]any,
618 opts ...InstallOption,
619) (ReleaseResources, error) {
gio69731e82024-08-01 14:15:55 +0400620 m.l.Lock()
621 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400622 if err := m.repo.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400623 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400624 }
gio3cdee592024-04-17 10:15:56 +0400625 env, err := m.Config()
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 }
gio308105e2024-04-19 13:12:13 +0400629 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400630 app, err := m.GetInstanceApp(instanceId)
631 if err != nil {
632 return ReleaseResources{}, err
633 }
gio308105e2024-04-19 13:12:13 +0400634 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400635 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400636 if err != nil {
gio778577f2024-04-29 09:44:38 +0400637 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400638 }
giof6ad2982024-08-23 17:42:49 +0400639 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +0400640 if err != nil {
641 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400642 }
giocb34ad22024-07-11 08:01:13 +0400643 networks, err := m.CreateNetworks(env)
644 if err != nil {
645 return ReleaseResources{}, err
646 }
giof6ad2982024-08-23 17:42:49 +0400647 clusters, err := m.GetClusters()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400648 if err != nil {
gio778577f2024-04-29 09:44:38 +0400649 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400650 }
giof6ad2982024-08-23 17:42:49 +0400651 rendered, err := app.Render(config.Release, env, networks, ToAccessConfigs(clusters), values, renderedCfg.LocalCharts, m.vpnAPIClient)
652 if err != nil {
gio94904702024-07-26 16:58:34 +0400653 return ReleaseResources{}, err
654 }
giof6ad2982024-08-23 17:42:49 +0400655 for _, ns := range rendered.Namespaces {
656 if ns.Name == "" {
657 return ReleaseResources{}, fmt.Errorf("namespace name missing")
658 }
659 if ns.Kubeconfig == "" {
660 continue
661 }
662 nsc, err := NewNamespaceCreator(kube.KubeConfigOpts{KubeConfig: ns.Kubeconfig})
663 if err != nil {
664 return ReleaseResources{}, err
665 }
666 if err := nsc.Create(ns.Name); err != nil {
667 return ReleaseResources{}, err
668 }
669 }
670 if err := installApp(m.repo, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
671 return ReleaseResources{}, err
672 }
673 for _, ocp := range renderedCfg.Out.ClusterProxy {
674 found := false
675 for _, ncp := range rendered.ClusterProxies {
676 if ocp == ncp {
677 found = true
678 break
679 }
680 }
681 if !found {
gio721c0042025-04-03 11:56:36 +0400682 if err := m.cnc.RemoveIngressProxy(ocp.From, ocp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400683 return ReleaseResources{}, err
684 }
685 }
686 }
687 for _, ncp := range rendered.ClusterProxies {
688 found := false
689 for _, ocp := range renderedCfg.Out.ClusterProxy {
690 if ocp == ncp {
691 found = true
692 break
693 }
694 }
695 if !found {
gio721c0042025-04-03 11:56:36 +0400696 if err := m.cnc.AddIngressProxy(ncp.From, ncp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400697 return ReleaseResources{}, err
698 }
699 }
700 }
gio94904702024-07-26 16:58:34 +0400701 return ReleaseResources{
702 Release: rendered.Config.Release,
703 RenderedRaw: rendered.Raw,
704 Helm: extractHelm(rendered.Resources),
705 }, nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400706}
707
708func (m *AppManager) Remove(instanceId string) error {
gio69731e82024-08-01 14:15:55 +0400709 m.l.Lock()
710 defer m.l.Unlock()
giof6ad2982024-08-23 17:42:49 +0400711 if err := m.repo.Pull(); err != nil {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400712 return err
713 }
gio864b4332024-09-05 13:56:47 +0400714 var cfg renderedInstance
giof6ad2982024-08-23 17:42:49 +0400715 if _, err := m.repo.Do(func(r soft.RepoFS) (string, error) {
giocdfa3722024-06-13 20:10:14 +0400716 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof6ad2982024-08-23 17:42:49 +0400717 renderedCfg, err := readRendered(m.repo, filepath.Join(instanceDir, "rendered.json"))
giocdfa3722024-06-13 20:10:14 +0400718 if err != nil {
719 return "", err
720 }
gio864b4332024-09-05 13:56:47 +0400721 cfg = renderedCfg
giof6ad2982024-08-23 17:42:49 +0400722 r.RemoveAll(instanceDir)
gio5887caa2024-10-03 15:07:23 +0400723 curr := instanceDir
gio829b1b72024-10-05 21:50:56 +0400724 for {
gio5887caa2024-10-03 15:07:23 +0400725 p := filepath.Dir(curr)
gio829b1b72024-10-05 21:50:56 +0400726 if p == curr {
727 break
728 }
gio5887caa2024-10-03 15:07:23 +0400729 n := filepath.Base(curr)
gio92116ca2024-10-06 13:55:46 +0400730 kustPath := filepath.Join(p, kustomizationFileName)
gio5887caa2024-10-03 15:07:23 +0400731 kust, err := soft.ReadKustomization(r, kustPath)
732 if err != nil {
733 return "", err
734 }
735 kust.RemoveResources(n)
gio829b1b72024-10-05 21:50:56 +0400736 if len(kust.Resources) > 0 || p == m.appDirRoot {
gio5887caa2024-10-03 15:07:23 +0400737 soft.WriteYaml(r, kustPath, kust)
738 break
739 } else {
740 if err := r.RemoveAll(kustPath); err != nil {
741 return "", err
742 }
743 }
gio5887caa2024-10-03 15:07:23 +0400744 curr = p
gio3af43942024-04-16 08:13:50 +0400745 }
gio3af43942024-04-16 08:13:50 +0400746 return fmt.Sprintf("uninstall: %s", instanceId), nil
giocdfa3722024-06-13 20:10:14 +0400747 }); err != nil {
748 return err
749 }
gio802311e2024-11-04 08:37:34 +0400750 if err := closePorts(cfg.Output.PortForward, cfg.Release.Namespace); err != nil {
giocdfa3722024-06-13 20:10:14 +0400751 return err
752 }
giof6ad2982024-08-23 17:42:49 +0400753 for _, cp := range cfg.Out.ClusterProxy {
gio721c0042025-04-03 11:56:36 +0400754 if err := m.cnc.RemoveIngressProxy(cp.From, cp.To); err != nil {
giof6ad2982024-08-23 17:42:49 +0400755 return err
756 }
757 }
gio864b4332024-09-05 13:56:47 +0400758 for vmName, vmCfg := range cfg.Out.VM {
759 if vmCfg.VPN.Enabled {
gio92116ca2024-10-06 13:55:46 +0400760 // Not found error is ignored as VM might have not had enough time to boot before uninstalling it.
761 if err := m.vpnAPIClient.ExpireNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400762 return err
763 }
764 if err := m.vpnAPIClient.ExpireKey(vmCfg.Username, vmCfg.VPN.AuthKey); err != nil {
765 return err
766 }
gio92116ca2024-10-06 13:55:46 +0400767 if err := m.vpnAPIClient.RemoveNode(vmCfg.Username, vmName); err != nil && !errors.Is(err, ErrorNotFound) {
gio864b4332024-09-05 13:56:47 +0400768 return err
769 }
770 }
771 }
giocdfa3722024-06-13 20:10:14 +0400772 return nil
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400773}
774
giocb34ad22024-07-11 08:01:13 +0400775func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
776 ret := []Network{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400777 {
giocdfa3722024-06-13 20:10:14 +0400778 Name: "Public",
779 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
780 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
781 Domain: env.Domain,
782 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
783 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
784 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400785 },
gio7841f4f2024-07-26 19:53:49 +0400786 }
787 if env.PrivateDomain != "" {
788 ret = append(ret, Network{
giocdfa3722024-06-13 20:10:14 +0400789 Name: "Private",
790 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
791 Domain: env.PrivateDomain,
792 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
793 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
794 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
gio7841f4f2024-07-26 19:53:49 +0400795 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400796 }
gio7fbd4ad2024-08-27 10:06:39 +0400797 n, err := m.GetAllAppInstances("network")
giocb34ad22024-07-11 08:01:13 +0400798 if err != nil {
799 return nil, err
800 }
801 for _, a := range n {
802 ret = append(ret, Network{
803 Name: a.Input["name"].(string),
804 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
805 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
806 Domain: a.Input["domain"].(string),
807 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
808 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
809 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
810 })
811 }
812 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400813}
gio3cdee592024-04-17 10:15:56 +0400814
giof6ad2982024-08-23 17:42:49 +0400815func (m *AppManager) GetClusters() ([]cluster.State, error) {
816 ret := []cluster.State{
817 {
818 Name: "default",
819 },
820 }
821 files, err := m.repo.ListDir("/clusters")
822 if err != nil {
823 if errors.Is(err, fs.ErrNotExist) {
824 return ret, nil
825 }
826 return nil, err
827 }
828 for _, f := range files {
829 if !f.IsDir() {
830 continue
831 }
832 cfgPath := filepath.Clean(filepath.Join("/clusters", f.Name(), "config.json"))
833 var c cluster.State
834 if err := soft.ReadJson(m.repo, cfgPath, &c); err != nil {
835 if errors.Is(err, fs.ErrNotExist) {
836 continue
837 }
838 return nil, err
839 }
840 ret = append(ret, c)
841 }
842 return ret, nil
843}
844
gio0eaf2712024-04-14 13:08:46 +0400845type installOptions struct {
gio94904702024-07-26 16:58:34 +0400846 NoPull bool
giof8843412024-05-22 16:38:05 +0400847 NoPublish bool
848 Env *EnvConfig
giocb34ad22024-07-11 08:01:13 +0400849 Networks []Network
giof15b9da2024-09-19 06:59:16 +0400850 Clusters []Cluster
giof8843412024-05-22 16:38:05 +0400851 Branch string
852 LG LocalChartGenerator
853 FetchContainerImages bool
giof71a0832024-06-27 14:45:45 +0400854 Force bool
gio9d66f322024-07-06 13:45:10 +0400855 NoLock bool
gio0eaf2712024-04-14 13:08:46 +0400856}
857
858type InstallOption func(*installOptions)
859
860func WithConfig(env *EnvConfig) InstallOption {
861 return func(o *installOptions) {
862 o.Env = env
863 }
864}
865
giocb34ad22024-07-11 08:01:13 +0400866func WithNetworks(networks []Network) InstallOption {
867 return func(o *installOptions) {
868 o.Networks = networks
869 }
870}
871
gio23bdc1b2024-07-11 16:07:47 +0400872func WithNoNetworks() InstallOption {
873 return WithNetworks([]Network{})
874}
875
giof15b9da2024-09-19 06:59:16 +0400876func WithClusters(clusters []Cluster) InstallOption {
877 return func(o *installOptions) {
878 o.Clusters = clusters
879 }
880}
881
gio0eaf2712024-04-14 13:08:46 +0400882func WithBranch(branch string) InstallOption {
883 return func(o *installOptions) {
884 o.Branch = branch
885 }
886}
887
giof71a0832024-06-27 14:45:45 +0400888func WithForce() InstallOption {
889 return func(o *installOptions) {
890 o.Force = true
891 }
892}
893
giof8843412024-05-22 16:38:05 +0400894func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
895 return func(o *installOptions) {
896 o.LG = lg
897 }
898}
899
900func WithFetchContainerImages() InstallOption {
901 return func(o *installOptions) {
902 o.FetchContainerImages = true
903 }
904}
905
906func WithNoPublish() InstallOption {
907 return func(o *installOptions) {
908 o.NoPublish = true
909 }
910}
911
gio94904702024-07-26 16:58:34 +0400912func WithNoPull() InstallOption {
913 return func(o *installOptions) {
914 o.NoPull = true
915 }
916}
917
gio9d66f322024-07-06 13:45:10 +0400918func WithNoLock() InstallOption {
919 return func(o *installOptions) {
920 o.NoLock = true
921 }
922}
923
giof8843412024-05-22 16:38:05 +0400924// InfraAppmanager
925
926type InfraAppManager struct {
927 repoIO soft.RepoIO
928 nsc NamespaceCreator
929 hf HelmFetcher
930 lg LocalChartGenerator
931}
932
933func NewInfraAppManager(
934 repoIO soft.RepoIO,
935 nsc NamespaceCreator,
936 hf HelmFetcher,
937 lg LocalChartGenerator,
938) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400939 return &InfraAppManager{
940 repoIO,
giof8843412024-05-22 16:38:05 +0400941 nsc,
942 hf,
943 lg,
gio3cdee592024-04-17 10:15:56 +0400944 }, nil
945}
946
947func (m *InfraAppManager) Config() (InfraConfig, error) {
948 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400949 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400950 return InfraConfig{}, err
951 } else {
952 return cfg, nil
953 }
954}
955
gioe72b54f2024-04-22 10:44:41 +0400956func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
957 var cfg InfraAppInstanceConfig
958 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
959 return InfraAppInstanceConfig{}, err
960 } else {
961 return cfg, nil
962 }
963}
964
965func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
gio92116ca2024-10-06 13:55:46 +0400966 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", kustomizationFileName))
gioe72b54f2024-04-22 10:44:41 +0400967 if err != nil {
968 return InfraAppInstanceConfig{}, err
969 }
970 for _, app := range kust.Resources {
971 if app == id {
972 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
973 if err != nil {
974 return InfraAppInstanceConfig{}, err
975 }
976 cfg.Id = id
977 return cfg, nil
978 }
979 }
980 return InfraAppInstanceConfig{}, nil
981}
982
gio778577f2024-04-29 09:44:38 +0400983func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400984 appDir = filepath.Clean(appDir)
985 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400986 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400987 }
giof8843412024-05-22 16:38:05 +0400988 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400989 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400990 }
991 infra, err := m.Config()
992 if err != nil {
gio778577f2024-04-29 09:44:38 +0400993 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400994 }
995 release := Release{
996 Namespace: namespace,
997 RepoAddr: m.repoIO.FullAddress(),
998 AppDir: appDir,
999 }
gio7841f4f2024-07-26 19:53:49 +04001000 networks := m.CreateNetworks(infra)
1001 rendered, err := app.Render(release, infra, networks, values, nil)
giof8843412024-05-22 16:38:05 +04001002 if err != nil {
1003 return ReleaseResources{}, err
1004 }
giof71a0832024-06-27 14:45:45 +04001005 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, m.repoIO, "/helm-charts")
1006 if err != nil {
giof8843412024-05-22 16:38:05 +04001007 return ReleaseResources{}, err
1008 }
giof71a0832024-06-27 14:45:45 +04001009 localCharts := generateLocalCharts(m.lg, charts)
gio7841f4f2024-07-26 19:53:49 +04001010 rendered, err = app.Render(release, infra, networks, values, localCharts)
gio3cdee592024-04-17 10:15:56 +04001011 if err != nil {
gio778577f2024-04-29 09:44:38 +04001012 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +04001013 }
gio94904702024-07-26 16:58:34 +04001014 if err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data); err != nil {
1015 return ReleaseResources{}, err
1016 }
1017 return ReleaseResources{
1018 Release: rendered.Config.Release,
1019 RenderedRaw: rendered.Raw,
1020 Helm: extractHelm(rendered.Resources),
1021 }, nil
gioe72b54f2024-04-22 10:44:41 +04001022}
1023
giof8843412024-05-22 16:38:05 +04001024// TODO(gio): take app configuration from the repo
1025func (m *InfraAppManager) Update(
1026 instanceId string,
1027 values map[string]any,
1028 opts ...InstallOption,
1029) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +04001030 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +04001031 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001032 }
gio7841f4f2024-07-26 19:53:49 +04001033 infra, err := m.Config()
gioe72b54f2024-04-22 10:44:41 +04001034 if err != nil {
gio778577f2024-04-29 09:44:38 +04001035 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001036 }
1037 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +04001038 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
1039 if err != nil {
1040 return ReleaseResources{}, err
1041 }
1042 app, err := NewCueInfraApp(appCfg)
1043 if err != nil {
1044 return ReleaseResources{}, err
1045 }
gioe72b54f2024-04-22 10:44:41 +04001046 instanceConfigPath := filepath.Join(instanceDir, "config.json")
1047 config, err := m.appConfig(instanceConfigPath)
1048 if err != nil {
gio778577f2024-04-29 09:44:38 +04001049 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001050 }
giocdfa3722024-06-13 20:10:14 +04001051 renderedCfg, err := readRendered(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
giof8843412024-05-22 16:38:05 +04001052 if err != nil {
1053 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001054 }
gio7841f4f2024-07-26 19:53:49 +04001055 networks := m.CreateNetworks(infra)
1056 rendered, err := app.Render(config.Release, infra, networks, values, renderedCfg.LocalCharts)
gioe72b54f2024-04-22 10:44:41 +04001057 if err != nil {
gio778577f2024-04-29 09:44:38 +04001058 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +04001059 }
gio94904702024-07-26 16:58:34 +04001060 if err := installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Resources, rendered.Data, opts...); err != nil {
1061 return ReleaseResources{}, err
1062 }
1063 return ReleaseResources{
1064 Release: rendered.Config.Release,
1065 RenderedRaw: rendered.Raw,
1066 Helm: extractHelm(rendered.Resources),
1067 }, nil
gio3cdee592024-04-17 10:15:56 +04001068}
giof8843412024-05-22 16:38:05 +04001069
gio7841f4f2024-07-26 19:53:49 +04001070func (m *InfraAppManager) CreateNetworks(infra InfraConfig) []InfraNetwork {
1071 return []InfraNetwork{
1072 {
1073 Name: "Public",
1074 IngressClass: fmt.Sprintf("%s-ingress-public", infra.Name),
1075 CertificateIssuer: fmt.Sprintf("%s-public", infra.Name),
1076 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", infra.Name),
1077 ReservePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", infra.Name),
1078 DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", infra.Name),
1079 },
1080 }
1081}
1082
giof8843412024-05-22 16:38:05 +04001083func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
1084 ret := make(map[string]string)
1085 for name, chart := range charts.Git {
1086 chartRoot := filepath.Join(root, name)
1087 ret[name] = chartRoot
1088 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
1089 return nil, err
1090 }
1091 }
1092 return ret, nil
1093}
1094
1095func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
1096 ret := make(map[string]helmv2.HelmChartTemplateSpec)
1097 for name, path := range charts {
1098 ret[name] = g.Generate(path)
1099 }
1100 return ret
1101}
1102
1103func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
1104 for _, img := range imgs {
1105 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
1106 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
1107 "skopeo",
1108 "--insecure-policy",
1109 "copy",
1110 "--dest-tls-verify=false", // TODO(gio): enable
1111 "--multi-arch=all",
1112 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
1113 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
1114 }); err != nil {
1115 return err
1116 }
1117 }
1118 return nil
1119}
1120
1121type renderedInstance struct {
gio802311e2024-11-04 08:37:34 +04001122 Release Release `json:"release"`
giof8843412024-05-22 16:38:05 +04001123 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
gio864b4332024-09-05 13:56:47 +04001124 Out outRendered `json:"out"`
gio802311e2024-11-04 08:37:34 +04001125 Output outputRendered `json:"output"`
1126}
1127
1128type outputRendered struct {
1129 PortForward []PortForward `json:"openPort"`
gio864b4332024-09-05 13:56:47 +04001130}
1131
1132type outRendered struct {
giof6ad2982024-08-23 17:42:49 +04001133 ClusterProxy map[string]ClusterProxy
1134 VM map[string]vmRendered `json:"vm"`
gio864b4332024-09-05 13:56:47 +04001135}
1136
1137type vmRendered struct {
1138 Username string `json:"username"`
1139 VPN struct {
1140 Enabled bool `json:"enabled"`
1141 AuthKey string `json:"authKey"`
1142 } `json:"vpn"`
giof8843412024-05-22 16:38:05 +04001143}
1144
giocdfa3722024-06-13 20:10:14 +04001145func readRendered(fs soft.RepoFS, path string) (renderedInstance, error) {
giof8843412024-05-22 16:38:05 +04001146 r, err := fs.Reader(path)
1147 if err != nil {
giocdfa3722024-06-13 20:10:14 +04001148 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001149 }
1150 defer r.Close()
1151 var cfg renderedInstance
1152 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
giocdfa3722024-06-13 20:10:14 +04001153 return renderedInstance{}, err
giof8843412024-05-22 16:38:05 +04001154 }
giocdfa3722024-06-13 20:10:14 +04001155 return cfg, nil
giof8843412024-05-22 16:38:05 +04001156}
gioefa0ed42024-06-13 12:31:43 +04001157
1158func findPortFields(scm Schema) []string {
1159 switch scm.Kind() {
1160 case KindBoolean:
1161 return []string{}
1162 case KindInt:
1163 return []string{}
1164 case KindString:
1165 return []string{}
1166 case KindStruct:
1167 ret := []string{}
1168 for _, f := range scm.Fields() {
1169 for _, p := range findPortFields(f.Schema) {
1170 if p == "" {
1171 ret = append(ret, f.Name)
1172 } else {
1173 ret = append(ret, fmt.Sprintf("%s.%s", f.Name, p))
1174 }
1175 }
1176 }
1177 return ret
1178 case KindNetwork:
1179 return []string{}
gio4ece99c2024-07-18 11:05:50 +04001180 case KindMultiNetwork:
1181 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001182 case KindAuth:
1183 return []string{}
1184 case KindSSHKey:
1185 return []string{}
1186 case KindNumber:
1187 return []string{}
1188 case KindArrayString:
1189 return []string{}
1190 case KindPort:
1191 return []string{""}
gio36b23b32024-08-25 12:20:54 +04001192 case KindVPNAuthKey:
1193 return []string{}
giof6ad2982024-08-23 17:42:49 +04001194 case KindCluster:
1195 return []string{}
gioefa0ed42024-06-13 12:31:43 +04001196 default:
1197 panic("MUST NOT REACH!")
1198 }
1199}
1200
1201func setPortFields(values map[string]any, ports map[string]reservePortResp) error {
1202 for p, r := range ports {
1203 if err := setPortField(values, p, r.Port); err != nil {
1204 return err
1205 }
1206 }
1207 return nil
1208}
1209
1210func setPortField(values map[string]any, field string, port int) error {
1211 f := strings.SplitN(field, ".", 2)
1212 if len(f) == 2 {
1213 var sub map[string]any
1214 if s, ok := values[f[0]]; ok {
1215 sub, ok = s.(map[string]any)
1216 if !ok {
1217 return fmt.Errorf("expected map")
1218 }
1219 } else {
1220 sub = map[string]any{}
1221 values[f[0]] = sub
1222 }
1223 if err := setPortField(sub, f[1], port); err != nil {
1224 return err
1225 }
1226 } else {
1227 values[f[0]] = port
1228 }
1229 return nil
1230}
giof6ad2982024-08-23 17:42:49 +04001231
1232type Cluster struct {
1233 Name string `json:"name"`
1234 Kubeconfig string `json:"kubeconfig"`
1235 IngressClassName string `json:"ingressClassName"`
1236}
1237
1238func ClusterStateToAccessConfig(c cluster.State) Cluster {
1239 return Cluster{
1240 Name: c.Name,
1241 Kubeconfig: c.Kubeconfig,
1242 IngressClassName: c.IngressClassName,
1243 }
1244}
1245
1246func ToAccessConfigs(clusters []cluster.State) []Cluster {
1247 ret := make([]Cluster, 0, len(clusters))
1248 for _, c := range clusters {
1249 ret = append(ret, ClusterStateToAccessConfig(c))
1250 }
1251 return ret
1252}