blob: 204caec71ab442fabfccf4a7cf11a7548fb3b78b [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"
12 "time"
13
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040014 "github.com/go-git/go-billy/v5/util"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040015 "github.com/go-git/go-git/v5"
16 "github.com/go-git/go-git/v5/plumbing/object"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040017 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040018 "golang.org/x/crypto/ssh"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040019 "sigs.k8s.io/yaml"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040020
21 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040022)
23
24type RepoIO interface {
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040025 Addr() string
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040026 Fetch() error
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040027 ReadConfig() (Config, error)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040028 ReadAppConfig(path string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040029 ReadKustomization(path string) (*Kustomization, error)
30 WriteKustomization(path string, kust Kustomization) error
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +040031 ReadYaml(path string) (any, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040032 WriteYaml(path string, data any) error
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040033 CommitAndPush(message string) error
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040034 WriteCommitAndPush(path, contents, 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 Lekveishvili724885f2023-11-29 16:18:42 +040057func (r *repoIO) Addr() string {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040058 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 Lekveishvili724885f2023-11-29 16:18:42 +0400164func (r *repoIO) WriteCommitAndPush(path, contents, message string) error {
165 w, err := r.Writer(path)
166 if err != nil {
167 return err
168 }
169 defer w.Close()
170 if _, err := io.WriteString(w, contents); err != nil {
171 return err
172 }
173 return r.CommitAndPush(message)
174}
175
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400176func (r *repoIO) CommitAndPush(message string) error {
177 wt, err := r.repo.Worktree()
178 if err != nil {
179 return err
180 }
181 if err := wt.AddGlob("*"); err != nil {
182 return err
183 }
184 if _, err := wt.Commit(message, &git.CommitOptions{
185 Author: &object.Signature{
186 Name: "pcloud-installer",
187 When: time.Now(),
188 },
189 }); err != nil {
190 return err
191 }
192 return r.repo.Push(&git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400193 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400194 Auth: auth(r.signer),
195 })
196}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400197
198func (r *repoIO) CreateDir(path string) error {
199 wt, err := r.repo.Worktree()
200 if err != nil {
201 return err
202 }
203 return wt.Filesystem.MkdirAll(path, fs.ModePerm)
204}
205
206func (r *repoIO) RemoveDir(path string) error {
207 wt, err := r.repo.Worktree()
208 if err != nil {
209 return err
210 }
211 err = util.RemoveAll(wt.Filesystem, path)
212 if err == nil || errors.Is(err, fs.ErrNotExist) {
213 return nil
214 }
215 return err
216}
217
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400218type Release struct {
219 Namespace string `json:"Namespace"`
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400220}
221
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400222type Derived struct {
223 Release Release `json:"Release"`
224 Global Values `json:"Global"`
225 Values map[string]any `json:"Values"`
226}
227
228type AppConfig struct {
229 Id string `json:"id"`
230 AppId string `json:"appId"`
231 Config map[string]any `json:"config"`
232 Derived Derived `json:"derived"`
233}
234
235func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any, derived Derived) error {
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400236 if !filepath.IsAbs(appRootDir) {
237 return fmt.Errorf("Expected absolute path: %s", appRootDir)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400238 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400239 appRootDir = filepath.Clean(appRootDir)
240 for p := appRootDir; p != "/"; {
241 parent, child := filepath.Split(p)
242 kustPath := filepath.Join(parent, "kustomization.yaml")
243 kust, err := r.ReadKustomization(kustPath)
244 if err != nil {
245 if errors.Is(err, fs.ErrNotExist) {
246 k := NewKustomization()
247 kust = &k
248 } else {
249 return err
250 }
251 }
252 kust.AddResources(child)
253 if err := r.WriteKustomization(kustPath, *kust); err != nil {
254 return err
255 }
256 p = filepath.Clean(parent)
257 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400258 {
259 if err := r.RemoveDir(appRootDir); err != nil {
260 return err
261 }
262 if err := r.CreateDir(appRootDir); err != nil {
263 return err
264 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400265 cfg := AppConfig{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400266 AppId: app.Name,
267 Config: values,
268 Derived: derived,
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400269 }
270 if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400271 return err
272 }
273 }
274 {
275 appKust := NewKustomization()
276 for _, t := range app.Templates {
277 appKust.AddResources(t.Name())
278 out, err := r.Writer(path.Join(appRootDir, t.Name()))
279 if err != nil {
280 return err
281 }
282 defer out.Close()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400283 if err := t.Execute(out, derived); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400284 return err
285 }
286 }
287 if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
288 return err
289 }
290 }
291 return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
292}
293
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400294func (r *repoIO) RemoveApp(appRootDir string) error {
295 r.RemoveDir(appRootDir)
296 parent, child := filepath.Split(appRootDir)
297 kustPath := filepath.Join(parent, "kustomization.yaml")
298 kust, err := r.ReadKustomization(kustPath)
299 if err != nil {
300 return err
301 }
302 kust.RemoveResources(child)
303 r.WriteKustomization(kustPath, *kust)
304 return r.CommitAndPush(fmt.Sprintf("uninstall: %s", child))
305}
306
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400307func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
308 if !filepath.IsAbs(root) {
309 return nil, fmt.Errorf("Expected absolute path: %s", root)
310 }
311 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
312 if err != nil {
313 return nil, err
314 }
315 ret := make([]AppConfig, 0)
316 for _, app := range kust.Resources {
317 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
318 if err != nil {
319 return nil, err
320 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400321 cfg.Id = app
322 if cfg.AppId == name {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400323 ret = append(ret, cfg)
324 }
325 }
326 return ret, nil
327}
328
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400329func (r *repoIO) FindInstance(root string, id string) (AppConfig, error) {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400330 if !filepath.IsAbs(root) {
331 return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
332 }
333 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
334 if err != nil {
335 return AppConfig{}, err
336 }
337 for _, app := range kust.Resources {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400338 if app == id {
339 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
340 if err != nil {
341 return AppConfig{}, err
342 }
343 cfg.Id = id
344 return cfg, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400345 }
346 }
347 return AppConfig{}, nil
348}
349
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400350func auth(signer ssh.Signer) *gitssh.PublicKeys {
351 return &gitssh.PublicKeys{
352 Signer: signer,
353 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
354 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
355 // TODO(giolekva): verify server public key
356 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
357 return nil
358 },
359 },
360 }
361}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400362
Giorgi Lekveishvili57dffb32023-08-07 15:45:43 +0400363func ReadYaml[T any](r io.Reader, o *T) error {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400364 if contents, err := ioutil.ReadAll(r); err != nil {
365 return err
366 } else {
367 return yaml.UnmarshalStrict(contents, o)
368 }
369}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400370
371func deriveValues(values any, schema map[string]any, networks []Network) (map[string]any, error) {
372 ret := make(map[string]any)
373 for k, v := range values.(map[string]any) { // TODO(giolekva): validate
374 def, err := fieldSchema(schema, k)
375 if err != nil {
376 return nil, err
377 }
378 t, ok := def["type"]
379 if !ok {
380 return nil, fmt.Errorf("Found field with undefined type: %s", k)
381 }
382 if t == "string" {
383 role, ok := def["role"]
384 if ok && role == "network" {
385 n, err := findNetwork(networks, v.(string)) // TODO(giolekva): validate
386 if err != nil {
387 return nil, err
388 }
389 ret[k] = n
390 } else {
391 ret[k] = v
392 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400393 } else if t == "boolean" {
394 ret[k] = v
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400395 } else {
396 ret[k], err = deriveValues(v, def, networks)
397 if err != nil {
398 return nil, err
399 }
400 }
401 }
402 return ret, nil
403}
404
405func findNetwork(networks []Network, name string) (Network, error) {
406 for _, n := range networks {
407 if n.Name == name {
408 return n, nil
409 }
410 }
411 return Network{}, fmt.Errorf("Network not found: %s", name)
412}
413
414func fieldSchema(schema map[string]any, key string) (map[string]any, error) {
415 properties, ok := schema["properties"]
416 if !ok {
417 return nil, fmt.Errorf("Properties not found")
418 }
419 propMap, ok := properties.(map[string]any)
420 if !ok {
421 return nil, fmt.Errorf("Expected properties to be map")
422 }
423 def, ok := propMap[key]
424 if !ok {
425 return nil, fmt.Errorf("Unknown field: %s", key)
426 }
427 ret, ok := def.(map[string]any)
428 if !ok {
429 return nil, fmt.Errorf("Invalid schema")
430 }
431 return ret, nil
432}
433
434type Network struct {
435 Name string
436 IngressClass string
437 CertificateIssuer string
438 Domain string
439}