blob: 7e89eec212e3e1167adbef97dc9609ab838af7ed [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"
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +04009 "io/ioutil"
gio3af43942024-04-16 08:13:50 +040010 "net/http"
11 "path"
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +040012 "path/filepath"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040013
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040014 "sigs.k8s.io/yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040015)
16
gio3cdee592024-04-17 10:15:56 +040017const appDirRoot = "/apps"
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040018const configFileName = "config.yaml"
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040019const kustomizationFileName = "kustomization.yaml"
20
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040021type AppManager struct {
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040022 repoIO RepoIO
23 nsCreator NamespaceCreator
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040024}
25
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040026func NewAppManager(repoIO RepoIO, nsCreator NamespaceCreator) (*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,
Giorgi Lekveishvilibd6be7f2023-05-26 15:51:28 +040030 }, nil
31}
32
gio3cdee592024-04-17 10:15:56 +040033func (m *AppManager) Config() (AppEnvConfig, error) {
34 var cfg AppEnvConfig
gio3af43942024-04-16 08:13:50 +040035 if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040036 return AppEnvConfig{}, err
gio3af43942024-04-16 08:13:50 +040037 } else {
38 return cfg, nil
39 }
40}
41
gio3cdee592024-04-17 10:15:56 +040042func (m *AppManager) appConfig(path string) (AppInstanceConfig, error) {
43 var cfg AppInstanceConfig
gio3af43942024-04-16 08:13:50 +040044 if err := ReadYaml(m.repoIO, path, &cfg); err != nil {
gio3cdee592024-04-17 10:15:56 +040045 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040046 } else {
47 return cfg, nil
48 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040049}
50
gio3cdee592024-04-17 10:15:56 +040051func (m *AppManager) FindAllInstances(name string) ([]AppInstanceConfig, error) {
52 kust, err := ReadKustomization(m.repoIO, filepath.Join(appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040053 if err != nil {
54 return nil, err
55 }
gio3cdee592024-04-17 10:15:56 +040056 ret := make([]AppInstanceConfig, 0)
gio3af43942024-04-16 08:13:50 +040057 for _, app := range kust.Resources {
gio3cdee592024-04-17 10:15:56 +040058 cfg, err := m.appConfig(filepath.Join(appDirRoot, app, "config.yaml"))
gio3af43942024-04-16 08:13:50 +040059 if err != nil {
60 return nil, err
61 }
62 cfg.Id = app
63 if cfg.AppId == name {
64 ret = append(ret, cfg)
65 }
66 }
67 return ret, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040068}
69
gio3cdee592024-04-17 10:15:56 +040070func (m *AppManager) FindInstance(id string) (AppInstanceConfig, error) {
71 kust, err := ReadKustomization(m.repoIO, filepath.Join(appDirRoot, "kustomization.yaml"))
gio3af43942024-04-16 08:13:50 +040072 if err != nil {
gio3cdee592024-04-17 10:15:56 +040073 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040074 }
75 for _, app := range kust.Resources {
76 if app == id {
gio3cdee592024-04-17 10:15:56 +040077 cfg, err := m.appConfig(filepath.Join(appDirRoot, app, "config.yaml"))
gio3af43942024-04-16 08:13:50 +040078 if err != nil {
gio3cdee592024-04-17 10:15:56 +040079 return AppInstanceConfig{}, err
gio3af43942024-04-16 08:13:50 +040080 }
81 cfg.Id = id
82 return cfg, nil
83 }
84 }
gio3cdee592024-04-17 10:15:56 +040085 return AppInstanceConfig{}, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040086}
87
gio3cdee592024-04-17 10:15:56 +040088func (m *AppManager) AppConfig(name string) (AppInstanceConfig, error) {
89 configF, err := m.repoIO.Reader(filepath.Join(appDirRoot, name, configFileName))
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +040090 if err != nil {
gio3cdee592024-04-17 10:15:56 +040091 return AppInstanceConfig{}, err
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +040092 }
93 defer configF.Close()
gio3cdee592024-04-17 10:15:56 +040094 var cfg AppInstanceConfig
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +040095 contents, err := ioutil.ReadAll(configF)
96 if err != nil {
gio3cdee592024-04-17 10:15:56 +040097 return AppInstanceConfig{}, err
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +040098 }
99 err = yaml.UnmarshalStrict(contents, &cfg)
100 return cfg, err
101}
102
gio3af43942024-04-16 08:13:50 +0400103type allocatePortReq struct {
104 Protocol string `json:"protocol"`
105 SourcePort int `json:"sourcePort"`
106 TargetService string `json:"targetService"`
107 TargetPort int `json:"targetPort"`
108}
109
110func openPorts(ports []PortForward) error {
111 for _, p := range ports {
112 var buf bytes.Buffer
113 req := allocatePortReq{
114 Protocol: p.Protocol,
115 SourcePort: p.SourcePort,
116 TargetService: p.TargetService,
117 TargetPort: p.TargetPort,
118 }
119 if err := json.NewEncoder(&buf).Encode(req); err != nil {
120 return err
121 }
122 resp, err := http.Post(p.Allocator, "application/json", &buf)
123 if err != nil {
124 return err
125 }
126 if resp.StatusCode != http.StatusOK {
127 return fmt.Errorf("Could not allocate port %d, status code: %d", p.SourcePort, resp.StatusCode)
128 }
129 }
130 return nil
131}
132
133func createKustomizationChain(r RepoFS, path string) error {
134 for p := filepath.Clean(path); p != "/"; {
135 parent, child := filepath.Split(p)
136 kustPath := filepath.Join(parent, "kustomization.yaml")
137 kust, err := ReadKustomization(r, kustPath)
138 if err != nil {
139 if errors.Is(err, fs.ErrNotExist) {
140 k := NewKustomization()
141 kust = &k
142 } else {
143 return err
144 }
145 }
146 kust.AddResources(child)
147 if err := WriteYaml(r, kustPath, kust); err != nil {
148 return err
149 }
150 p = filepath.Clean(parent)
151 }
152 return nil
153}
154
gio3cdee592024-04-17 10:15:56 +0400155// TODO(gio): rename to CommitApp
156func InstallApp(repo RepoIO, appDir string, rendered Rendered) error {
gio3af43942024-04-16 08:13:50 +0400157 if err := openPorts(rendered.Ports); err != nil {
158 return err
159 }
160 return repo.Atomic(func(r RepoFS) (string, error) {
161 if err := createKustomizationChain(r, appDir); err != nil {
162 return "", err
163 }
164 {
165 if err := r.RemoveDir(appDir); err != nil {
166 return "", err
167 }
168 if err := r.CreateDir(appDir); err != nil {
169 return "", err
170 }
gio3cdee592024-04-17 10:15:56 +0400171 if err := WriteYaml(r, path.Join(appDir, configFileName), rendered.Config); err != nil {
gio3af43942024-04-16 08:13:50 +0400172 return "", err
173 }
174 }
175 {
176 appKust := NewKustomization()
177 for name, contents := range rendered.Resources {
178 appKust.AddResources(name)
179 out, err := r.Writer(path.Join(appDir, name))
180 if err != nil {
181 return "", err
182 }
183 defer out.Close()
184 if _, err := out.Write(contents); err != nil {
185 return "", err
186 }
187 }
188 if err := WriteYaml(r, path.Join(appDir, "kustomization.yaml"), appKust); err != nil {
189 return "", err
190 }
191 }
gio3cdee592024-04-17 10:15:56 +0400192 return fmt.Sprintf("install: %s", rendered.Name), nil
gio3af43942024-04-16 08:13:50 +0400193 })
194}
195
gio3cdee592024-04-17 10:15:56 +0400196// TODO(gio): commit instanceId -> appDir mapping as well
197func (m *AppManager) Install(app EnvApp, instanceId string, appDir string, namespace string, values map[string]any) error {
gio3af43942024-04-16 08:13:50 +0400198 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400199 if err := m.repoIO.Pull(); err != nil {
200 return err
201 }
gio3cdee592024-04-17 10:15:56 +0400202 if err := m.nsCreator.Create(namespace); err != nil {
203 return err
204 }
205 env, err := m.Config()
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400206 if err != nil {
207 return err
208 }
gio3cdee592024-04-17 10:15:56 +0400209 release := Release{
210 AppInstanceId: instanceId,
211 Namespace: namespace,
212 RepoAddr: m.repoIO.FullAddress(),
213 AppDir: appDir,
214 }
215 rendered, err := app.Render(release, env, values)
gioef01fbb2024-04-12 16:52:59 +0400216 if err != nil {
217 return err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400218 }
gio3cdee592024-04-17 10:15:56 +0400219 return InstallApp(m.repoIO, appDir, rendered)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400220}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400221
gio3cdee592024-04-17 10:15:56 +0400222func (m *AppManager) Update(app EnvApp, instanceId string, values map[string]any) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400223 if err := m.repoIO.Pull(); err != nil {
224 return err
225 }
gio3cdee592024-04-17 10:15:56 +0400226 env, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400227 if err != nil {
228 return err
229 }
gio3cdee592024-04-17 10:15:56 +0400230 instanceDir := filepath.Join(appDirRoot, instanceId)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400231 instanceConfigPath := filepath.Join(instanceDir, configFileName)
gio3cdee592024-04-17 10:15:56 +0400232 config, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400233 if err != nil {
234 return err
235 }
gio3cdee592024-04-17 10:15:56 +0400236 release := Release{
237 AppInstanceId: instanceId,
238 Namespace: config.Release.Namespace,
239 RepoAddr: m.repoIO.FullAddress(),
240 AppDir: instanceDir,
241 }
242 rendered, err := app.Render(release, env, values)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400243 if err != nil {
244 return err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400245 }
gio3cdee592024-04-17 10:15:56 +0400246 return InstallApp(m.repoIO, instanceDir, rendered)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400247}
248
249func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400250 if err := m.repoIO.Pull(); err != nil {
251 return err
252 }
gio3af43942024-04-16 08:13:50 +0400253 return m.repoIO.Atomic(func(r RepoFS) (string, error) {
gio3cdee592024-04-17 10:15:56 +0400254 r.RemoveDir(filepath.Join(appDirRoot, instanceId))
255 kustPath := filepath.Join(appDirRoot, "kustomization.yaml")
gio3af43942024-04-16 08:13:50 +0400256 kust, err := ReadKustomization(r, kustPath)
257 if err != nil {
258 return "", err
259 }
260 kust.RemoveResources(instanceId)
261 WriteYaml(r, kustPath, kust)
262 return fmt.Sprintf("uninstall: %s", instanceId), nil
263 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400264}
265
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400266// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
gio3cdee592024-04-17 10:15:56 +0400267func CreateNetworks(env AppEnvConfig) []Network {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400268 return []Network{
269 {
270 Name: "Public",
gio3cdee592024-04-17 10:15:56 +0400271 IngressClass: fmt.Sprintf("%s-ingress-public", env.InfraName),
272 CertificateIssuer: fmt.Sprintf("%s-public", env.Id),
273 Domain: env.Domain,
274 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400275 },
276 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400277 Name: "Private",
gio3cdee592024-04-17 10:15:56 +0400278 IngressClass: fmt.Sprintf("%s-ingress-private", env.Id),
279 Domain: env.PrivateDomain,
280 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400281 },
282 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400283}
gio3cdee592024-04-17 10:15:56 +0400284
285// InfraAppmanager
286
287type InfraAppManager struct {
288 repoIO RepoIO
289 nsCreator NamespaceCreator
290}
291
292func NewInfraAppManager(repoIO RepoIO, nsCreator NamespaceCreator) (*InfraAppManager, error) {
293 return &InfraAppManager{
294 repoIO,
295 nsCreator,
296 }, nil
297}
298
299func (m *InfraAppManager) Config() (InfraConfig, error) {
300 var cfg InfraConfig
301 if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
302 return InfraConfig{}, err
303 } else {
304 return cfg, nil
305 }
306}
307
308func (m *InfraAppManager) Install(app InfraApp, appDir string, namespace string, values map[string]any) error {
309 appDir = filepath.Clean(appDir)
310 if err := m.repoIO.Pull(); err != nil {
311 return err
312 }
313 if err := m.nsCreator.Create(namespace); err != nil {
314 return err
315 }
316 infra, err := m.Config()
317 if err != nil {
318 return err
319 }
320 release := Release{
321 Namespace: namespace,
322 RepoAddr: m.repoIO.FullAddress(),
323 AppDir: appDir,
324 }
325 rendered, err := app.Render(release, infra, values)
326 if err != nil {
327 return err
328 }
329 return InstallApp(m.repoIO, appDir, rendered)
330}