blob: ffbdcb042903dad84de36e9295c3f9b73d608152 [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"
10 "path"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040011 "path/filepath"
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040012 "sync"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040013 "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 Lekveishvili724885f2023-11-29 16:18:42 +040026 Addr() string
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040027 Pull() 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 Lekveishvili724885f2023-11-29 16:18:42 +040035 WriteCommitAndPush(path, contents, message string) error
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040036 Reader(path string) (io.ReadCloser, error)
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +040037 Writer(path string) (io.WriteCloser, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040038 CreateDir(path string) error
39 RemoveDir(path string) error
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040040 InstallApp(app App, path string, values map[string]any, derived Derived) error
41 RemoveApp(path string) error
42 FindAllInstances(root string, appId string) ([]AppConfig, error)
43 FindInstance(root string, id string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040044}
45
46type repoIO struct {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040047 repo *soft.Repository
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040048 signer ssh.Signer
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040049 l sync.Locker
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040050}
51
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040052func NewRepoIO(repo *soft.Repository, signer ssh.Signer) RepoIO {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040053 return &repoIO{
54 repo,
55 signer,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040056 &sync.Mutex{},
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040057 }
58}
59
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040060func (r *repoIO) Addr() string {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040061 return r.repo.Addr.Addr
62}
63
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040064func (r *repoIO) Pull() error {
65 r.l.Lock()
66 defer r.l.Unlock()
67 return r.pullWithoutLock()
68}
69
70func (r *repoIO) pullWithoutLock() error {
71 wt, err := r.repo.Worktree()
72 if err != nil {
73 fmt.Printf("EEEER wt: %s\b", err)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040074 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040075 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040076 err = wt.Pull(&git.PullOptions{
77 Auth: auth(r.signer),
78 Force: true,
79 })
80 // TODO(gio): propagate error
81 if err != nil {
82 fmt.Printf("EEEER: %s\b", err)
83 }
84 return nil
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040085}
86
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040087func (r *repoIO) ReadConfig() (Config, error) {
88 configF, err := r.Reader(configFileName)
89 if err != nil {
90 return Config{}, err
91 }
92 defer configF.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040093 var cfg Config
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +040094 if err := ReadYaml(configF, &cfg); err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040095 return Config{}, err
96 } else {
97 return cfg, nil
98 }
99}
100
101func (r *repoIO) ReadAppConfig(path string) (AppConfig, error) {
102 configF, err := r.Reader(path)
103 if err != nil {
104 return AppConfig{}, err
105 }
106 defer configF.Close()
107 var cfg AppConfig
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400108 if err := ReadYaml(configF, &cfg); err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400109 return AppConfig{}, err
110 } else {
111 return cfg, nil
112 }
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400113}
114
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400115func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
116 inp, err := r.Reader(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400117 if err != nil {
118 return nil, err
119 }
120 defer inp.Close()
121 return ReadKustomization(inp)
122}
123
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400124func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
125 wt, err := r.repo.Worktree()
126 if err != nil {
127 return nil, err
128 }
129 return wt.Filesystem.Open(path)
130}
131
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400132func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400133 wt, err := r.repo.Worktree()
134 if err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400135 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400136 }
137 if err := wt.Filesystem.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400138 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400139 }
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400140 return wt.Filesystem.Create(path)
141}
142
143func (r *repoIO) WriteKustomization(path string, kust Kustomization) error {
144 out, err := r.Writer(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400145 if err != nil {
146 return err
147 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400148 return kust.Write(out)
149}
150
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400151func (r *repoIO) WriteYaml(path string, data any) error {
152 out, err := r.Writer(path)
153 if err != nil {
154 return err
155 }
156 serialized, err := yaml.Marshal(data)
157 if err != nil {
158 return err
159 }
160 if _, err := out.Write(serialized); err != nil {
161 return err
162 }
163 return nil
164}
165
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +0400166func (r *repoIO) ReadYaml(path string) (any, error) {
167 inp, err := r.Reader(path)
168 if err != nil {
169 return nil, err
170 }
171 data := make(map[string]any)
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400172 if err := ReadYaml(inp, &data); err != nil {
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +0400173 return nil, err
174 }
175 return data, err
176}
177
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400178func (r *repoIO) WriteCommitAndPush(path, contents, message string) error {
179 w, err := r.Writer(path)
180 if err != nil {
181 return err
182 }
183 defer w.Close()
184 if _, err := io.WriteString(w, contents); err != nil {
185 return err
186 }
187 return r.CommitAndPush(message)
188}
189
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400190func (r *repoIO) CommitAndPush(message string) error {
191 wt, err := r.repo.Worktree()
192 if err != nil {
193 return err
194 }
195 if err := wt.AddGlob("*"); err != nil {
196 return err
197 }
198 if _, err := wt.Commit(message, &git.CommitOptions{
199 Author: &object.Signature{
200 Name: "pcloud-installer",
201 When: time.Now(),
202 },
203 }); err != nil {
204 return err
205 }
206 return r.repo.Push(&git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400207 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400208 Auth: auth(r.signer),
209 })
210}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400211
212func (r *repoIO) CreateDir(path string) error {
213 wt, err := r.repo.Worktree()
214 if err != nil {
215 return err
216 }
217 return wt.Filesystem.MkdirAll(path, fs.ModePerm)
218}
219
220func (r *repoIO) RemoveDir(path string) error {
221 wt, err := r.repo.Worktree()
222 if err != nil {
223 return err
224 }
225 err = util.RemoveAll(wt.Filesystem, path)
226 if err == nil || errors.Is(err, fs.ErrNotExist) {
227 return nil
228 }
229 return err
230}
231
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400232type Release struct {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400233 Namespace string `json:"namespace"`
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400234}
235
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400236type Derived struct {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400237 Release Release `json:"release"`
238 Global Values `json:"global"`
239 Values map[string]any `json:"input"` // TODO(gio): rename to input
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400240}
241
242type AppConfig struct {
243 Id string `json:"id"`
244 AppId string `json:"appId"`
245 Config map[string]any `json:"config"`
246 Derived Derived `json:"derived"`
247}
248
249func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any, derived Derived) error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400250 r.l.Lock()
251 defer r.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400252 if err := r.pullWithoutLock(); err != nil {
253 return err
254 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400255 if !filepath.IsAbs(appRootDir) {
256 return fmt.Errorf("Expected absolute path: %s", appRootDir)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400257 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400258 appRootDir = filepath.Clean(appRootDir)
259 for p := appRootDir; p != "/"; {
260 parent, child := filepath.Split(p)
261 kustPath := filepath.Join(parent, "kustomization.yaml")
262 kust, err := r.ReadKustomization(kustPath)
263 if err != nil {
264 if errors.Is(err, fs.ErrNotExist) {
265 k := NewKustomization()
266 kust = &k
267 } else {
268 return err
269 }
270 }
271 kust.AddResources(child)
272 if err := r.WriteKustomization(kustPath, *kust); err != nil {
273 return err
274 }
275 p = filepath.Clean(parent)
276 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400277 {
278 if err := r.RemoveDir(appRootDir); err != nil {
279 return err
280 }
281 if err := r.CreateDir(appRootDir); err != nil {
282 return err
283 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400284 cfg := AppConfig{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400285 AppId: app.Name(),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400286 Config: values,
287 Derived: derived,
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400288 }
289 if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400290 return err
291 }
292 }
293 {
294 appKust := NewKustomization()
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400295 rendered, err := app.Render(derived)
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400296 if err != nil {
297 return err
298 }
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400299 for name, contents := range rendered.Resources {
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400300 appKust.AddResources(name)
301 out, err := r.Writer(path.Join(appRootDir, name))
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400302 if err != nil {
303 return err
304 }
305 defer out.Close()
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400306 if _, err := out.Write(contents); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400307 return err
308 }
309 }
310 if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
311 return err
312 }
313 }
Giorgi Lekveishvili7c037392024-03-11 14:40:24 +0400314 return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name()))
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400315}
316
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400317func (r *repoIO) RemoveApp(appRootDir string) error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400318 r.l.Lock()
319 defer r.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400320 r.RemoveDir(appRootDir)
321 parent, child := filepath.Split(appRootDir)
322 kustPath := filepath.Join(parent, "kustomization.yaml")
323 kust, err := r.ReadKustomization(kustPath)
324 if err != nil {
325 return err
326 }
327 kust.RemoveResources(child)
328 r.WriteKustomization(kustPath, *kust)
329 return r.CommitAndPush(fmt.Sprintf("uninstall: %s", child))
330}
331
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400332func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
333 if !filepath.IsAbs(root) {
334 return nil, fmt.Errorf("Expected absolute path: %s", root)
335 }
336 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
337 if err != nil {
338 return nil, err
339 }
340 ret := make([]AppConfig, 0)
341 for _, app := range kust.Resources {
342 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
343 if err != nil {
344 return nil, err
345 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400346 cfg.Id = app
347 if cfg.AppId == name {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400348 ret = append(ret, cfg)
349 }
350 }
351 return ret, nil
352}
353
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400354func (r *repoIO) FindInstance(root string, id string) (AppConfig, error) {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400355 if !filepath.IsAbs(root) {
356 return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
357 }
358 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
359 if err != nil {
360 return AppConfig{}, err
361 }
362 for _, app := range kust.Resources {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400363 if app == id {
364 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
365 if err != nil {
366 return AppConfig{}, err
367 }
368 cfg.Id = id
369 return cfg, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400370 }
371 }
372 return AppConfig{}, nil
373}
374
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400375func auth(signer ssh.Signer) *gitssh.PublicKeys {
376 return &gitssh.PublicKeys{
377 Signer: signer,
378 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
379 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
380 // TODO(giolekva): verify server public key
381 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
382 return nil
383 },
384 },
385 }
386}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400387
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400388func ReadYaml[T any](r io.Reader, o *T) error {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400389 if contents, err := ioutil.ReadAll(r); err != nil {
390 return err
391 } else {
392 return yaml.UnmarshalStrict(contents, o)
393 }
394}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400395
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400396func deriveValues(values any, schema Schema, networks []Network) (map[string]any, error) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400397 ret := make(map[string]any)
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400398 for k, def := range schema.Fields() {
399 // TODO(gio): validate that it is map
400 v, ok := values.(map[string]any)[k]
401 // TODO(gio): if missing use default value
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400402 if !ok {
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400403 if def.Kind() == KindSSHKey {
404 key, err := NewECDSASSHKeyPair("tmp")
405 if err != nil {
406 return nil, err
407 }
408 ret[k] = map[string]string{
409 "public": string(key.RawAuthorizedKey()),
410 "private": string(key.RawPrivateKey()),
411 }
412 }
413 continue
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400414 }
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400415 switch def.Kind() {
416 case KindBoolean:
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400417 ret[k] = v
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400418 case KindString:
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400419 ret[k] = v
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400420 case KindNetwork:
421 n, err := findNetwork(networks, v.(string)) // TODO(giolekva): validate
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400422 if err != nil {
423 return nil, err
424 }
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400425 ret[k] = n
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400426 case KindAuth:
427 r, err := deriveValues(v, AuthSchema, networks)
428 if err != nil {
429 return nil, err
430 }
431 ret[k] = r
Giorgi Lekveishvilib6a58062024-04-02 16:49:19 +0400432 case KindSSHKey:
433 r, err := deriveValues(v, SSHKeySchema, networks)
434 if err != nil {
435 return nil, err
436 }
437 ret[k] = r
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400438 case KindStruct:
439 r, err := deriveValues(v, def, networks)
440 if err != nil {
441 return nil, err
442 }
443 ret[k] = r
444 default:
445 return nil, fmt.Errorf("Should not reach!")
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400446 }
447 }
448 return ret, nil
449}
450
451func findNetwork(networks []Network, name string) (Network, error) {
452 for _, n := range networks {
453 if n.Name == name {
454 return n, nil
455 }
456 }
457 return Network{}, fmt.Errorf("Network not found: %s", name)
458}
459
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400460type Network struct {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400461 Name string `json:"name,omitempty"`
462 IngressClass string `json:"ingressClass,omitempty"`
463 CertificateIssuer string `json:"certificateIssuer,omitempty"`
464 Domain string `json:"domain,omitempty"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400465}