blob: 05825943c4a55239208b1ab6e49853506e97e735 [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 Lekveishvili0ccd1482023-06-21 15:02:24 +040028 WriteYaml(path string, data any) error
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040029 CommitAndPush(message string) error
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040030 Reader(path string) (io.ReadCloser, error)
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +040031 Writer(path string) (io.WriteCloser, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040032 CreateDir(path string) error
33 RemoveDir(path string) error
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +040034 InstallApp(app App, path string, values map[string]any, derived Derived) error
35 RemoveApp(path string) error
36 FindAllInstances(root string, appId string) ([]AppConfig, error)
37 FindInstance(root string, id string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040038}
39
40type repoIO struct {
41 repo *git.Repository
42 signer ssh.Signer
43}
44
45func NewRepoIO(repo *git.Repository, signer ssh.Signer) RepoIO {
46 return &repoIO{
47 repo,
48 signer,
49 }
50}
51
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040052func (r *repoIO) Fetch() error {
53 err := r.repo.Fetch(&git.FetchOptions{
54 RemoteName: "origin",
55 Auth: auth(r.signer),
56 Force: true,
57 })
58 if err == nil || err == git.NoErrAlreadyUpToDate {
59 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040060 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040061 return err
62}
63
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040064func (r *repoIO) ReadConfig() (Config, error) {
65 configF, err := r.Reader(configFileName)
66 if err != nil {
67 return Config{}, err
68 }
69 defer configF.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040070 var cfg Config
71 if err := readYaml(configF, &cfg); err != nil {
72 return Config{}, err
73 } else {
74 return cfg, nil
75 }
76}
77
78func (r *repoIO) ReadAppConfig(path string) (AppConfig, error) {
79 configF, err := r.Reader(path)
80 if err != nil {
81 return AppConfig{}, err
82 }
83 defer configF.Close()
84 var cfg AppConfig
85 if err := readYaml(configF, &cfg); err != nil {
86 return AppConfig{}, err
87 } else {
88 return cfg, nil
89 }
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040090}
91
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040092func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
93 inp, err := r.Reader(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040094 if err != nil {
95 return nil, err
96 }
97 defer inp.Close()
98 return ReadKustomization(inp)
99}
100
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400101func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
102 wt, err := r.repo.Worktree()
103 if err != nil {
104 return nil, err
105 }
106 return wt.Filesystem.Open(path)
107}
108
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400109func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400110 wt, err := r.repo.Worktree()
111 if err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400112 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400113 }
114 if err := wt.Filesystem.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400115 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400116 }
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400117 return wt.Filesystem.Create(path)
118}
119
120func (r *repoIO) WriteKustomization(path string, kust Kustomization) error {
121 out, err := r.Writer(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400122 if err != nil {
123 return err
124 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400125 return kust.Write(out)
126}
127
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400128func (r *repoIO) WriteYaml(path string, data any) error {
129 out, err := r.Writer(path)
130 if err != nil {
131 return err
132 }
133 serialized, err := yaml.Marshal(data)
134 if err != nil {
135 return err
136 }
137 if _, err := out.Write(serialized); err != nil {
138 return err
139 }
140 return nil
141}
142
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400143func (r *repoIO) CommitAndPush(message string) error {
144 wt, err := r.repo.Worktree()
145 if err != nil {
146 return err
147 }
148 if err := wt.AddGlob("*"); err != nil {
149 return err
150 }
151 if _, err := wt.Commit(message, &git.CommitOptions{
152 Author: &object.Signature{
153 Name: "pcloud-installer",
154 When: time.Now(),
155 },
156 }); err != nil {
157 return err
158 }
159 return r.repo.Push(&git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400160 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400161 Auth: auth(r.signer),
162 })
163}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400164
165func (r *repoIO) CreateDir(path string) error {
166 wt, err := r.repo.Worktree()
167 if err != nil {
168 return err
169 }
170 return wt.Filesystem.MkdirAll(path, fs.ModePerm)
171}
172
173func (r *repoIO) RemoveDir(path string) error {
174 wt, err := r.repo.Worktree()
175 if err != nil {
176 return err
177 }
178 err = util.RemoveAll(wt.Filesystem, path)
179 if err == nil || errors.Is(err, fs.ErrNotExist) {
180 return nil
181 }
182 return err
183}
184
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400185type Release struct {
186 Namespace string `json:"Namespace"`
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400187}
188
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400189type Derived struct {
190 Release Release `json:"Release"`
191 Global Values `json:"Global"`
192 Values map[string]any `json:"Values"`
193}
194
195type AppConfig struct {
196 Id string `json:"id"`
197 AppId string `json:"appId"`
198 Config map[string]any `json:"config"`
199 Derived Derived `json:"derived"`
200}
201
202func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any, derived Derived) error {
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400203 if !filepath.IsAbs(appRootDir) {
204 return fmt.Errorf("Expected absolute path: %s", appRootDir)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400205 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400206 appRootDir = filepath.Clean(appRootDir)
207 for p := appRootDir; p != "/"; {
208 parent, child := filepath.Split(p)
209 kustPath := filepath.Join(parent, "kustomization.yaml")
210 kust, err := r.ReadKustomization(kustPath)
211 if err != nil {
212 if errors.Is(err, fs.ErrNotExist) {
213 k := NewKustomization()
214 kust = &k
215 } else {
216 return err
217 }
218 }
219 kust.AddResources(child)
220 if err := r.WriteKustomization(kustPath, *kust); err != nil {
221 return err
222 }
223 p = filepath.Clean(parent)
224 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400225 {
226 if err := r.RemoveDir(appRootDir); err != nil {
227 return err
228 }
229 if err := r.CreateDir(appRootDir); err != nil {
230 return err
231 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400232 cfg := AppConfig{
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400233 AppId: app.Name,
234 Config: values,
235 Derived: derived,
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400236 }
237 if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400238 return err
239 }
240 }
241 {
242 appKust := NewKustomization()
243 for _, t := range app.Templates {
244 appKust.AddResources(t.Name())
245 out, err := r.Writer(path.Join(appRootDir, t.Name()))
246 if err != nil {
247 return err
248 }
249 defer out.Close()
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400250 if err := t.Execute(out, derived); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400251 return err
252 }
253 }
254 if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
255 return err
256 }
257 }
258 return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
259}
260
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400261func (r *repoIO) RemoveApp(appRootDir string) error {
262 r.RemoveDir(appRootDir)
263 parent, child := filepath.Split(appRootDir)
264 kustPath := filepath.Join(parent, "kustomization.yaml")
265 kust, err := r.ReadKustomization(kustPath)
266 if err != nil {
267 return err
268 }
269 kust.RemoveResources(child)
270 r.WriteKustomization(kustPath, *kust)
271 return r.CommitAndPush(fmt.Sprintf("uninstall: %s", child))
272}
273
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400274func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
275 if !filepath.IsAbs(root) {
276 return nil, fmt.Errorf("Expected absolute path: %s", root)
277 }
278 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
279 if err != nil {
280 return nil, err
281 }
282 ret := make([]AppConfig, 0)
283 for _, app := range kust.Resources {
284 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
285 if err != nil {
286 return nil, err
287 }
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400288 cfg.Id = app
289 if cfg.AppId == name {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400290 ret = append(ret, cfg)
291 }
292 }
293 return ret, nil
294}
295
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400296func (r *repoIO) FindInstance(root string, id string) (AppConfig, error) {
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400297 if !filepath.IsAbs(root) {
298 return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
299 }
300 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
301 if err != nil {
302 return AppConfig{}, err
303 }
304 for _, app := range kust.Resources {
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400305 if app == id {
306 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
307 if err != nil {
308 return AppConfig{}, err
309 }
310 cfg.Id = id
311 return cfg, nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400312 }
313 }
314 return AppConfig{}, nil
315}
316
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400317func auth(signer ssh.Signer) *gitssh.PublicKeys {
318 return &gitssh.PublicKeys{
319 Signer: signer,
320 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
321 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
322 // TODO(giolekva): verify server public key
323 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
324 return nil
325 },
326 },
327 }
328}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400329
330func readYaml[T any](r io.Reader, o *T) error {
331 if contents, err := ioutil.ReadAll(r); err != nil {
332 return err
333 } else {
334 return yaml.UnmarshalStrict(contents, o)
335 }
336}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400337
338func deriveValues(values any, schema map[string]any, networks []Network) (map[string]any, error) {
339 ret := make(map[string]any)
340 for k, v := range values.(map[string]any) { // TODO(giolekva): validate
341 def, err := fieldSchema(schema, k)
342 if err != nil {
343 return nil, err
344 }
345 t, ok := def["type"]
346 if !ok {
347 return nil, fmt.Errorf("Found field with undefined type: %s", k)
348 }
349 if t == "string" {
350 role, ok := def["role"]
351 if ok && role == "network" {
352 n, err := findNetwork(networks, v.(string)) // TODO(giolekva): validate
353 if err != nil {
354 return nil, err
355 }
356 ret[k] = n
357 } else {
358 ret[k] = v
359 }
360 } else {
361 ret[k], err = deriveValues(v, def, networks)
362 if err != nil {
363 return nil, err
364 }
365 }
366 }
367 return ret, nil
368}
369
370func findNetwork(networks []Network, name string) (Network, error) {
371 for _, n := range networks {
372 if n.Name == name {
373 return n, nil
374 }
375 }
376 return Network{}, fmt.Errorf("Network not found: %s", name)
377}
378
379func fieldSchema(schema map[string]any, key string) (map[string]any, error) {
380 properties, ok := schema["properties"]
381 if !ok {
382 return nil, fmt.Errorf("Properties not found")
383 }
384 propMap, ok := properties.(map[string]any)
385 if !ok {
386 return nil, fmt.Errorf("Expected properties to be map")
387 }
388 def, ok := propMap[key]
389 if !ok {
390 return nil, fmt.Errorf("Unknown field: %s", key)
391 }
392 ret, ok := def.(map[string]any)
393 if !ok {
394 return nil, fmt.Errorf("Invalid schema")
395 }
396 return ret, nil
397}
398
399type Network struct {
400 Name string
401 IngressClass string
402 CertificateIssuer string
403 Domain string
404}