blob: dbbeda9adc8524e814e1d2b905d214dea1a648f1 [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 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 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 Lekveishvili0ccd1482023-06-21 15:02:24 +040064func (r *repoIO) Fetch() error {
65 err := r.repo.Fetch(&git.FetchOptions{
66 RemoteName: "origin",
67 Auth: auth(r.signer),
68 Force: true,
69 })
70 if err == nil || err == git.NoErrAlreadyUpToDate {
71 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040072 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040073 return err
74}
75
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040076func (r *repoIO) ReadConfig() (Config, error) {
77 configF, err := r.Reader(configFileName)
78 if err != nil {
79 return Config{}, err
80 }
81 defer configF.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040082 var cfg Config
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +040083 if err := ReadYaml(configF, &cfg); err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040084 return Config{}, err
85 } else {
86 return cfg, nil
87 }
88}
89
90func (r *repoIO) ReadAppConfig(path string) (AppConfig, error) {
91 configF, err := r.Reader(path)
92 if err != nil {
93 return AppConfig{}, err
94 }
95 defer configF.Close()
96 var cfg AppConfig
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +040097 if err := ReadYaml(configF, &cfg); err != nil {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040098 return AppConfig{}, err
99 } else {
100 return cfg, nil
101 }
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +0400102}
103
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400104func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
105 inp, err := r.Reader(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400106 if err != nil {
107 return nil, err
108 }
109 defer inp.Close()
110 return ReadKustomization(inp)
111}
112
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400113func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
114 wt, err := r.repo.Worktree()
115 if err != nil {
116 return nil, err
117 }
118 return wt.Filesystem.Open(path)
119}
120
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400121func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400122 wt, err := r.repo.Worktree()
123 if err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400124 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400125 }
126 if err := wt.Filesystem.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400127 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400128 }
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400129 return wt.Filesystem.Create(path)
130}
131
132func (r *repoIO) WriteKustomization(path string, kust Kustomization) error {
133 out, err := r.Writer(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400134 if err != nil {
135 return err
136 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400137 return kust.Write(out)
138}
139
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400140func (r *repoIO) WriteYaml(path string, data any) error {
141 out, err := r.Writer(path)
142 if err != nil {
143 return err
144 }
145 serialized, err := yaml.Marshal(data)
146 if err != nil {
147 return err
148 }
149 if _, err := out.Write(serialized); err != nil {
150 return err
151 }
152 return nil
153}
154
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +0400155func (r *repoIO) ReadYaml(path string) (any, error) {
156 inp, err := r.Reader(path)
157 if err != nil {
158 return nil, err
159 }
160 data := make(map[string]any)
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400161 if err := ReadYaml(inp, &data); err != nil {
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +0400162 return nil, err
163 }
164 return data, err
165}
166
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400167func (r *repoIO) WriteCommitAndPush(path, contents, message string) error {
168 w, err := r.Writer(path)
169 if err != nil {
170 return err
171 }
172 defer w.Close()
173 if _, err := io.WriteString(w, contents); err != nil {
174 return err
175 }
176 return r.CommitAndPush(message)
177}
178
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400179func (r *repoIO) CommitAndPush(message string) error {
180 wt, err := r.repo.Worktree()
181 if err != nil {
182 return err
183 }
184 if err := wt.AddGlob("*"); err != nil {
185 return err
186 }
187 if _, err := wt.Commit(message, &git.CommitOptions{
188 Author: &object.Signature{
189 Name: "pcloud-installer",
190 When: time.Now(),
191 },
192 }); err != nil {
193 return err
194 }
195 return r.repo.Push(&git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400196 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400197 Auth: auth(r.signer),
198 })
199}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400200
201func (r *repoIO) CreateDir(path string) error {
202 wt, err := r.repo.Worktree()
203 if err != nil {
204 return err
205 }
206 return wt.Filesystem.MkdirAll(path, fs.ModePerm)
207}
208
209func (r *repoIO) RemoveDir(path string) error {
210 wt, err := r.repo.Worktree()
211 if err != nil {
212 return err
213 }
214 err = util.RemoveAll(wt.Filesystem, path)
215 if err == nil || errors.Is(err, fs.ErrNotExist) {
216 return nil
217 }
218 return err
219}
220
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400221type Release struct {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400222 Namespace string `json:"namespace"`
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400223}
224
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400225type Derived struct {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400226 Release Release `json:"release"`
227 Global Values `json:"global"`
228 Values map[string]any `json:"input"` // TODO(gio): rename to input
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400229}
230
231type AppConfig struct {
232 Id string `json:"id"`
233 AppId string `json:"appId"`
234 Config map[string]any `json:"config"`
235 Derived Derived `json:"derived"`
236}
237
238func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any, derived Derived) error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400239 r.l.Lock()
240 defer r.l.Unlock()
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400241 if !filepath.IsAbs(appRootDir) {
242 return fmt.Errorf("Expected absolute path: %s", appRootDir)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400243 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400244 appRootDir = filepath.Clean(appRootDir)
245 for p := appRootDir; p != "/"; {
246 parent, child := filepath.Split(p)
247 kustPath := filepath.Join(parent, "kustomization.yaml")
248 kust, err := r.ReadKustomization(kustPath)
249 if err != nil {
250 if errors.Is(err, fs.ErrNotExist) {
251 k := NewKustomization()
252 kust = &k
253 } else {
254 return err
255 }
256 }
257 kust.AddResources(child)
258 if err := r.WriteKustomization(kustPath, *kust); err != nil {
259 return err
260 }
261 p = filepath.Clean(parent)
262 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400263 {
264 if err := r.RemoveDir(appRootDir); err != nil {
265 return err
266 }
267 if err := r.CreateDir(appRootDir); err != nil {
268 return err
269 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400270 cfg := AppConfig{
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400271 AppId: app.Name(),
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400272 Config: values,
273 Derived: derived,
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400274 }
275 if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400276 return err
277 }
278 }
279 {
280 appKust := NewKustomization()
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400281 rendered, err := app.Render(derived)
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400282 if err != nil {
283 return err
284 }
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400285 for name, contents := range rendered.Resources {
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400286 appKust.AddResources(name)
287 out, err := r.Writer(path.Join(appRootDir, name))
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400288 if err != nil {
289 return err
290 }
291 defer out.Close()
Giorgi Lekveishvili3f697b12024-01-04 00:56:06 +0400292 if _, err := out.Write(contents); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400293 return err
294 }
295 }
296 if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
297 return err
298 }
299 }
Giorgi Lekveishvili7c037392024-03-11 14:40:24 +0400300 return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name()))
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400301}
302
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400303func (r *repoIO) RemoveApp(appRootDir string) error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400304 r.l.Lock()
305 defer r.l.Unlock()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400306 r.RemoveDir(appRootDir)
307 parent, child := filepath.Split(appRootDir)
308 kustPath := filepath.Join(parent, "kustomization.yaml")
309 kust, err := r.ReadKustomization(kustPath)
310 if err != nil {
311 return err
312 }
313 kust.RemoveResources(child)
314 r.WriteKustomization(kustPath, *kust)
315 return r.CommitAndPush(fmt.Sprintf("uninstall: %s", child))
316}
317
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400318func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
319 if !filepath.IsAbs(root) {
320 return nil, fmt.Errorf("Expected absolute path: %s", root)
321 }
322 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
323 if err != nil {
324 return nil, err
325 }
326 ret := make([]AppConfig, 0)
327 for _, app := range kust.Resources {
328 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
329 if err != nil {
330 return nil, err
331 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400332 cfg.Id = app
333 if cfg.AppId == name {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400334 ret = append(ret, cfg)
335 }
336 }
337 return ret, nil
338}
339
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400340func (r *repoIO) FindInstance(root string, id string) (AppConfig, error) {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400341 if !filepath.IsAbs(root) {
342 return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
343 }
344 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
345 if err != nil {
346 return AppConfig{}, err
347 }
348 for _, app := range kust.Resources {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400349 if app == id {
350 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
351 if err != nil {
352 return AppConfig{}, err
353 }
354 cfg.Id = id
355 return cfg, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400356 }
357 }
358 return AppConfig{}, nil
359}
360
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400361func auth(signer ssh.Signer) *gitssh.PublicKeys {
362 return &gitssh.PublicKeys{
363 Signer: signer,
364 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
365 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
366 // TODO(giolekva): verify server public key
367 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
368 return nil
369 },
370 },
371 }
372}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400373
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400374func ReadYaml[T any](r io.Reader, o *T) error {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400375 if contents, err := ioutil.ReadAll(r); err != nil {
376 return err
377 } else {
378 return yaml.UnmarshalStrict(contents, o)
379 }
380}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400381
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400382func deriveValues(values any, schema Schema, networks []Network) (map[string]any, error) {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400383 ret := make(map[string]any)
384 for k, v := range values.(map[string]any) { // TODO(giolekva): validate
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400385 def, ok := schema.Fields()[k]
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400386 if !ok {
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400387 return nil, fmt.Errorf("Field not found: %s", k)
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400388 }
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400389 switch def.Kind() {
390 case KindBoolean:
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400391 ret[k] = v
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400392 case KindString:
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400393 ret[k] = v
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400394 case KindNetwork:
395 n, err := findNetwork(networks, v.(string)) // TODO(giolekva): validate
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400396 if err != nil {
397 return nil, err
398 }
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400399 ret[k] = n
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400400 case KindAuth:
401 r, err := deriveValues(v, AuthSchema, networks)
402 if err != nil {
403 return nil, err
404 }
405 ret[k] = r
Giorgi Lekveishvili7c427602024-01-04 00:13:55 +0400406 case KindStruct:
407 r, err := deriveValues(v, def, networks)
408 if err != nil {
409 return nil, err
410 }
411 ret[k] = r
412 default:
413 return nil, fmt.Errorf("Should not reach!")
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400414 }
415 }
416 return ret, nil
417}
418
419func findNetwork(networks []Network, name string) (Network, error) {
420 for _, n := range networks {
421 if n.Name == name {
422 return n, nil
423 }
424 }
425 return Network{}, fmt.Errorf("Network not found: %s", name)
426}
427
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400428type Network struct {
Giorgi Lekveishvili9b52ab92024-01-05 13:12:48 +0400429 Name string `json:"name,omitempty"`
430 IngressClass string `json:"ingressClass,omitempty"`
431 CertificateIssuer string `json:"certificateIssuer,omitempty"`
432 Domain string `json:"domain,omitempty"`
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400433}