blob: 0e41a9027fcb0bb7d6ce3b52ea93b9c754c16023 [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"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040012)
13
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040014const configFileName = "config.yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040015const kustomizationFileName = "kustomization.yaml"
16
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040017type AppManager struct {
gio308105e2024-04-19 13:12:13 +040018 repoIO RepoIO
19 nsCreator NamespaceCreator
20 appDirRoot string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040021}
22
gio308105e2024-04-19 13:12:13 +040023func NewAppManager(repoIO RepoIO, nsCreator NamespaceCreator, appDirRoot string) (*AppManager, error) {
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040024 return &AppManager{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040025 repoIO,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040026 nsCreator,
gio308105e2024-04-19 13:12:13 +040027 appDirRoot,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040028 }, nil
29}
30
gio3cdee592024-04-17 10:15:56 +040031func (m *AppManager) Config() (AppEnvConfig, error) {
32 var cfg AppEnvConfig
gio3af43942024-04-16 08:13:50 +040033 if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040034 return AppEnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040035 } else {
36 return cfg, nil
37 }
38}
39
gio3cdee592024-04-17 10:15:56 +040040func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
41 var cfg AppInstanceConfig
gio308105e2024-04-19 13:12:13 +040042 if err := ReadJson(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040043 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040044 } else {
45 return cfg, nil
46 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040047}
48
gio308105e2024-04-19 13:12:13 +040049func (m *AppManager) FindAllInstances() ([]AppInstanceConfig, error) {
50 kust, err := ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040051 if err != nil {
52 return nil, err
53 }
gio3cdee592024-04-17 10:15:56 +040054 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040055 for _, app := range kust.Resources {
gio308105e2024-04-19 13:12:13 +040056 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
57 if err != nil {
58 return nil, err
59 }
60 cfg.Id = app
61 ret = append(ret, cfg)
62 }
63 return ret, nil
64}
65
66func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
67 kust, err := ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
68 if err != nil {
69 return nil, err
70 }
71 ret := make([]AppInstanceConfig, 0)
72 for _, app := range kust.Resources {
73 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +040074 if err != nil {
75 return nil, err
76 }
77 cfg.Id = app
78 if cfg.AppId == name {
79 ret = append(ret, cfg)
80 }
81 }
82 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040083}
84
gio3cdee592024-04-17 10:15:56 +040085func (m *AppManager) FindInstance(id string) (AppInstanceConfig, error) {
gio308105e2024-04-19 13:12:13 +040086 kust, err := ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040087 if err != nil {
gio3cdee592024-04-17 10:15:56 +040088 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040089 }
90 for _, app := range kust.Resources {
91 if app == id {
gio308105e2024-04-19 13:12:13 +040092 cfg, err := m.appConfig(filepath.Join(m.appDirRoot, app, "config.json"))
gio3af43942024-04-16 08:13:50 +040093 if err != nil {
gio3cdee592024-04-17 10:15:56 +040094 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040095 }
96 cfg.Id = id
97 return cfg, nil
98 }
99 }
gio3cdee592024-04-17 10:15:56 +0400100 return AppInstanceConfig{}, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400101}
102
gio3cdee592024-04-17 10:15:56 +0400103func (m *AppManager) AppConfig(name string) (AppInstanceConfig, error) {
gio3cdee592024-04-17 10:15:56 +0400104 var cfg AppInstanceConfig
gio308105e2024-04-19 13:12:13 +0400105 if err := ReadJson(m.repoIO, filepath.Join(m.appDirRoot, name, "config.json"), &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +0400106 return AppInstanceConfig{}, err
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400107 }
gio308105e2024-04-19 13:12:13 +0400108 return cfg, nil
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +0400109}
110
gio3af43942024-04-16 08:13:50 +0400111type allocatePortReq struct {
112 Protocol string `json:"protocol"`
113 SourcePort int `json:"sourcePort"`
114 TargetService string `json:"targetService"`
115 TargetPort int `json:"targetPort"`
116}
117
118func openPorts(ports []PortForward) error {
119 for _, p := range ports {
120 var buf bytes.Buffer
121 req := allocatePortReq{
122 Protocol: p.Protocol,
123 SourcePort: p.SourcePort,
124 TargetService: p.TargetService,
125 TargetPort: p.TargetPort,
126 }
127 if err := json.NewEncoder(&buf).Encode(req); err != nil {
128 return err
129 }
130 resp, err := http.Post(p.Allocator, "application/json", &buf)
131 if err != nil {
132 return err
133 }
134 if resp.StatusCode != http.StatusOK {
135 return fmt.Errorf("Could not allocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
136 }
137 }
138 return nil
139}
140
141func createKustomizationChain(r RepoFS, path string) error {
142 for p := filepath.Clean(path); p != "/"; {
143 parent, child := filepath.Split(p)
144 kustPath := filepath.Join(parent, "kustomization.yaml")
145 kust, err := ReadKustomization(r, kustPath)
146 if err != nil {
147 if errors.Is(err, fs.ErrNotExist) {
148 k := NewKustomization()
149 kust = &k
150 } else {
151 return err
152 }
153 }
154 kust.AddResources(child)
155 if err := WriteYaml(r, kustPath, kust); err != nil {
156 return err
157 }
158 p = filepath.Clean(parent)
159 }
160 return nil
161}
162
gio3cdee592024-04-17 10:15:56 +0400163// TODO(gio): rename to CommitApp
gio308105e2024-04-19 13:12:13 +0400164func InstallApp(repo RepoIO, appDir string, rendered Rendered, opts ...DoOption) error {
165 // if err := openPorts(rendered.Ports); err != nil {
166 // return err
167 // }
168 return repo.Do(func(r RepoFS) (string, error) {
169 if err := r.RemoveDir(appDir); err != nil {
170 return "", err
171 }
172 resourcesDir := path.Join(appDir, "resources")
173 if err := r.CreateDir(resourcesDir); err != nil {
gio3af43942024-04-16 08:13:50 +0400174 return "", err
175 }
176 {
gio3cdee592024-04-17 10:15:56 +0400177 if err := WriteYaml(r, path.Join(appDir, configFileName), rendered.Config); err != nil {
gio3af43942024-04-16 08:13:50 +0400178 return "", err
179 }
gio308105e2024-04-19 13:12:13 +0400180 if err := WriteJson(r, path.Join(appDir, "config.json"), rendered.Config); err != nil {
181 return "", err
182 }
183 for name, contents := range rendered.Data {
184 if name == "config.json" || name == "kustomization.yaml" || name == "resources" {
185 return "", fmt.Errorf("%s is forbidden", name)
186 }
187 w, err := r.Writer(path.Join(appDir, name))
gio3af43942024-04-16 08:13:50 +0400188 if err != nil {
189 return "", err
190 }
gio308105e2024-04-19 13:12:13 +0400191 defer w.Close()
192 if _, err := w.Write(contents); err != nil {
gio3af43942024-04-16 08:13:50 +0400193 return "", err
194 }
195 }
gio308105e2024-04-19 13:12:13 +0400196 }
197 {
198 if err := createKustomizationChain(r, resourcesDir); err != nil {
199 return "", err
200 }
201 appKust := NewKustomization()
202 for name, contents := range rendered.Resources {
203 appKust.AddResources(name)
204 w, err := r.Writer(path.Join(resourcesDir, name))
205 if err != nil {
206 return "", err
207 }
208 defer w.Close()
209 if _, err := w.Write(contents); err != nil {
210 return "", err
211 }
212 }
213 if err := WriteYaml(r, path.Join(resourcesDir, "kustomization.yaml"), appKust); err != nil {
gio3af43942024-04-16 08:13:50 +0400214 return "", err
215 }
216 }
gio3cdee592024-04-17 10:15:56 +0400217 return fmt.Sprintf("install: %s", rendered.Name), nil
gio308105e2024-04-19 13:12:13 +0400218 }, opts...)
gio3af43942024-04-16 08:13:50 +0400219}
220
gio3cdee592024-04-17 10:15:56 +0400221// TODO(gio): commit instanceId -> appDir mapping as well
222func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any) error {
gio3af43942024-04-16 08:13:50 +0400223 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400224 if err := m.repoIO.Pull(); err != nil {
225 return err
226 }
gio3cdee592024-04-17 10:15:56 +0400227 if err := m.nsCreator.Create(namespace); err != nil {
228 return err
229 }
230 env, err := m.Config()
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400231 if err != nil {
232 return err
233 }
gio3cdee592024-04-17 10:15:56 +0400234 release := Release{
235 AppInstanceId: instanceId,
236 Namespace: namespace,
237 RepoAddr: m.repoIO.FullAddress(),
238 AppDir: appDir,
239 }
240 rendered, err := app.Render(release, env, values)
gioef01fbb2024-04-12 16:52:59 +0400241 if err != nil {
242 return err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400243 }
gio3cdee592024-04-17 10:15:56 +0400244 return InstallApp(m.repoIO, appDir, rendered)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400245}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400246
gio308105e2024-04-19 13:12:13 +0400247func (m *AppManager) Update(app EnvApp, instanceId string, values map[string]any, opts ...DoOption) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400248 if err := m.repoIO.Pull(); err != nil {
249 return err
250 }
gio3cdee592024-04-17 10:15:56 +0400251 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400252 if err != nil {
253 return err
254 }
gio308105e2024-04-19 13:12:13 +0400255 instanceDir := filepath.Join(m.appDirRoot, instanceId)
256 instanceConfigPath := filepath.Join(instanceDir, "config.json")
gio3cdee592024-04-17 10:15:56 +0400257 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400258 if err != nil {
259 return err
260 }
gio3cdee592024-04-17 10:15:56 +0400261 release := Release{
262 AppInstanceId: instanceId,
263 Namespace: config.Release.Namespace,
264 RepoAddr: m.repoIO.FullAddress(),
265 AppDir: instanceDir,
266 }
267 rendered, err := app.Render(release, env, values)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400268 if err != nil {
269 return err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400270 }
gio308105e2024-04-19 13:12:13 +0400271 fmt.Println(rendered)
272 return InstallApp(m.repoIO, instanceDir, rendered, opts...)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400273}
274
275func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400276 if err := m.repoIO.Pull(); err != nil {
277 return err
278 }
gio308105e2024-04-19 13:12:13 +0400279 return m.repoIO.Do(func(r RepoFS) (string, error) {
280 r.RemoveDir(filepath.Join(m.appDirRoot, instanceId))
281 kustPath := filepath.Join(m.appDirRoot, "kustomization.yaml")
gio3af43942024-04-16 08:13:50 +0400282 kust, err := ReadKustomization(r, kustPath)
283 if err != nil {
284 return "", err
285 }
286 kust.RemoveResources(instanceId)
287 WriteYaml(r, kustPath, kust)
288 return fmt.Sprintf("uninstall: %s", instanceId), nil
289 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400290}
291
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400292// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gio3cdee592024-04-17 10:15:56 +0400293func CreateNetworks(env AppEnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400294 return []Network{
295 {
296 Name: "Public",
gio3cdee592024-04-17 10:15:56 +0400297 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
298 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
299 Domain: env.Domain,
300 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400301 },
302 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400303 Name: "Private",
gio3cdee592024-04-17 10:15:56 +0400304 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
305 Domain: env.PrivateDomain,
306 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400307 },
308 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400309}
gio3cdee592024-04-17 10:15:56 +0400310
311// InfraAppmanager
312
313type InfraAppManager struct {
314 repoIO RepoIO
315 nsCreator NamespaceCreator
316}
317
318func NewInfraAppManager(repoIO RepoIO, nsCreator NamespaceCreator) (*InfraAppManager, error) {
319 return &InfraAppManager{
320 repoIO,
321 nsCreator,
322 }, nil
323}
324
325func (m *InfraAppManager) Config() (InfraConfig, error) {
326 var cfg InfraConfig
327 if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
328 return InfraConfig{}, err
329 } else {
330 return cfg, nil
331 }
332}
333
334func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) error {
335 appDir = filepath.Clean(appDir)
336 if err := m.repoIO.Pull(); err != nil {
337 return err
338 }
339 if err := m.nsCreator.Create(namespace); err != nil {
340 return err
341 }
342 infra, err := m.Config()
343 if err != nil {
344 return err
345 }
346 release := Release{
347 Namespace: namespace,
348 RepoAddr: m.repoIO.FullAddress(),
349 AppDir: appDir,
350 }
351 rendered, err := app.Render(release, infra, values)
352 if err != nil {
353 return err
354 }
355 return InstallApp(m.repoIO, appDir, rendered)
356}