blob: 9ee9671f920e90db6b0162d2be56ba5dc488db06 [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"`
204}
205
206type ReleaseResources struct {
207 Helm []Resource
208}
209
gio3cdee592024-04-17 10:15:56 +0400210// TODO(gio): rename to CommitApp
gio0eaf2712024-04-14 13:08:46 +0400211func installApp(
gioe72b54f2024-04-22 10:44:41 +0400212 repo soft.RepoIO,
213 appDir string,
214 name string,
215 config any,
216 ports []PortForward,
217 resources CueAppData,
218 data CueAppData,
giof8843412024-05-22 16:38:05 +0400219 opts ...InstallOption,
gio778577f2024-04-29 09:44:38 +0400220) (ReleaseResources, error) {
giof8843412024-05-22 16:38:05 +0400221 var o installOptions
222 for _, i := range opts {
223 i(&o)
224 }
225 dopts := []soft.DoOption{}
226 if o.Branch != "" {
227 dopts = append(dopts, soft.WithForce())
228 dopts = append(dopts, soft.WithCommitToBranch(o.Branch))
229 }
230 if o.NoPublish {
231 dopts = append(dopts, soft.WithNoCommit())
232 }
gio778577f2024-04-29 09:44:38 +0400233 return ReleaseResources{}, repo.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400234 if err := r.RemoveDir(appDir); err != nil {
235 return "", err
236 }
237 resourcesDir := path.Join(appDir, "resources")
238 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400239 return "", err
240 }
241 {
gioe72b54f2024-04-22 10:44:41 +0400242 if err := soft.WriteYaml(r, path.Join(appDir, configFileName), config); err != nil {
gio3af43942024-04-16 08:13:50 +0400243 return "", err
244 }
gioe72b54f2024-04-22 10:44:41 +0400245 if err := soft.WriteJson(r, path.Join(appDir, "config.json"), config); err != nil {
gio308105e2024-04-19 13:12:13 +0400246 return "", err
247 }
gioe72b54f2024-04-22 10:44:41 +0400248 for name, contents := range data {
gio308105e2024-04-19 13:12:13 +0400249 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
250 return "", fmt.Errorf("%s is forbidden", name)
251 }
252 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400253 if err != nil {
254 return "", err
255 }
gio308105e2024-04-19 13:12:13 +0400256 defer w.Close()
257 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400258 return "", err
259 }
260 }
gio308105e2024-04-19 13:12:13 +0400261 }
262 {
263 if err := createKustomizationChain(r, resourcesDir); err != nil {
264 return "", err
265 }
gioe72b54f2024-04-22 10:44:41 +0400266 appKust := io.NewKustomization()
267 for name, contents := range resources {
gio308105e2024-04-19 13:12:13 +0400268 appKust.AddResources(name)
269 w, err := r.Writer(path.Join(resourcesDir, name))
270 if err != nil {
271 return "", err
272 }
273 defer w.Close()
274 if _, err := w.Write(contents); err != nil {
275 return "", err
276 }
277 }
gioe72b54f2024-04-22 10:44:41 +0400278 if err := soft.WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400279 return "", err
280 }
281 }
gioe72b54f2024-04-22 10:44:41 +0400282 return fmt.Sprintf("install: %s", name), nil
giof8843412024-05-22 16:38:05 +0400283 }, dopts...)
gio3af43942024-04-16 08:13:50 +0400284}
285
gio3cdee592024-04-17 10:15:56 +0400286// TODO(gio): commit instanceId -> appDir mapping as well
giof8843412024-05-22 16:38:05 +0400287func (m *AppManager) Install(
288 app EnvApp,
289 instanceId string,
290 appDir string,
291 namespace string,
292 values map[string]any,
293 opts ...InstallOption,
294) (ReleaseResources, error) {
gio0eaf2712024-04-14 13:08:46 +0400295 o := &installOptions{}
296 for _, i := range opts {
297 i(o)
298 }
gio3af43942024-04-16 08:13:50 +0400299 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400300 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400301 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400302 }
giof8843412024-05-22 16:38:05 +0400303 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400304 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400305 }
gio0eaf2712024-04-14 13:08:46 +0400306 var env EnvConfig
307 if o.Env != nil {
308 env = *o.Env
309 } else {
310 var err error
311 env, err = m.Config()
312 if err != nil {
313 return ReleaseResources{}, err
314 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400315 }
giof8843412024-05-22 16:38:05 +0400316 var lg LocalChartGenerator
317 if o.LG != nil {
318 lg = o.LG
319 } else {
320 lg = GitRepositoryLocalChartGenerator{env.Id, env.Id}
321 }
gio3cdee592024-04-17 10:15:56 +0400322 release := Release{
323 AppInstanceId: instanceId,
324 Namespace: namespace,
325 RepoAddr: m.repoIO.FullAddress(),
326 AppDir: appDir,
327 }
giof8843412024-05-22 16:38:05 +0400328 rendered, err := app.Render(release, env, values, nil)
gioef01fbb2024-04-12 16:52:59 +0400329 if err != nil {
gio778577f2024-04-29 09:44:38 +0400330 return ReleaseResources{}, err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400331 }
giof8843412024-05-22 16:38:05 +0400332 imageRegistry := fmt.Sprintf("zot.%s", env.PrivateDomain)
333 if o.FetchContainerImages {
334 if err := pullContainerImages(instanceId, rendered.ContainerImages, imageRegistry, namespace, m.jc); err != nil {
335 return ReleaseResources{}, err
336 }
gio0eaf2712024-04-14 13:08:46 +0400337 }
giof8843412024-05-22 16:38:05 +0400338 var localCharts map[string]helmv2.HelmChartTemplateSpec
339 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
340 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
341 if err != nil {
342 return "", err
343 }
344 localCharts = generateLocalCharts(lg, charts)
345 return "pull helm charts", nil
346 }); err != nil {
347 return ReleaseResources{}, err
348 }
349 if o.FetchContainerImages {
350 release.ImageRegistry = imageRegistry
351 }
352 rendered, err = app.Render(release, env, values, localCharts)
353 if err != nil {
354 return ReleaseResources{}, err
355 }
356 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 +0400357 return ReleaseResources{}, err
358 }
gioff2a29a2024-05-01 17:06:42 +0400359 // TODO(gio): add ingress-nginx to release resources
360 if err := openPorts(rendered.Ports); err != nil {
361 return ReleaseResources{}, err
362 }
gio778577f2024-04-29 09:44:38 +0400363 return ReleaseResources{
364 Helm: extractHelm(rendered.Resources),
365 }, nil
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400366}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400367
gio778577f2024-04-29 09:44:38 +0400368type helmRelease struct {
369 Metadata Resource `json:"metadata"`
gio0eaf2712024-04-14 13:08:46 +0400370 Kind string `json:"kind"`
gio778577f2024-04-29 09:44:38 +0400371 Status struct {
372 Conditions []struct {
373 Type string `json:"type"`
374 Status string `json:"status"`
375 } `json:"conditions"`
376 } `json:"status,omitempty"`
377}
378
379func extractHelm(resources CueAppData) []Resource {
380 ret := make([]Resource, 0, len(resources))
381 for _, contents := range resources {
382 var h helmRelease
383 if err := yaml.Unmarshal(contents, &h); err != nil {
384 panic(err) // TODO(gio): handle
385 }
gio0eaf2712024-04-14 13:08:46 +0400386 if h.Kind == "HelmRelease" {
387 ret = append(ret, h.Metadata)
388 }
gio778577f2024-04-29 09:44:38 +0400389 }
390 return ret
391}
392
giof8843412024-05-22 16:38:05 +0400393// TODO(gio): take app configuration from the repo
394func (m *AppManager) Update(
395 instanceId string,
396 values map[string]any,
397 opts ...InstallOption,
398) (ReleaseResources, error) {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400399 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400400 return ReleaseResources{}, err
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400401 }
gio3cdee592024-04-17 10:15:56 +0400402 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400403 if err != nil {
gio778577f2024-04-29 09:44:38 +0400404 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400405 }
gio308105e2024-04-19 13:12:13 +0400406 instanceDir := filepath.Join(m.appDirRoot, instanceId)
giof8843412024-05-22 16:38:05 +0400407 app, err := m.GetInstanceApp(instanceId)
408 if err != nil {
409 return ReleaseResources{}, err
410 }
gio308105e2024-04-19 13:12:13 +0400411 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400412 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400413 if err != nil {
gio778577f2024-04-29 09:44:38 +0400414 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400415 }
giof8843412024-05-22 16:38:05 +0400416 localCharts, err := extractLocalCharts(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
417 if err != nil {
418 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400419 }
giof8843412024-05-22 16:38:05 +0400420 rendered, err := app.Render(config.Release, env, values, localCharts)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400421 if err != nil {
gio778577f2024-04-29 09:44:38 +0400422 return ReleaseResources{}, err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400423 }
gio0eaf2712024-04-14 13:08:46 +0400424 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400425}
426
427func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400428 if err := m.repoIO.Pull(); err != nil {
429 return err
430 }
gioe72b54f2024-04-22 10:44:41 +0400431 return m.repoIO.Do(func(r soft.RepoFS) (string, error) {
gio308105e2024-04-19 13:12:13 +0400432 r.RemoveDir(filepath.Join(m.appDirRoot, instanceId))
433 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gioe72b54f2024-04-22 10:44:41 +0400434 kust, err := soft.ReadKustomization(r, kustPath)
gio3af43942024-04-16 08:13:50 +0400435 if err != nil {
436 return "", err
437 }
438 kust.RemoveResources(instanceId)
gioe72b54f2024-04-22 10:44:41 +0400439 soft.WriteYaml(r, kustPath, kust)
gio3af43942024-04-16 08:13:50 +0400440 return fmt.Sprintf("uninstall: %s", instanceId), nil
441 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400442}
443
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400444// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gioe72b54f2024-04-22 10:44:41 +0400445func CreateNetworks(env EnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400446 return []Network{
447 {
448 Name: "Public",
gio3cdee592024-04-17 10:15:56 +0400449 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
450 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
451 Domain: env.Domain,
452 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400453 },
454 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400455 Name: "Private",
gio3cdee592024-04-17 10:15:56 +0400456 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
457 Domain: env.PrivateDomain,
458 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400459 },
460 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400461}
gio3cdee592024-04-17 10:15:56 +0400462
gio0eaf2712024-04-14 13:08:46 +0400463type installOptions struct {
giof8843412024-05-22 16:38:05 +0400464 NoPublish bool
465 Env *EnvConfig
466 Branch string
467 LG LocalChartGenerator
468 FetchContainerImages bool
gio0eaf2712024-04-14 13:08:46 +0400469}
470
471type InstallOption func(*installOptions)
472
473func WithConfig(env *EnvConfig) InstallOption {
474 return func(o *installOptions) {
475 o.Env = env
476 }
477}
478
479func WithBranch(branch string) InstallOption {
480 return func(o *installOptions) {
481 o.Branch = branch
482 }
483}
484
giof8843412024-05-22 16:38:05 +0400485func WithLocalChartGenerator(lg LocalChartGenerator) InstallOption {
486 return func(o *installOptions) {
487 o.LG = lg
488 }
489}
490
491func WithFetchContainerImages() InstallOption {
492 return func(o *installOptions) {
493 o.FetchContainerImages = true
494 }
495}
496
497func WithNoPublish() InstallOption {
498 return func(o *installOptions) {
499 o.NoPublish = true
500 }
501}
502
503// InfraAppmanager
504
505type InfraAppManager struct {
506 repoIO soft.RepoIO
507 nsc NamespaceCreator
508 hf HelmFetcher
509 lg LocalChartGenerator
510}
511
512func NewInfraAppManager(
513 repoIO soft.RepoIO,
514 nsc NamespaceCreator,
515 hf HelmFetcher,
516 lg LocalChartGenerator,
517) (*InfraAppManager, error) {
gio3cdee592024-04-17 10:15:56 +0400518 return &InfraAppManager{
519 repoIO,
giof8843412024-05-22 16:38:05 +0400520 nsc,
521 hf,
522 lg,
gio3cdee592024-04-17 10:15:56 +0400523 }, nil
524}
525
526func (m *InfraAppManager) Config() (InfraConfig, error) {
527 var cfg InfraConfig
gioe72b54f2024-04-22 10:44:41 +0400528 if err := soft.ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400529 return InfraConfig{}, err
530 } else {
531 return cfg, nil
532 }
533}
534
gioe72b54f2024-04-22 10:44:41 +0400535func (m *InfraAppManager) appConfig(path string) (InfraAppInstanceConfig, error) {
536 var cfg InfraAppInstanceConfig
537 if err := soft.ReadJson(m.repoIO, path, &cfg); err != nil {
538 return InfraAppInstanceConfig{}, err
539 } else {
540 return cfg, nil
541 }
542}
543
544func (m *InfraAppManager) FindInstance(id string) (InfraAppInstanceConfig, error) {
545 kust, err := soft.ReadKustomization(m.repoIO, filepath.Join("/infrastructure", "kustomization.yaml"))
546 if err != nil {
547 return InfraAppInstanceConfig{}, err
548 }
549 for _, app := range kust.Resources {
550 if app == id {
551 cfg, err := m.appConfig(filepath.Join("/infrastructure", app, "config.json"))
552 if err != nil {
553 return InfraAppInstanceConfig{}, err
554 }
555 cfg.Id = id
556 return cfg, nil
557 }
558 }
559 return InfraAppInstanceConfig{}, nil
560}
561
gio778577f2024-04-29 09:44:38 +0400562func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) (ReleaseResources, error) {
gio3cdee592024-04-17 10:15:56 +0400563 appDir = filepath.Clean(appDir)
564 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400565 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400566 }
giof8843412024-05-22 16:38:05 +0400567 if err := m.nsc.Create(namespace); err != nil {
gio778577f2024-04-29 09:44:38 +0400568 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400569 }
570 infra, err := m.Config()
571 if err != nil {
gio778577f2024-04-29 09:44:38 +0400572 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400573 }
574 release := Release{
575 Namespace: namespace,
576 RepoAddr: m.repoIO.FullAddress(),
577 AppDir: appDir,
578 }
giof8843412024-05-22 16:38:05 +0400579 rendered, err := app.Render(release, infra, values, nil)
580 if err != nil {
581 return ReleaseResources{}, err
582 }
583 var localCharts map[string]helmv2.HelmChartTemplateSpec
584 if err := m.repoIO.Do(func(rfs soft.RepoFS) (string, error) {
585 charts, err := pullHelmCharts(m.hf, rendered.HelmCharts, rfs, "/helm-charts")
586 if err != nil {
587 return "", err
588 }
589 localCharts = generateLocalCharts(m.lg, charts)
590 return "pull helm charts", nil
591 }); err != nil {
592 return ReleaseResources{}, err
593 }
594 rendered, err = app.Render(release, infra, values, localCharts)
gio3cdee592024-04-17 10:15:56 +0400595 if err != nil {
gio778577f2024-04-29 09:44:38 +0400596 return ReleaseResources{}, err
gio3cdee592024-04-17 10:15:56 +0400597 }
gio0eaf2712024-04-14 13:08:46 +0400598 return installApp(m.repoIO, appDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data)
gioe72b54f2024-04-22 10:44:41 +0400599}
600
giof8843412024-05-22 16:38:05 +0400601// TODO(gio): take app configuration from the repo
602func (m *InfraAppManager) Update(
603 instanceId string,
604 values map[string]any,
605 opts ...InstallOption,
606) (ReleaseResources, error) {
gioe72b54f2024-04-22 10:44:41 +0400607 if err := m.repoIO.Pull(); err != nil {
gio778577f2024-04-29 09:44:38 +0400608 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400609 }
610 env, err := m.Config()
611 if err != nil {
gio778577f2024-04-29 09:44:38 +0400612 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400613 }
614 instanceDir := filepath.Join("/infrastructure", instanceId)
giof8843412024-05-22 16:38:05 +0400615 appCfg, err := GetCueAppData(m.repoIO, instanceDir)
616 if err != nil {
617 return ReleaseResources{}, err
618 }
619 app, err := NewCueInfraApp(appCfg)
620 if err != nil {
621 return ReleaseResources{}, err
622 }
gioe72b54f2024-04-22 10:44:41 +0400623 instanceConfigPath := filepath.Join(instanceDir, "config.json")
624 config, err := m.appConfig(instanceConfigPath)
625 if err != nil {
gio778577f2024-04-29 09:44:38 +0400626 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400627 }
giof8843412024-05-22 16:38:05 +0400628 localCharts, err := extractLocalCharts(m.repoIO, filepath.Join(instanceDir, "rendered.json"))
629 if err != nil {
630 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400631 }
giof8843412024-05-22 16:38:05 +0400632 rendered, err := app.Render(config.Release, env, values, localCharts)
gioe72b54f2024-04-22 10:44:41 +0400633 if err != nil {
gio778577f2024-04-29 09:44:38 +0400634 return ReleaseResources{}, err
gioe72b54f2024-04-22 10:44:41 +0400635 }
gio0eaf2712024-04-14 13:08:46 +0400636 return installApp(m.repoIO, instanceDir, rendered.Name, rendered.Config, rendered.Ports, rendered.Resources, rendered.Data, opts...)
gio3cdee592024-04-17 10:15:56 +0400637}
giof8843412024-05-22 16:38:05 +0400638
639func pullHelmCharts(hf HelmFetcher, charts HelmCharts, rfs soft.RepoFS, root string) (map[string]string, error) {
640 ret := make(map[string]string)
641 for name, chart := range charts.Git {
642 chartRoot := filepath.Join(root, name)
643 ret[name] = chartRoot
644 if err := hf.Pull(chart, rfs, chartRoot); err != nil {
645 return nil, err
646 }
647 }
648 return ret, nil
649}
650
651func generateLocalCharts(g LocalChartGenerator, charts map[string]string) map[string]helmv2.HelmChartTemplateSpec {
652 ret := make(map[string]helmv2.HelmChartTemplateSpec)
653 for name, path := range charts {
654 ret[name] = g.Generate(path)
655 }
656 return ret
657}
658
659func pullContainerImages(appName string, imgs map[string]ContainerImage, registry, namespace string, jc JobCreator) error {
660 for _, img := range imgs {
661 name := fmt.Sprintf("copy-image-%s-%s-%s-%s", appName, img.Repository, img.Name, img.Tag)
662 if err := jc.Create(name, namespace, "giolekva/skopeo:latest", []string{
663 "skopeo",
664 "--insecure-policy",
665 "copy",
666 "--dest-tls-verify=false", // TODO(gio): enable
667 "--multi-arch=all",
668 fmt.Sprintf("docker://%s/%s/%s:%s", img.Registry, img.Repository, img.Name, img.Tag),
669 fmt.Sprintf("docker://%s/%s/%s:%s", registry, img.Repository, img.Name, img.Tag),
670 }); err != nil {
671 return err
672 }
673 }
674 return nil
675}
676
677type renderedInstance struct {
678 LocalCharts map[string]helmv2.HelmChartTemplateSpec `json:"localCharts"`
679}
680
681func extractLocalCharts(fs soft.RepoFS, path string) (map[string]helmv2.HelmChartTemplateSpec, error) {
682 r, err := fs.Reader(path)
683 if err != nil {
684 return nil, err
685 }
686 defer r.Close()
687 var cfg renderedInstance
688 if err := json.NewDecoder(r).Decode(&cfg); err != nil {
689 return nil, err
690 }
691 return cfg.LocalCharts, nil
692}