blob: ec7981c65a49a78f103991a181935b67ff743a68 [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 Lekveishvili3550b432023-06-09 19:37:51 +040020)
21
22type RepoIO interface {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040023 Fetch() error
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040024 ReadConfig() (Config, error)
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040025 ReadAppConfig(path string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040026 ReadKustomization(path string) (*Kustomization, error)
27 WriteKustomization(path string, kust Kustomization) error
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +040028 ReadYaml(path string) (any, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040029 WriteYaml(path string, data any) error
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040030 CommitAndPush(message string) error
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040031 Reader(path string) (io.ReadCloser, error)
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +040032 Writer(path string) (io.WriteCloser, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040033 CreateDir(path string) error
34 RemoveDir(path string) error
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040035 InstallApp(app App, path string, values map[string]any, derived Derived) error
36 RemoveApp(path string) error
37 FindAllInstances(root string, appId string) ([]AppConfig, error)
38 FindInstance(root string, id string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040039}
40
41type repoIO struct {
42 repo *git.Repository
43 signer ssh.Signer
44}
45
46func NewRepoIO(repo *git.Repository, signer ssh.Signer) RepoIO {
47 return &repoIO{
48 repo,
49 signer,
50 }
51}
52
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040053func (r *repoIO) Fetch() error {
54 err := r.repo.Fetch(&git.FetchOptions{
55 RemoteName: "origin",
56 Auth: auth(r.signer),
57 Force: true,
58 })
59 if err == nil || err == git.NoErrAlreadyUpToDate {
60 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040061 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040062 return err
63}
64
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040065func (r *repoIO) ReadConfig() (Config, error) {
66 configF, err := r.Reader(configFileName)
67 if err != nil {
68 return Config{}, err
69 }
70 defer configF.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040071 var cfg Config
72 if err := readYaml(configF, &cfg); err != nil {
73 return Config{}, err
74 } else {
75 return cfg, nil
76 }
77}
78
79func (r *repoIO) ReadAppConfig(path string) (AppConfig, error) {
80 configF, err := r.Reader(path)
81 if err != nil {
82 return AppConfig{}, err
83 }
84 defer configF.Close()
85 var cfg AppConfig
86 if err := readYaml(configF, &cfg); err != nil {
87 return AppConfig{}, err
88 } else {
89 return cfg, nil
90 }
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040091}
92
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040093func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
94 inp, err := r.Reader(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040095 if err != nil {
96 return nil, err
97 }
98 defer inp.Close()
99 return ReadKustomization(inp)
100}
101
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400102func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
103 wt, err := r.repo.Worktree()
104 if err != nil {
105 return nil, err
106 }
107 return wt.Filesystem.Open(path)
108}
109
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400110func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400111 wt, err := r.repo.Worktree()
112 if err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400113 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400114 }
115 if err := wt.Filesystem.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400116 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400117 }
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400118 return wt.Filesystem.Create(path)
119}
120
121func (r *repoIO) WriteKustomization(path string, kust Kustomization) error {
122 out, err := r.Writer(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400123 if err != nil {
124 return err
125 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400126 return kust.Write(out)
127}
128
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400129func (r *repoIO) WriteYaml(path string, data any) error {
130 out, err := r.Writer(path)
131 if err != nil {
132 return err
133 }
134 serialized, err := yaml.Marshal(data)
135 if err != nil {
136 return err
137 }
138 if _, err := out.Write(serialized); err != nil {
139 return err
140 }
141 return nil
142}
143
Giorgi Lekveishvili1506a4f2023-07-11 11:49:02 +0400144func (r *repoIO) ReadYaml(path string) (any, error) {
145 inp, err := r.Reader(path)
146 if err != nil {
147 return nil, err
148 }
149 data := make(map[string]any)
150 if err := readYaml(inp, &data); err != nil {
151 return nil, err
152 }
153 return data, err
154}
155
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400156func (r *repoIO) CommitAndPush(message string) error {
157 wt, err := r.repo.Worktree()
158 if err != nil {
159 return err
160 }
161 if err := wt.AddGlob("*"); err != nil {
162 return err
163 }
164 if _, err := wt.Commit(message, &git.CommitOptions{
165 Author: &object.Signature{
166 Name: "pcloud-installer",
167 When: time.Now(),
168 },
169 }); err != nil {
170 return err
171 }
172 return r.repo.Push(&git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400173 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400174 Auth: auth(r.signer),
175 })
176}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400177
178func (r *repoIO) CreateDir(path string) error {
179 wt, err := r.repo.Worktree()
180 if err != nil {
181 return err
182 }
183 return wt.Filesystem.MkdirAll(path, fs.ModePerm)
184}
185
186func (r *repoIO) RemoveDir(path string) error {
187 wt, err := r.repo.Worktree()
188 if err != nil {
189 return err
190 }
191 err = util.RemoveAll(wt.Filesystem, path)
192 if err == nil || errors.Is(err, fs.ErrNotExist) {
193 return nil
194 }
195 return err
196}
197
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400198type Release struct {
199 Namespace string `json:"Namespace"`
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400200}
201
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400202type Derived struct {
203 Release Release `json:"Release"`
204 Global Values `json:"Global"`
205 Values map[string]any `json:"Values"`
206}
207
208type AppConfig struct {
209 Id string `json:"id"`
210 AppId string `json:"appId"`
211 Config map[string]any `json:"config"`
212 Derived Derived `json:"derived"`
213}
214
215func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any, derived Derived) error {
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400216 if !filepath.IsAbs(appRootDir) {
217 return fmt.Errorf("Expected absolute path: %s", appRootDir)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400218 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400219 appRootDir = filepath.Clean(appRootDir)
220 for p := appRootDir; p != "/"; {
221 parent, child := filepath.Split(p)
222 kustPath := filepath.Join(parent, "kustomization.yaml")
223 kust, err := r.ReadKustomization(kustPath)
224 if err != nil {
225 if errors.Is(err, fs.ErrNotExist) {
226 k := NewKustomization()
227 kust = &k
228 } else {
229 return err
230 }
231 }
232 kust.AddResources(child)
233 if err := r.WriteKustomization(kustPath, *kust); err != nil {
234 return err
235 }
236 p = filepath.Clean(parent)
237 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400238 {
239 if err := r.RemoveDir(appRootDir); err != nil {
240 return err
241 }
242 if err := r.CreateDir(appRootDir); err != nil {
243 return err
244 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400245 cfg := AppConfig{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400246 AppId: app.Name,
247 Config: values,
248 Derived: derived,
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400249 }
250 if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400251 return err
252 }
253 }
254 {
255 appKust := NewKustomization()
256 for _, t := range app.Templates {
257 appKust.AddResources(t.Name())
258 out, err := r.Writer(path.Join(appRootDir, t.Name()))
259 if err != nil {
260 return err
261 }
262 defer out.Close()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400263 if err := t.Execute(out, derived); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400264 return err
265 }
266 }
267 if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
268 return err
269 }
270 }
271 return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
272}
273
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400274func (r *repoIO) RemoveApp(appRootDir string) error {
275 r.RemoveDir(appRootDir)
276 parent, child := filepath.Split(appRootDir)
277 kustPath := filepath.Join(parent, "kustomization.yaml")
278 kust, err := r.ReadKustomization(kustPath)
279 if err != nil {
280 return err
281 }
282 kust.RemoveResources(child)
283 r.WriteKustomization(kustPath, *kust)
284 return r.CommitAndPush(fmt.Sprintf("uninstall: %s", child))
285}
286
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400287func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
288 if !filepath.IsAbs(root) {
289 return nil, fmt.Errorf("Expected absolute path: %s", root)
290 }
291 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
292 if err != nil {
293 return nil, err
294 }
295 ret := make([]AppConfig, 0)
296 for _, app := range kust.Resources {
297 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
298 if err != nil {
299 return nil, err
300 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400301 cfg.Id = app
302 if cfg.AppId == name {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400303 ret = append(ret, cfg)
304 }
305 }
306 return ret, nil
307}
308
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400309func (r *repoIO) FindInstance(root string, id string) (AppConfig, error) {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400310 if !filepath.IsAbs(root) {
311 return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
312 }
313 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
314 if err != nil {
315 return AppConfig{}, err
316 }
317 for _, app := range kust.Resources {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400318 if app == id {
319 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
320 if err != nil {
321 return AppConfig{}, err
322 }
323 cfg.Id = id
324 return cfg, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400325 }
326 }
327 return AppConfig{}, nil
328}
329
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400330func auth(signer ssh.Signer) *gitssh.PublicKeys {
331 return &gitssh.PublicKeys{
332 Signer: signer,
333 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
334 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
335 // TODO(giolekva): verify server public key
336 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
337 return nil
338 },
339 },
340 }
341}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400342
343func readYaml[T any](r io.Reader, o *T) error {
344 if contents, err := ioutil.ReadAll(r); err != nil {
345 return err
346 } else {
347 return yaml.UnmarshalStrict(contents, o)
348 }
349}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400350
351func deriveValues(values any, schema map[string]any, networks []Network) (map[string]any, error) {
352 ret := make(map[string]any)
353 for k, v := range values.(map[string]any) { // TODO(giolekva): validate
354 def, err := fieldSchema(schema, k)
355 if err != nil {
356 return nil, err
357 }
358 t, ok := def["type"]
359 if !ok {
360 return nil, fmt.Errorf("Found field with undefined type: %s", k)
361 }
362 if t == "string" {
363 role, ok := def["role"]
364 if ok && role == "network" {
365 n, err := findNetwork(networks, v.(string)) // TODO(giolekva): validate
366 if err != nil {
367 return nil, err
368 }
369 ret[k] = n
370 } else {
371 ret[k] = v
372 }
373 } else {
374 ret[k], err = deriveValues(v, def, networks)
375 if err != nil {
376 return nil, err
377 }
378 }
379 }
380 return ret, nil
381}
382
383func findNetwork(networks []Network, name string) (Network, error) {
384 for _, n := range networks {
385 if n.Name == name {
386 return n, nil
387 }
388 }
389 return Network{}, fmt.Errorf("Network not found: %s", name)
390}
391
392func fieldSchema(schema map[string]any, key string) (map[string]any, error) {
393 properties, ok := schema["properties"]
394 if !ok {
395 return nil, fmt.Errorf("Properties not found")
396 }
397 propMap, ok := properties.(map[string]any)
398 if !ok {
399 return nil, fmt.Errorf("Expected properties to be map")
400 }
401 def, ok := propMap[key]
402 if !ok {
403 return nil, fmt.Errorf("Unknown field: %s", key)
404 }
405 ret, ok := def.(map[string]any)
406 if !ok {
407 return nil, fmt.Errorf("Invalid schema")
408 }
409 return ret, nil
410}
411
412type Network struct {
413 Name string
414 IngressClass string
415 CertificateIssuer string
416 Domain string
417}