blob: 871dae543c07f9ad1787a4f9218a6857a2ae8435 [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"
gio3af43942024-04-16 08:13:50 +04008 "io/fs"
gio3af43942024-04-16 08:13:50 +04009 "net/http"
10 "path"
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +040011 "path/filepath"
gioe72b54f2024-04-22 10:44:41 +040012
13 "github.com/giolekva/pcloud/core/installer/io"
14 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040015)
16
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040017const configFileName = "config.yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040018const kustomizationFileName = "kustomization.yaml"
19
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040020type AppManager struct {
gioe72b54f2024-04-22 10:44:41 +040021 repoIO soft.RepoIO
gio308105e2024-04-19 13:12:13 +040022 nsCreator NamespaceCreator
23 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040024}
25
gioe72b54f2024-04-22 10:44:41 +040026func NewAppManager(repoIO soft.RepoIO, nsCreator NamespaceCreator, appDirRoot string) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040027 return &AppManager{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040028 repoIO,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040029 nsCreator,
gio308105e2024-04-19 13:12:13 +040030 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040031 }, nil
32}
33
gioe72b54f2024-04-22 10:44:41 +040034func (m *AppManager) Config() (EnvConfig, error) {
35 var cfg EnvConfig
36 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
37 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040038 } else {
39 return cfg, nil
40 }
41}
42
gio3cdee592024-04-17 10:15:56 +040043func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
44 var cfg AppInstanceConfig
gioe72b54f2024-04-22 10:44:41 +040045 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040046 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040047 } else {
48 return cfg, nil
49 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040050}
51
gio308105e2024-04-19 13:12:13 +040052func (m *AppManager) FindAllInstances() ([]AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040053 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040054 if err != nil {
55 return nil, err
56 }
gio3cdee592024-04-17 10:15:56 +040057 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040058 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040059 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
60 if err != nil {
61 return nil, err
62 }
63 cfg.Id = app
64 ret = append(ret, cfg)
65 }
66 return ret, nil
67}
68
69func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040070 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio308105e2024-04-19 13:12:13 +040071 if err != nil {
72 return nil, err
73 }
74 ret := make([]AppInstanceConfig, 0)
75 for _, app := range kust.Resources {
76 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +040077 if err != nil {
78 return nil, err
79 }
80 cfg.Id = app
81 if cfg.AppId == name {
82 ret = append(ret, cfg)
83 }
84 }
85 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040086}
87
gio3cdee592024-04-17 10:15:56 +040088func (m *AppManager) FindInstance(id string) (AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040089 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040090 if err != nil {
gio3cdee592024-04-17 10:15:56 +040091 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040092 }
93 for _, app := range kust.Resources {
94 if app == id {
gio308105e2024-04-19 13:12:13 +040095 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +040096 if err != nil {
gio3cdee592024-04-17 10:15:56 +040097 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040098 }
99 cfg.Id = id
100 return cfg, nil
101 }
102 }
gio3cdee592024-04-17 10:15:56 +0400103 return AppInstanceConfig{}, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400104}
105
gio3cdee592024-04-17 10:15:56 +0400106func (m *AppManager) AppConfig(name string) (AppInstanceConfig, error) {
gio3cdee592024-04-17 10:15:56 +0400107 var cfg AppInstanceConfig
gioe72b54f2024-04-22 10:44:41 +0400108 if err := soft.ReadJson(m.repoIO, filepath.Join(m.appDirRoot, name, "config.json"), &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400109 return AppInstanceConfig{}, err
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400110 }
gio308105e2024-04-19 13:12:13 +0400111 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400112}
113
gio3af43942024-04-16 08:13:50 +0400114type allocatePortReq struct {
115 Protocol string `json:"protocol"`
116 SourcePort int `json:"sourcePort"`
117 TargetService string `json:"targetService"`
118 TargetPort int `json:"targetPort"`
119}
120
121func openPorts(ports []PortForward) error {
122 for _, p := range ports {
123 var buf bytes.Buffer
124 req := allocatePortReq{
125 Protocol: p.Protocol,
126 SourcePort: p.SourcePort,
127 TargetService: p.TargetService,
128 TargetPort: p.TargetPort,
129 }
130 if err := json.NewEncoder(&buf).Encode(req); err != nil {
131 return err
132 }
133 resp, err := http.Post(p.Allocator, "application/json", &buf)
134 if err != nil {
135 return err
136 }
137 if resp.StatusCode != http.StatusOK {
138 return fmt.Errorf("Could not allocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
139 }
140 }
141 return nil
142}
143
gioe72b54f2024-04-22 10:44:41 +0400144func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400145 for p := filepath.Clean(path); p != "/"; {
146 parent, child := filepath.Split(p)
147 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400148 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400149 if err != nil {
150 if errors.Is(err, fs.ErrNotExist) {
gioe72b54f2024-04-22 10:44:41 +0400151 k := io.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400152 kust = &k
153 } else {
154 return err
155 }
156 }
157 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400158 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400159 return err
160 }
161 p = filepath.Clean(parent)
162 }
163 return nil
164}
165
gio3cdee592024-04-17 10:15:56 +0400166// TODO(gio): rename to CommitApp
gioe72b54f2024-04-22 10:44:41 +0400167func InstallApp(
168 repo soft.RepoIO,
169 appDir string,
170 name string,
171 config any,
172 ports []PortForward,
173 resources CueAppData,
174 data CueAppData,
175 opts ...soft.DoOption) error {
gio308105e2024-04-19 13:12:13 +0400176 // if err := openPorts(rendered.Ports); err != nil {
177 // return err
178 // }
gioe72b54f2024-04-22 10:44:41 +0400179 return repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400180 if err := r.RemoveDir(appDir); err != nil {
181 return "", err
182 }
183 resourcesDir := path.Join(appDir, "resources")
184 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400185 return "", err
186 }
187 {
gioe72b54f2024-04-22 10:44:41 +0400188 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400189 return "", err
190 }
gioe72b54f2024-04-22 10:44:41 +0400191 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400192 return "", err
193 }
gioe72b54f2024-04-22 10:44:41 +0400194 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400195 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
196 return "", fmt.Errorf("%s is forbidden", name)
197 }
198 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400199 if err != nil {
200 return "", err
201 }
gio308105e2024-04-19 13:12:13 +0400202 defer w.Close()
203 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400204 return "", err
205 }
206 }
gio308105e2024-04-19 13:12:13 +0400207 }
208 {
209 if err := createKustomizationChain(r, resourcesDir); err != nil {
210 return "", err
211 }
gioe72b54f2024-04-22 10:44:41 +0400212 appKust := io.NewKustomization()
213 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400214 appKust.AddResources(name)
215 w, err := r.Writer(path.Join(resourcesDir, name))
216 if err != nil {
217 return "", err
218 }
219 defer w.Close()
220 if _, err := w.Write(contents); err != nil {
221 return "", err
222 }
223 }
gioe72b54f2024-04-22 10:44:41 +0400224 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400225 return "", err
226 }
227 }
gioe72b54f2024-04-22 10:44:41 +0400228 return fmt.Sprintf("install: %s", name), nil
gio308105e2024-04-19 13:12:13 +0400229 }, opts...)
gio3af43942024-04-16 08:13:50 +0400230}
231
gio3cdee592024-04-17 10:15:56 +0400232// TODO(gio): commit instanceId -> appDir mapping as well
233func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any) error {
gio3af43942024-04-16 08:13:50 +0400234 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400235 if err := m.repoIO.Pull(); err != nil {
236 return err
237 }
gio3cdee592024-04-17 10:15:56 +0400238 if err := m.nsCreator.Create(namespace); err != nil {
239 return err
240 }
241 env, err := m.Config()
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400242 if err != nil {
243 return err
244 }
gio3cdee592024-04-17 10:15:56 +0400245 release := Release{
246 AppInstanceId: instanceId,
247 Namespace: namespace,
248 RepoAddr: m.repoIO.FullAddress(),
249 AppDir: appDir,
250 }
251 rendered, err := app.Render(release, env, values)
gioef01fbb2024-04-12 16:52:59 +0400252 if err != nil {
253 return err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400254 }
gioe72b54f2024-04-22 10:44:41 +0400255 return InstallApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400256}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400257
gioe72b54f2024-04-22 10:44:41 +0400258func (m *AppManager) Update(app EnvApp, instanceId string, values map[string]any, opts ...soft.DoOption) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400259 if err := m.repoIO.Pull(); err != nil {
260 return err
261 }
gio3cdee592024-04-17 10:15:56 +0400262 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400263 if err != nil {
264 return err
265 }
gio308105e2024-04-19 13:12:13 +0400266 instanceDir := filepath.Join(m.appDirRoot, instanceId)
267 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400268 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400269 if err != nil {
270 return err
271 }
gio3cdee592024-04-17 10:15:56 +0400272 release := Release{
273 AppInstanceId: instanceId,
274 Namespace: config.Release.Namespace,
275 RepoAddr: m.repoIO.FullAddress(),
276 AppDir: instanceDir,
277 }
278 rendered, err := app.Render(release, env, values)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400279 if err != nil {
280 return err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400281 }
gioe72b54f2024-04-22 10:44:41 +0400282 return InstallApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400283}
284
285func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400286 if err := m.repoIO.Pull(); err != nil {
287 return err
288 }
gioe72b54f2024-04-22 10:44:41 +0400289 return m.repoIO.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400290 r.RemoveDir(filepath.Join(m.appDirRoot, instanceId))
291 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400292 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400293 if err != nil {
294 return "", err
295 }
296 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400297 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400298 return fmt.Sprintf("uninstall: %s", instanceId), nil
299 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400300}
301
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400302// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gioe72b54f2024-04-22 10:44:41 +0400303func CreateNetworks(env EnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400304 return []Network{
305 {
306 Name: "Public",
gio3cdee592024-04-17 10:15:56 +0400307 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
308 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
309 Domain: env.Domain,
310 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400311 },
312 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400313 Name: "Private",
gio3cdee592024-04-17 10:15:56 +0400314 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
315 Domain: env.PrivateDomain,
316 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400317 },
318 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400319}
gio3cdee592024-04-17 10:15:56 +0400320
321// InfraAppmanager
322
323type InfraAppManager struct {
gioe72b54f2024-04-22 10:44:41 +0400324 repoIO soft.RepoIO
gio3cdee592024-04-17 10:15:56 +0400325 nsCreator NamespaceCreator
326}
327
gioe72b54f2024-04-22 10:44:41 +0400328func NewInfraAppManager(repoIO soft.RepoIO, nsCreator NamespaceCreator) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400329 return &InfraAppManager{
330 repoIO,
331 nsCreator,
332 }, nil
333}
334
335func (m *InfraAppManager) Config() (InfraConfig, error) {
336 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400337 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400338 return InfraConfig{}, err
339 } else {
340 return cfg, nil
341 }
342}
343
gioe72b54f2024-04-22 10:44:41 +0400344func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
345 var cfg InfraAppInstanceConfig
346 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
347 return InfraAppInstanceConfig{}, err
348 } else {
349 return cfg, nil
350 }
351}
352
353func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
354 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
355 if err != nil {
356 return InfraAppInstanceConfig{}, err
357 }
358 for _, app := range kust.Resources {
359 if app == id {
360 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
361 if err != nil {
362 return InfraAppInstanceConfig{}, err
363 }
364 cfg.Id = id
365 return cfg, nil
366 }
367 }
368 return InfraAppInstanceConfig{}, nil
369}
370
gio3cdee592024-04-17 10:15:56 +0400371func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) error {
372 appDir = filepath.Clean(appDir)
373 if err := m.repoIO.Pull(); err != nil {
374 return err
375 }
376 if err := m.nsCreator.Create(namespace); err != nil {
377 return err
378 }
379 infra, err := m.Config()
380 if err != nil {
381 return err
382 }
383 release := Release{
384 Namespace: namespace,
385 RepoAddr: m.repoIO.FullAddress(),
386 AppDir: appDir,
387 }
388 rendered, err := app.Render(release, infra, values)
389 if err != nil {
390 return err
391 }
gioe72b54f2024-04-22 10:44:41 +0400392 return InstallApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
393}
394
395func (m *InfraAppManager) Update(app InfraApp, instanceId string, values map[string]any, opts ...soft.DoOption) error {
396 if err := m.repoIO.Pull(); err != nil {
397 return err
398 }
399 env, err := m.Config()
400 if err != nil {
401 return err
402 }
403 instanceDir := filepath.Join("/infrastructure", instanceId)
404 instanceConfigPath := filepath.Join(instanceDir, "config.json")
405 config, err := m.appConfig(instanceConfigPath)
406 if err != nil {
407 return err
408 }
409 release := Release{
410 AppInstanceId: instanceId,
411 Namespace: config.Release.Namespace,
412 RepoAddr: m.repoIO.FullAddress(),
413 AppDir: instanceDir,
414 }
415 rendered, err := app.Render(release, env, values)
416 if err != nil {
417 return err
418 }
419 return InstallApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400420}