blob: 612f9a7a583bf1bd7cf0681110eb423d8598571c [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"
giof8843412024-05-22 16:38:05 +040012 "strings"
gioe72b54f2024-04-22 10:44:41 +040013
14 "github.com/giolekva/pcloud/core/installer/io"
15 "github.com/giolekva/pcloud/core/installer/soft"
gio778577f2024-04-29 09:44:38 +040016
giof8843412024-05-22 16:38:05 +040017 helmv2 "github.com/fluxcd/helm-controller/api/v2"
gio778577f2024-04-29 09:44:38 +040018 "sigs.k8s.io/yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040019)
20
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040021const configFileName = "config.yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040022const kustomizationFileName = "kustomization.yaml"
23
gio778577f2024-04-29 09:44:38 +040024var ErrorNotFound = errors.New("not found")
25
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040026type AppManager struct {
gioe72b54f2024-04-22 10:44:41 +040027 repoIO soft.RepoIO
giof8843412024-05-22 16:38:05 +040028 nsc NamespaceCreator
29 jc JobCreator
30 hf HelmFetcher
gio308105e2024-04-19 13:12:13 +040031 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040032}
33
giof8843412024-05-22 16:38:05 +040034func NewAppManager(
35 repoIO soft.RepoIO,
36 nsc NamespaceCreator,
37 jc JobCreator,
38 hf HelmFetcher,
39 appDirRoot string,
40) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040041 return &AppManager{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040042 repoIO,
giof8843412024-05-22 16:38:05 +040043 nsc,
44 jc,
45 hf,
gio308105e2024-04-19 13:12:13 +040046 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040047 }, nil
48}
49
gioe72b54f2024-04-22 10:44:41 +040050func (m *AppManager) Config() (EnvConfig, error) {
51 var cfg EnvConfig
52 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
53 return EnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040054 } else {
55 return cfg, nil
56 }
57}
58
gio3cdee592024-04-17 10:15:56 +040059func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
60 var cfg AppInstanceConfig
gioe72b54f2024-04-22 10:44:41 +040061 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040062 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040063 } else {
64 return cfg, nil
65 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040066}
67
gio308105e2024-04-19 13:12:13 +040068func (m *AppManager) FindAllInstances() ([]AppInstanceConfig, error) {
gio09a3e5b2024-04-26 14:11:06 +040069 m.repoIO.Pull()
gioe72b54f2024-04-22 10:44:41 +040070 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040071 if err != nil {
72 return nil, err
73 }
gio3cdee592024-04-17 10:15:56 +040074 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040075 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040076 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
77 if err != nil {
78 return nil, err
79 }
80 cfg.Id = app
81 ret = append(ret, cfg)
82 }
83 return ret, nil
84}
85
86func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +040087 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio308105e2024-04-19 13:12:13 +040088 if err != nil {
89 return nil, err
90 }
91 ret := make([]AppInstanceConfig, 0)
92 for _, app := range kust.Resources {
93 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +040094 if err != nil {
95 return nil, err
96 }
97 cfg.Id = app
98 if cfg.AppId == name {
99 ret = append(ret, cfg)
100 }
101 }
102 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400103}
104
gio778577f2024-04-29 09:44:38 +0400105func (m *AppManager) FindInstance(id string) (*AppInstanceConfig, error) {
gioe72b54f2024-04-22 10:44:41 +0400106 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +0400107 if err != nil {
gio778577f2024-04-29 09:44:38 +0400108 return nil, err
gio3af43942024-04-16 08:13:50 +0400109 }
110 for _, app := range kust.Resources {
111 if app == id {
gio308105e2024-04-19 13:12:13 +0400112 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +0400113 if err != nil {
gio778577f2024-04-29 09:44:38 +0400114 return nil, err
gio3af43942024-04-16 08:13:50 +0400115 }
116 cfg.Id = id
gio778577f2024-04-29 09:44:38 +0400117 return &cfg, nil
gio3af43942024-04-16 08:13:50 +0400118 }
119 }
gio778577f2024-04-29 09:44:38 +0400120 return nil, ErrorNotFound
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400121}
122
giof8843412024-05-22 16:38:05 +0400123func GetCueAppData(fs soft.RepoFS, dir string) (CueAppData, error) {
124 files, err := fs.ListDir(dir)
125 if err != nil {
126 return nil, err
127 }
128 cfg := CueAppData{}
129 for _, f := range files {
130 if !f.IsDir() && strings.HasSuffix(f.Name(), ".cue") {
131 contents, err := soft.ReadFile(fs, filepath.Join(dir, f.Name()))
132 if err != nil {
133 return nil, err
134 }
135 cfg[f.Name()] = contents
136 }
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400137 }
gio308105e2024-04-19 13:12:13 +0400138 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400139}
140
giof8843412024-05-22 16:38:05 +0400141func (m *AppManager) GetInstanceApp(id string) (EnvApp, error) {
142 cfg, err := GetCueAppData(m.repoIO, filepath.Join(m.appDirRoot, id))
143 if err != nil {
144 return nil, err
145 }
146 return NewCueEnvApp(cfg)
147}
148
gio3af43942024-04-16 08:13:50 +0400149type allocatePortReq struct {
150 Protocol string `json:"protocol"`
151 SourcePort int `json:"sourcePort"`
152 TargetService string `json:"targetService"`
153 TargetPort int `json:"targetPort"`
154}
155
156func openPorts(ports []PortForward) error {
157 for _, p := range ports {
158 var buf bytes.Buffer
159 req := allocatePortReq{
160 Protocol: p.Protocol,
161 SourcePort: p.SourcePort,
162 TargetService: p.TargetService,
163 TargetPort: p.TargetPort,
164 }
165 if err := json.NewEncoder(&buf).Encode(req); err != nil {
166 return err
167 }
168 resp, err := http.Post(p.Allocator, "application/json", &buf)
169 if err != nil {
170 return err
171 }
172 if resp.StatusCode != http.StatusOK {
173 return fmt.Errorf("Could not allocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
174 }
175 }
176 return nil
177}
178
gioe72b54f2024-04-22 10:44:41 +0400179func createKustomizationChain(r soft.RepoFS, path string) error {
gio3af43942024-04-16 08:13:50 +0400180 for p := filepath.Clean(path); p != "/"; {
181 parent, child := filepath.Split(p)
182 kustPath := filepath.Join(parent, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400183 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400184 if err != nil {
185 if errors.Is(err, fs.ErrNotExist) {
gioe72b54f2024-04-22 10:44:41 +0400186 k := io.NewKustomization()
gio3af43942024-04-16 08:13:50 +0400187 kust = &k
188 } else {
189 return err
190 }
191 }
192 kust.AddResources(child)
gioe72b54f2024-04-22 10:44:41 +0400193 if err := soft.WriteYaml(r, kustPath, kust); err != nil {
gio3af43942024-04-16 08:13:50 +0400194 return err
195 }
196 p = filepath.Clean(parent)
197 }
198 return nil
199}
200
gio778577f2024-04-29 09:44:38 +0400201type Resource struct {
202 Name string `json:"name"`
203 Namespace string `json:"namespace"`
giof9f0bee2024-06-11 20:10:05 +0400204 Info string `json:"info"`
gio778577f2024-04-29 09:44:38 +0400205}
206
207type ReleaseResources struct {
208 Helm []Resource
209}
210
gio3cdee592024-04-17 10:15:56 +0400211// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400212func installApp(
gioe72b54f2024-04-22 10:44:41 +0400213 repo soft.RepoIO,
214 appDir string,
215 name string,
216 config any,
217 ports []PortForward,
218 resources CueAppData,
219 data CueAppData,
giof8843412024-05-22 16:38:05 +0400220 opts ...InstallOption,
gio778577f2024-04-29 09:44:38 +0400221) (ReleaseResources, error) {
giof8843412024-05-22 16:38:05 +0400222 var o installOptions
223 for _, i := range opts {
224 i(&o)
225 }
226 dopts := []soft.DoOption{}
227 if o.Branch != "" {
228 dopts = append(dopts, soft.WithForce())
229 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
230 }
231 if o.NoPublish {
232 dopts = append(dopts, soft.WithNoCommit())
233 }
gio778577f2024-04-29 09:44:38 +0400234 return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400235 if err := r.RemoveDir(appDir); err != nil {
236 return "", err
237 }
238 resourcesDir := path.Join(appDir, "resources")
239 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400240 return "", err
241 }
242 {
gioe72b54f2024-04-22 10:44:41 +0400243 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400244 return "", err
245 }
gioe72b54f2024-04-22 10:44:41 +0400246 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400247 return "", err
248 }
gioe72b54f2024-04-22 10:44:41 +0400249 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400250 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
251 return "", fmt.Errorf("%s is forbidden", name)
252 }
253 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400254 if err != nil {
255 return "", err
256 }
gio308105e2024-04-19 13:12:13 +0400257 defer w.Close()
258 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400259 return "", err
260 }
261 }
gio308105e2024-04-19 13:12:13 +0400262 }
263 {
264 if err := createKustomizationChain(r, resourcesDir); err != nil {
265 return "", err
266 }
gioe72b54f2024-04-22 10:44:41 +0400267 appKust := io.NewKustomization()
268 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400269 appKust.AddResources(name)
270 w, err := r.Writer(path.Join(resourcesDir, name))
271 if err != nil {
272 return "", err
273 }
274 defer w.Close()
275 if _, err := w.Write(contents); err != nil {
276 return "", err
277 }
278 }
gioe72b54f2024-04-22 10:44:41 +0400279 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400280 return "", err
281 }
282 }
gioe72b54f2024-04-22 10:44:41 +0400283 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400284 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400285}
286
gio3cdee592024-04-17 10:15:56 +0400287// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400288func (m *AppManager) Install(
289 app EnvApp,
290 instanceId string,
291 appDir string,
292 namespace string,
293 values map[string]any,
294 opts ...InstallOption,
295) (ReleaseResources, error) {
gio0eaf2712024-04-14 13:08:46 +0400296 o := &installOptions{}
297 for _, i := range opts {
298 i(o)
299 }
gio3af43942024-04-16 08:13:50 +0400300 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400301 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400302 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400303 }
giof8843412024-05-22 16:38:05 +0400304 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400305 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400306 }
gio0eaf2712024-04-14 13:08:46 +0400307 var env EnvConfig
308 if o.Env != nil {
309 env = *o.Env
310 } else {
311 var err error
312 env, err = m.Config()
313 if err != nil {
314 return ReleaseResources{}, err
315 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400316 }
giof8843412024-05-22 16:38:05 +0400317 var lg LocalChartGenerator
318 if o.LG != nil {
319 lg = o.LG
320 } else {
321 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
322 }
gio3cdee592024-04-17 10:15:56 +0400323 release := Release{
324 AppInstanceId: instanceId,
325 Namespace: namespace,
326 RepoAddr: m.repoIO.FullAddress(),
327 AppDir: appDir,
328 }
giof8843412024-05-22 16:38:05 +0400329 rendered, err := app.Render(release, env, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400330 if err != nil {
gio778577f2024-04-29 09:44:38 +0400331 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400332 }
giof8843412024-05-22 16:38:05 +0400333 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
334 if o.FetchContainerImages {
335 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
336 return ReleaseResources{}, err
337 }
gio0eaf2712024-04-14 13:08:46 +0400338 }
giof8843412024-05-22 16:38:05 +0400339 var localCharts map[string]helmv2.HelmChartTemplateSpec
340 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
341 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
342 if err != nil {
343 return "", err
344 }
345 localCharts = generateLocalCharts(lg, charts)
346 return "pull helm charts", nil
347 }); err != nil {
348 return ReleaseResources{}, err
349 }
350 if o.FetchContainerImages {
351 release.ImageRegistry = imageRegistry
352 }
353 rendered, err = app.Render(release, env, values, localCharts)
354 if err != nil {
355 return ReleaseResources{}, err
356 }
357 if _, err := installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...); err != nil {
gio778577f2024-04-29 09:44:38 +0400358 return ReleaseResources{}, err
359 }
gioff2a29a2024-05-01 17:06:42 +0400360 // TODO(gio): add ingress-nginx to release resources
361 if err := openPorts(rendered.Ports); err != nil {
362 return ReleaseResources{}, err
363 }
gio778577f2024-04-29 09:44:38 +0400364 return ReleaseResources{
365 Helm: extractHelm(rendered.Resources),
366 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400367}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400368
gio778577f2024-04-29 09:44:38 +0400369type helmRelease struct {
giof9f0bee2024-06-11 20:10:05 +0400370 Metadata struct {
371 Name string `json:"name"`
372 Namespace string `json:"namespace"`
373 Annotations map[string]string `json:"annotations"`
374 } `json:"metadata"`
375 Kind string `json:"kind"`
376 Status struct {
gio778577f2024-04-29 09:44:38 +0400377 Conditions []struct {
378 Type string `json:"type"`
379 Status string `json:"status"`
380 } `json:"conditions"`
381 } `json:"status,omitempty"`
382}
383
384func extractHelm(resources CueAppData) []Resource {
385 ret := make([]Resource, 0, len(resources))
386 for _, contents := range resources {
387 var h helmRelease
388 if err := yaml.Unmarshal(contents, &h); err != nil {
389 panic(err) // TODO(gio): handle
390 }
gio0eaf2712024-04-14 13:08:46 +0400391 if h.Kind == "HelmRelease" {
giof9f0bee2024-06-11 20:10:05 +0400392 res := Resource{
393 Name: h.Metadata.Name,
394 Namespace: h.Metadata.Namespace,
395 Info: fmt.Sprintf("%s/%s", h.Metadata.Namespace, h.Metadata.Name),
396 }
397 if h.Metadata.Annotations != nil {
398 info, ok := h.Metadata.Annotations["dodo.cloud/installer-info"]
399 if ok && len(info) != 0 {
400 res.Info = info
401 }
402 }
403 ret = append(ret, res)
gio0eaf2712024-04-14 13:08:46 +0400404 }
gio778577f2024-04-29 09:44:38 +0400405 }
406 return ret
407}
408
giof8843412024-05-22 16:38:05 +0400409// TODO(gio): take app configuration from the repo
410func (m *AppManager) Update(
411 instanceId string,
412 values map[string]any,
413 opts ...InstallOption,
414) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400415 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400416 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400417 }
gio3cdee592024-04-17 10:15:56 +0400418 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400419 if err != nil {
gio778577f2024-04-29 09:44:38 +0400420 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400421 }
gio308105e2024-04-19 13:12:13 +0400422 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400423 app, err := m.GetInstanceApp(instanceId)
424 if err != nil {
425 return ReleaseResources{}, err
426 }
gio308105e2024-04-19 13:12:13 +0400427 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400428 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400429 if err != nil {
gio778577f2024-04-29 09:44:38 +0400430 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400431 }
giof8843412024-05-22 16:38:05 +0400432 localCharts, err := extractLocalCharts(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
433 if err != nil {
434 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400435 }
giof8843412024-05-22 16:38:05 +0400436 rendered, err := app.Render(config.Release, env, values, localCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400437 if err != nil {
gio778577f2024-04-29 09:44:38 +0400438 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400439 }
gio0eaf2712024-04-14 13:08:46 +0400440 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400441}
442
443func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400444 if err := m.repoIO.Pull(); err != nil {
445 return err
446 }
gioe72b54f2024-04-22 10:44:41 +0400447 return m.repoIO.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400448 r.RemoveDir(filepath.Join(m.appDirRoot, instanceId))
449 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400450 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400451 if err != nil {
452 return "", err
453 }
454 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400455 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400456 return fmt.Sprintf("uninstall: %s", instanceId), nil
457 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400458}
459
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400460// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gioe72b54f2024-04-22 10:44:41 +0400461func CreateNetworks(env EnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400462 return []Network{
463 {
464 Name: "Public",
gio3cdee592024-04-17 10:15:56 +0400465 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
466 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
467 Domain: env.Domain,
468 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400469 },
470 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400471 Name: "Private",
gio3cdee592024-04-17 10:15:56 +0400472 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
473 Domain: env.PrivateDomain,
474 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400475 },
476 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400477}
gio3cdee592024-04-17 10:15:56 +0400478
gio0eaf2712024-04-14 13:08:46 +0400479type installOptions struct {
giof8843412024-05-22 16:38:05 +0400480 NoPublish bool
481 Env *EnvConfig
482 Branch string
483 LG LocalChartGenerator
484 FetchContainerImages bool
gio0eaf2712024-04-14 13:08:46 +0400485}
486
487type InstallOption func(*installOptions)
488
489func WithConfig(env *EnvConfig) InstallOption {
490 return func(o *installOptions) {
491 o.Env = env
492 }
493}
494
495func WithBranch(branch string) InstallOption {
496 return func(o *installOptions) {
497 o.Branch = branch
498 }
499}
500
giof8843412024-05-22 16:38:05 +0400501func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
502 return func(o *installOptions) {
503 o.LG = lg
504 }
505}
506
507func WithFetchContainerImages() InstallOption {
508 return func(o *installOptions) {
509 o.FetchContainerImages = true
510 }
511}
512
513func WithNoPublish() InstallOption {
514 return func(o *installOptions) {
515 o.NoPublish = true
516 }
517}
518
519// InfraAppmanager
520
521type InfraAppManager struct {
522 repoIO soft.RepoIO
523 nsc NamespaceCreator
524 hf HelmFetcher
525 lg LocalChartGenerator
526}
527
528func NewInfraAppManager(
529 repoIO soft.RepoIO,
530 nsc NamespaceCreator,
531 hf HelmFetcher,
532 lg LocalChartGenerator,
533) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400534 return &InfraAppManager{
535 repoIO,
giof8843412024-05-22 16:38:05 +0400536 nsc,
537 hf,
538 lg,
gio3cdee592024-04-17 10:15:56 +0400539 }, nil
540}
541
542func (m *InfraAppManager) Config() (InfraConfig, error) {
543 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400544 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400545 return InfraConfig{}, err
546 } else {
547 return cfg, nil
548 }
549}
550
gioe72b54f2024-04-22 10:44:41 +0400551func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
552 var cfg InfraAppInstanceConfig
553 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
554 return InfraAppInstanceConfig{}, err
555 } else {
556 return cfg, nil
557 }
558}
559
560func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
561 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
562 if err != nil {
563 return InfraAppInstanceConfig{}, err
564 }
565 for _, app := range kust.Resources {
566 if app == id {
567 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
568 if err != nil {
569 return InfraAppInstanceConfig{}, err
570 }
571 cfg.Id = id
572 return cfg, nil
573 }
574 }
575 return InfraAppInstanceConfig{}, nil
576}
577
gio778577f2024-04-29 09:44:38 +0400578func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400579 appDir = filepath.Clean(appDir)
580 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400581 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400582 }
giof8843412024-05-22 16:38:05 +0400583 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400584 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400585 }
586 infra, err := m.Config()
587 if err != nil {
gio778577f2024-04-29 09:44:38 +0400588 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400589 }
590 release := Release{
591 Namespace: namespace,
592 RepoAddr: m.repoIO.FullAddress(),
593 AppDir: appDir,
594 }
giof8843412024-05-22 16:38:05 +0400595 rendered, err := app.Render(release, infra, values, nil)
596 if err != nil {
597 return ReleaseResources{}, err
598 }
599 var localCharts map[string]helmv2.HelmChartTemplateSpec
600 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
601 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
602 if err != nil {
603 return "", err
604 }
605 localCharts = generateLocalCharts(m.lg, charts)
606 return "pull helm charts", nil
607 }); err != nil {
608 return ReleaseResources{}, err
609 }
610 rendered, err = app.Render(release, infra, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400611 if err != nil {
gio778577f2024-04-29 09:44:38 +0400612 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400613 }
gio0eaf2712024-04-14 13:08:46 +0400614 return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
gioe72b54f2024-04-22 10:44:41 +0400615}
616
giof8843412024-05-22 16:38:05 +0400617// TODO(gio): take app configuration from the repo
618func (m *InfraAppManager) Update(
619 instanceId string,
620 values map[string]any,
621 opts ...InstallOption,
622) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400623 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400624 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400625 }
626 env, err := m.Config()
627 if err != nil {
gio778577f2024-04-29 09:44:38 +0400628 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400629 }
630 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400631 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
632 if err != nil {
633 return ReleaseResources{}, err
634 }
635 app, err := NewCueInfraApp(appCfg)
636 if err != nil {
637 return ReleaseResources{}, err
638 }
gioe72b54f2024-04-22 10:44:41 +0400639 instanceConfigPath := filepath.Join(instanceDir, "config.json")
640 config, err := m.appConfig(instanceConfigPath)
641 if err != nil {
gio778577f2024-04-29 09:44:38 +0400642 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400643 }
giof8843412024-05-22 16:38:05 +0400644 localCharts, err := extractLocalCharts(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
645 if err != nil {
646 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400647 }
giof8843412024-05-22 16:38:05 +0400648 rendered, err := app.Render(config.Release, env, values, localCharts)
gioe72b54f2024-04-22 10:44:41 +0400649 if err != nil {
gio778577f2024-04-29 09:44:38 +0400650 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400651 }
gio0eaf2712024-04-14 13:08:46 +0400652 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400653}
giof8843412024-05-22 16:38:05 +0400654
655func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
656 ret := make(map[string]string)
657 for name, chart := range charts.Git {
658 chartRoot := filepath.Join(root, name)
659 ret[name] = chartRoot
660 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
661 return nil, err
662 }
663 }
664 return ret, nil
665}
666
667func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
668 ret := make(map[string]helmv2.HelmChartTemplateSpec)
669 for name, path := range charts {
670 ret[name] = g.Generate(path)
671 }
672 return ret
673}
674
675func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
676 for _, img := range imgs {
677 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
678 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
679 "skopeo",
680 "--insecure-policy",
681 "copy",
682 "--dest-tls-verify=false", // TODO(gio): enable
683 "--multi-arch=all",
684 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
685 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
686 }); err != nil {
687 return err
688 }
689 }
690 return nil
691}
692
693type renderedInstance struct {
694 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
695}
696
697func extractLocalCharts(fs soft.RepoFS, path string) (map[string]helmv2.HelmChartTemplateSpec, error) {
698 r, err := fs.Reader(path)
699 if err != nil {
700 return nil, err
701 }
702 defer r.Close()
703 var cfg renderedInstance
704 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
705 return nil, err
706 }
707 return cfg.LocalCharts, nil
708}