blob: e438a2679bd9e75d4eac4a77f695be015d243b2c [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
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040017const appDir = "/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
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040033func (m *AppManager) Config() (Config, error) {
gio3af43942024-04-16 08:13:50 +040034 var cfg Config
35 if err := ReadYaml(m.repoIO, configFileName, &cfg); err != nil {
36 return Config{}, err
37 } else {
38 return cfg, nil
39 }
40}
41
42func (m *AppManager) appConfig(path string) (AppConfig, error) {
43 var cfg AppConfig
44 if err := ReadYaml(m.repoIO, path, &cfg); err != nil {
45 return AppConfig{}, err
46 } else {
47 return cfg, nil
48 }
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +040049}
50
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040051func (m *AppManager) FindAllInstances(name string) ([]AppConfig, error) {
gio3af43942024-04-16 08:13:50 +040052 kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
53 if err != nil {
54 return nil, err
55 }
56 ret := make([]AppConfig, 0)
57 for _, app := range kust.Resources {
58 cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
59 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
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040070func (m *AppManager) FindInstance(id string) (AppConfig, error) {
gio3af43942024-04-16 08:13:50 +040071 kust, err := ReadKustomization(m.repoIO, filepath.Join(appDir, "kustomization.yaml"))
72 if err != nil {
73 return AppConfig{}, err
74 }
75 for _, app := range kust.Resources {
76 if app == id {
77 cfg, err := m.appConfig(filepath.Join(appDir, app, "config.yaml"))
78 if err != nil {
79 return AppConfig{}, err
80 }
81 cfg.Id = id
82 return cfg, nil
83 }
84 }
85 return AppConfig{}, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040086}
87
88func (m *AppManager) AppConfig(name string) (AppConfig, error) {
89 configF, err := m.repoIO.Reader(filepath.Join(appDir, name, configFileName))
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +040090 if err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040091 return AppConfig{}, err
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +040092 }
93 defer configF.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040094 var cfg AppConfig
Giorgi Lekveishvili03ee5852023-05-30 13:20:10 +040095 contents, err := ioutil.ReadAll(configF)
96 if err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040097 return AppConfig{}, 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
155func InstallApp(repo RepoIO, nsc NamespaceCreator, app App, appDir string, namespace string, initValues map[string]any, derived Derived) error {
156 if err := nsc.Create(namespace); err != nil {
157 return err
158 }
159 derived.Release = Release{
160 Namespace: namespace,
161 RepoAddr: repo.FullAddress(),
162 AppDir: appDir,
163 }
164 rendered, err := app.Render(derived)
165 if err != nil {
166 return err
167 }
168 if err := openPorts(rendered.Ports); err != nil {
169 return err
170 }
171 return repo.Atomic(func(r RepoFS) (string, error) {
172 if err := createKustomizationChain(r, appDir); err != nil {
173 return "", err
174 }
175 {
176 if err := r.RemoveDir(appDir); err != nil {
177 return "", err
178 }
179 if err := r.CreateDir(appDir); err != nil {
180 return "", err
181 }
182 cfg := AppConfig{
183 AppId: app.Name(),
184 Config: initValues,
185 Derived: derived,
186 }
187 if err := WriteYaml(r, path.Join(appDir, configFileName), cfg); err != nil {
188 return "", err
189 }
190 }
191 {
192 appKust := NewKustomization()
193 for name, contents := range rendered.Resources {
194 appKust.AddResources(name)
195 out, err := r.Writer(path.Join(appDir, name))
196 if err != nil {
197 return "", err
198 }
199 defer out.Close()
200 if _, err := out.Write(contents); err != nil {
201 return "", err
202 }
203 }
204 if err := WriteYaml(r, path.Join(appDir, "kustomization.yaml"), appKust); err != nil {
205 return "", err
206 }
207 }
208 return fmt.Sprintf("install: %s", app.Name()), nil
209 })
210}
211
212func (m *AppManager) Install(app App, appDir string, namespace string, values map[string]any) error {
213 appDir = filepath.Clean(appDir)
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400214 if err := m.repoIO.Pull(); err != nil {
215 return err
216 }
gio3af43942024-04-16 08:13:50 +0400217 globalConfig, err := m.Config()
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400218 if err != nil {
219 return err
220 }
gio3af43942024-04-16 08:13:50 +0400221 derivedValues, err := deriveValues(values, app.Schema(), CreateNetworks(globalConfig))
gioef01fbb2024-04-12 16:52:59 +0400222 if err != nil {
223 return err
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400224 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400225 derived := Derived{
226 Global: globalConfig.Values,
227 Values: derivedValues,
Giorgi Lekveishvili7efe22f2023-05-30 13:01:53 +0400228 }
gio3af43942024-04-16 08:13:50 +0400229 return InstallApp(m.repoIO, m.nsCreator, app, appDir, namespace, values, derived)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400230}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400231
232func (m *AppManager) Update(app App, instanceId string, config map[string]any) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400233 if err := m.repoIO.Pull(); err != nil {
234 return err
235 }
gio3af43942024-04-16 08:13:50 +0400236 globalConfig, err := m.Config()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400237 if err != nil {
238 return err
239 }
240 instanceDir := filepath.Join(appDir, instanceId)
241 instanceConfigPath := filepath.Join(instanceDir, configFileName)
gio3af43942024-04-16 08:13:50 +0400242 appConfig, err := m.appConfig(instanceConfigPath)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400243 if err != nil {
244 return err
245 }
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400246 derivedValues, err := deriveValues(config, app.Schema(), CreateNetworks(globalConfig))
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400247 if err != nil {
248 return err
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400249 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400250 derived := Derived{
251 Global: globalConfig.Values,
252 Release: appConfig.Derived.Release,
253 Values: derivedValues,
254 }
gio3af43942024-04-16 08:13:50 +0400255 return InstallApp(m.repoIO, m.nsCreator, app, instanceDir, appConfig.Derived.Release.Namespace, config, derived)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400256}
257
258func (m *AppManager) Remove(instanceId string) error {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400259 if err := m.repoIO.Pull(); err != nil {
260 return err
261 }
gio3af43942024-04-16 08:13:50 +0400262 return m.repoIO.Atomic(func(r RepoFS) (string, error) {
263 r.RemoveDir(filepath.Join(appDir, instanceId))
264 kustPath := filepath.Join(appDir, "kustomization.yaml")
265 kust, err := ReadKustomization(r, kustPath)
266 if err != nil {
267 return "", err
268 }
269 kust.RemoveResources(instanceId)
270 WriteYaml(r, kustPath, kust)
271 return fmt.Sprintf("uninstall: %s", instanceId), nil
272 })
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400273}
274
Giorgi Lekveishvili67383962024-03-22 19:27:34 +0400275// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400276func CreateNetworks(global Config) []Network {
277 return []Network{
278 {
279 Name: "Public",
280 IngressClass: fmt.Sprintf("%s-ingress-public", global.Values.PCloudEnvName),
281 CertificateIssuer: fmt.Sprintf("%s-public", global.Values.Id),
282 Domain: global.Values.Domain,
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400283 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public/api/allocate", global.Values.PCloudEnvName),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400284 },
285 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400286 Name: "Private",
287 IngressClass: fmt.Sprintf("%s-ingress-private", global.Values.Id),
288 Domain: global.Values.PrivateDomain,
289 AllocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private/api/allocate", global.Values.Id),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400290 },
291 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400292}