blob: 54d9a7de87d1d6c364c0eb142daa91e6e32234a9 [file] [log] [blame]
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +04001package installer
2
3import (
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +04004 "errors"
5 "fmt"
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +04006 "io"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +04007 "io/fs"
Giorgi Lekveishvili76951482023-06-30 23:25:09 +04008 "io/ioutil"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +04009 "net"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040010 "net/netip"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040011 "path"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040012 "path/filepath"
13 "time"
14
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040015 "github.com/go-git/go-billy/v5/util"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040016 "github.com/go-git/go-git/v5"
17 "github.com/go-git/go-git/v5/plumbing/object"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040018 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040019 "golang.org/x/crypto/ssh"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040020 "sigs.k8s.io/yaml"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040021
22 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040023)
24
25type RepoIO interface {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040026 Addr() netip.AddrPort
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040027 Fetch() error
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040028 ReadConfig() (Config, error)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040029 ReadAppConfig(path string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040030 ReadKustomization(path string) (*Kustomization, error)
31 WriteKustomization(path string, kust Kustomization) error
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +040032 ReadYaml(path string) (any, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040033 WriteYaml(path string, data any) error
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040034 CommitAndPush(message string) error
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040035 Reader(path string) (io.ReadCloser, error)
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +040036 Writer(path string) (io.WriteCloser, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040037 CreateDir(path string) error
38 RemoveDir(path string) error
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040039 InstallApp(app App, path string, values map[string]any, derived Derived) error
40 RemoveApp(path string) error
41 FindAllInstances(root string, appId string) ([]AppConfig, error)
42 FindInstance(root string, id string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040043}
44
45type repoIO struct {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040046 repo *soft.Repository
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040047 signer ssh.Signer
48}
49
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040050func NewRepoIO(repo *soft.Repository, signer ssh.Signer) RepoIO {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040051 return &repoIO{
52 repo,
53 signer,
54 }
55}
56
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040057func (r *repoIO) Addr() netip.AddrPort {
58 return r.repo.Addr.Addr
59}
60
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040061func (r *repoIO) Fetch() error {
62 err := r.repo.Fetch(&git.FetchOptions{
63 RemoteName: "origin",
64 Auth: auth(r.signer),
65 Force: true,
66 })
67 if err == nil || err == git.NoErrAlreadyUpToDate {
68 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040069 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040070 return err
71}
72
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040073func (r *repoIO) ReadConfig() (Config, error) {
74 configF, err := r.Reader(configFileName)
75 if err != nil {
76 return Config{}, err
77 }
78 defer configF.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040079 var cfg Config
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +040080 if err := ReadYaml(configF, &cfg); err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040081 return Config{}, err
82 } else {
83 return cfg, nil
84 }
85}
86
87func (r *repoIO) ReadAppConfig(path string) (AppConfig, error) {
88 configF, err := r.Reader(path)
89 if err != nil {
90 return AppConfig{}, err
91 }
92 defer configF.Close()
93 var cfg AppConfig
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +040094 if err := ReadYaml(configF, &cfg); err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040095 return AppConfig{}, err
96 } else {
97 return cfg, nil
98 }
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040099}
100
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400101func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
102 inp, err := r.Reader(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400103 if err != nil {
104 return nil, err
105 }
106 defer inp.Close()
107 return ReadKustomization(inp)
108}
109
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400110func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
111 wt, err := r.repo.Worktree()
112 if err != nil {
113 return nil, err
114 }
115 return wt.Filesystem.Open(path)
116}
117
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400118func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400119 wt, err := r.repo.Worktree()
120 if err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400121 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400122 }
123 if err := wt.Filesystem.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400124 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400125 }
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400126 return wt.Filesystem.Create(path)
127}
128
129func (r *repoIO) WriteKustomization(path string, kust Kustomization) error {
130 out, err := r.Writer(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400131 if err != nil {
132 return err
133 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400134 return kust.Write(out)
135}
136
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400137func (r *repoIO) WriteYaml(path string, data any) error {
138 out, err := r.Writer(path)
139 if err != nil {
140 return err
141 }
142 serialized, err := yaml.Marshal(data)
143 if err != nil {
144 return err
145 }
146 if _, err := out.Write(serialized); err != nil {
147 return err
148 }
149 return nil
150}
151
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +0400152func (r *repoIO) ReadYaml(path string) (any, error) {
153 inp, err := r.Reader(path)
154 if err != nil {
155 return nil, err
156 }
157 data := make(map[string]any)
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400158 if err := ReadYaml(inp, &data); err != nil {
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +0400159 return nil, err
160 }
161 return data, err
162}
163
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400164func (r *repoIO) CommitAndPush(message string) error {
165 wt, err := r.repo.Worktree()
166 if err != nil {
167 return err
168 }
169 if err := wt.AddGlob("*"); err != nil {
170 return err
171 }
172 if _, err := wt.Commit(message, &git.CommitOptions{
173 Author: &object.Signature{
174 Name: "pcloud-installer",
175 When: time.Now(),
176 },
177 }); err != nil {
178 return err
179 }
180 return r.repo.Push(&git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400181 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400182 Auth: auth(r.signer),
183 })
184}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400185
186func (r *repoIO) CreateDir(path string) error {
187 wt, err := r.repo.Worktree()
188 if err != nil {
189 return err
190 }
191 return wt.Filesystem.MkdirAll(path, fs.ModePerm)
192}
193
194func (r *repoIO) RemoveDir(path string) error {
195 wt, err := r.repo.Worktree()
196 if err != nil {
197 return err
198 }
199 err = util.RemoveAll(wt.Filesystem, path)
200 if err == nil || errors.Is(err, fs.ErrNotExist) {
201 return nil
202 }
203 return err
204}
205
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400206type Release struct {
207 Namespace string `json:"Namespace"`
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400208}
209
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400210type Derived struct {
211 Release Release `json:"Release"`
212 Global Values `json:"Global"`
213 Values map[string]any `json:"Values"`
214}
215
216type AppConfig struct {
217 Id string `json:"id"`
218 AppId string `json:"appId"`
219 Config map[string]any `json:"config"`
220 Derived Derived `json:"derived"`
221}
222
223func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any, derived Derived) error {
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400224 if !filepath.IsAbs(appRootDir) {
225 return fmt.Errorf("Expected absolute path: %s", appRootDir)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400226 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400227 appRootDir = filepath.Clean(appRootDir)
228 for p := appRootDir; p != "/"; {
229 parent, child := filepath.Split(p)
230 kustPath := filepath.Join(parent, "kustomization.yaml")
231 kust, err := r.ReadKustomization(kustPath)
232 if err != nil {
233 if errors.Is(err, fs.ErrNotExist) {
234 k := NewKustomization()
235 kust = &k
236 } else {
237 return err
238 }
239 }
240 kust.AddResources(child)
241 if err := r.WriteKustomization(kustPath, *kust); err != nil {
242 return err
243 }
244 p = filepath.Clean(parent)
245 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400246 {
247 if err := r.RemoveDir(appRootDir); err != nil {
248 return err
249 }
250 if err := r.CreateDir(appRootDir); err != nil {
251 return err
252 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400253 cfg := AppConfig{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400254 AppId: app.Name,
255 Config: values,
256 Derived: derived,
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400257 }
258 if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400259 return err
260 }
261 }
262 {
263 appKust := NewKustomization()
264 for _, t := range app.Templates {
265 appKust.AddResources(t.Name())
266 out, err := r.Writer(path.Join(appRootDir, t.Name()))
267 if err != nil {
268 return err
269 }
270 defer out.Close()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400271 if err := t.Execute(out, derived); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400272 return err
273 }
274 }
275 if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
276 return err
277 }
278 }
279 return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
280}
281
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400282func (r *repoIO) RemoveApp(appRootDir string) error {
283 r.RemoveDir(appRootDir)
284 parent, child := filepath.Split(appRootDir)
285 kustPath := filepath.Join(parent, "kustomization.yaml")
286 kust, err := r.ReadKustomization(kustPath)
287 if err != nil {
288 return err
289 }
290 kust.RemoveResources(child)
291 r.WriteKustomization(kustPath, *kust)
292 return r.CommitAndPush(fmt.Sprintf("uninstall: %s", child))
293}
294
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400295func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
296 if !filepath.IsAbs(root) {
297 return nil, fmt.Errorf("Expected absolute path: %s", root)
298 }
299 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
300 if err != nil {
301 return nil, err
302 }
303 ret := make([]AppConfig, 0)
304 for _, app := range kust.Resources {
305 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
306 if err != nil {
307 return nil, err
308 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400309 cfg.Id = app
310 if cfg.AppId == name {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400311 ret = append(ret, cfg)
312 }
313 }
314 return ret, nil
315}
316
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400317func (r *repoIO) FindInstance(root string, id string) (AppConfig, error) {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400318 if !filepath.IsAbs(root) {
319 return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
320 }
321 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
322 if err != nil {
323 return AppConfig{}, err
324 }
325 for _, app := range kust.Resources {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400326 if app == id {
327 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
328 if err != nil {
329 return AppConfig{}, err
330 }
331 cfg.Id = id
332 return cfg, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400333 }
334 }
335 return AppConfig{}, nil
336}
337
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400338func auth(signer ssh.Signer) *gitssh.PublicKeys {
339 return &gitssh.PublicKeys{
340 Signer: signer,
341 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
342 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
343 // TODO(giolekva): verify server public key
344 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
345 return nil
346 },
347 },
348 }
349}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400350
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400351func ReadYaml[T any](r io.Reader, o *T) error {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400352 if contents, err := ioutil.ReadAll(r); err != nil {
353 return err
354 } else {
355 return yaml.UnmarshalStrict(contents, o)
356 }
357}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400358
359func deriveValues(values any, schema map[string]any, networks []Network) (map[string]any, error) {
360 ret := make(map[string]any)
361 for k, v := range values.(map[string]any) { // TODO(giolekva): validate
362 def, err := fieldSchema(schema, k)
363 if err != nil {
364 return nil, err
365 }
366 t, ok := def["type"]
367 if !ok {
368 return nil, fmt.Errorf("Found field with undefined type: %s", k)
369 }
370 if t == "string" {
371 role, ok := def["role"]
372 if ok && role == "network" {
373 n, err := findNetwork(networks, v.(string)) // TODO(giolekva): validate
374 if err != nil {
375 return nil, err
376 }
377 ret[k] = n
378 } else {
379 ret[k] = v
380 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400381 } else if t == "boolean" {
382 ret[k] = v
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400383 } else {
384 ret[k], err = deriveValues(v, def, networks)
385 if err != nil {
386 return nil, err
387 }
388 }
389 }
390 return ret, nil
391}
392
393func findNetwork(networks []Network, name string) (Network, error) {
394 for _, n := range networks {
395 if n.Name == name {
396 return n, nil
397 }
398 }
399 return Network{}, fmt.Errorf("Network not found: %s", name)
400}
401
402func fieldSchema(schema map[string]any, key string) (map[string]any, error) {
403 properties, ok := schema["properties"]
404 if !ok {
405 return nil, fmt.Errorf("Properties not found")
406 }
407 propMap, ok := properties.(map[string]any)
408 if !ok {
409 return nil, fmt.Errorf("Expected properties to be map")
410 }
411 def, ok := propMap[key]
412 if !ok {
413 return nil, fmt.Errorf("Unknown field: %s", key)
414 }
415 ret, ok := def.(map[string]any)
416 if !ok {
417 return nil, fmt.Errorf("Invalid schema")
418 }
419 return ret, nil
420}
421
422type Network struct {
423 Name string
424 IngressClass string
425 CertificateIssuer string
426 Domain string
427}