blob: b01cd82e1f976e8c78921b54ae7cce5e1785ee99 [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
34 InstallApp(app App, path string, values map[string]any) error
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040035 FindAllInstances(root string, name string) ([]AppConfig, error)
36 FindInstance(root string, name string) (AppConfig, error)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040037}
38
39type repoIO struct {
40 repo *git.Repository
41 signer ssh.Signer
42}
43
44func NewRepoIO(repo *git.Repository, signer ssh.Signer) RepoIO {
45 return &repoIO{
46 repo,
47 signer,
48 }
49}
50
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040051func (r *repoIO) Fetch() error {
52 err := r.repo.Fetch(&git.FetchOptions{
53 RemoteName: "origin",
54 Auth: auth(r.signer),
55 Force: true,
56 })
57 if err == nil || err == git.NoErrAlreadyUpToDate {
58 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040059 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040060 return err
61}
62
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040063func (r *repoIO) ReadConfig() (Config, error) {
64 configF, err := r.Reader(configFileName)
65 if err != nil {
66 return Config{}, err
67 }
68 defer configF.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +040069 var cfg Config
70 if err := readYaml(configF, &cfg); err != nil {
71 return Config{}, err
72 } else {
73 return cfg, nil
74 }
75}
76
77func (r *repoIO) ReadAppConfig(path string) (AppConfig, error) {
78 configF, err := r.Reader(path)
79 if err != nil {
80 return AppConfig{}, err
81 }
82 defer configF.Close()
83 var cfg AppConfig
84 if err := readYaml(configF, &cfg); err != nil {
85 return AppConfig{}, err
86 } else {
87 return cfg, nil
88 }
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040089}
90
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040091func (r *repoIO) ReadKustomization(path string) (*Kustomization, error) {
92 inp, err := r.Reader(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040093 if err != nil {
94 return nil, err
95 }
96 defer inp.Close()
97 return ReadKustomization(inp)
98}
99
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400100func (r *repoIO) Reader(path string) (io.ReadCloser, error) {
101 wt, err := r.repo.Worktree()
102 if err != nil {
103 return nil, err
104 }
105 return wt.Filesystem.Open(path)
106}
107
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400108func (r *repoIO) Writer(path string) (io.WriteCloser, error) {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400109 wt, err := r.repo.Worktree()
110 if err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400111 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400112 }
113 if err := wt.Filesystem.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400114 return nil, err
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400115 }
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400116 return wt.Filesystem.Create(path)
117}
118
119func (r *repoIO) WriteKustomization(path string, kust Kustomization) error {
120 out, err := r.Writer(path)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400121 if err != nil {
122 return err
123 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400124 return kust.Write(out)
125}
126
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400127func (r *repoIO) WriteYaml(path string, data any) error {
128 out, err := r.Writer(path)
129 if err != nil {
130 return err
131 }
132 serialized, err := yaml.Marshal(data)
133 if err != nil {
134 return err
135 }
136 if _, err := out.Write(serialized); err != nil {
137 return err
138 }
139 return nil
140}
141
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400142func (r *repoIO) CommitAndPush(message string) error {
143 wt, err := r.repo.Worktree()
144 if err != nil {
145 return err
146 }
147 if err := wt.AddGlob("*"); err != nil {
148 return err
149 }
150 if _, err := wt.Commit(message, &git.CommitOptions{
151 Author: &object.Signature{
152 Name: "pcloud-installer",
153 When: time.Now(),
154 },
155 }); err != nil {
156 return err
157 }
158 return r.repo.Push(&git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400159 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400160 Auth: auth(r.signer),
161 })
162}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400163
164func (r *repoIO) CreateDir(path string) error {
165 wt, err := r.repo.Worktree()
166 if err != nil {
167 return err
168 }
169 return wt.Filesystem.MkdirAll(path, fs.ModePerm)
170}
171
172func (r *repoIO) RemoveDir(path string) error {
173 wt, err := r.repo.Worktree()
174 if err != nil {
175 return err
176 }
177 err = util.RemoveAll(wt.Filesystem, path)
178 if err == nil || errors.Is(err, fs.ErrNotExist) {
179 return nil
180 }
181 return err
182}
183
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400184type AppConfig struct {
185 Id string `json:"id"`
186 Config map[string]any `json:"config"`
187}
188
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400189func (r *repoIO) InstallApp(app App, appRootDir string, values map[string]any) error {
190 if !filepath.IsAbs(appRootDir) {
191 return fmt.Errorf("Expected absolute path: %s", appRootDir)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400192 }
Giorgi Lekveishvili6e813182023-06-30 13:45:30 +0400193 appRootDir = filepath.Clean(appRootDir)
194 for p := appRootDir; p != "/"; {
195 parent, child := filepath.Split(p)
196 kustPath := filepath.Join(parent, "kustomization.yaml")
197 kust, err := r.ReadKustomization(kustPath)
198 if err != nil {
199 if errors.Is(err, fs.ErrNotExist) {
200 k := NewKustomization()
201 kust = &k
202 } else {
203 return err
204 }
205 }
206 kust.AddResources(child)
207 if err := r.WriteKustomization(kustPath, *kust); err != nil {
208 return err
209 }
210 p = filepath.Clean(parent)
211 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400212 {
213 if err := r.RemoveDir(appRootDir); err != nil {
214 return err
215 }
216 if err := r.CreateDir(appRootDir); err != nil {
217 return err
218 }
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400219 cfg := AppConfig{
220 Id: app.Name,
221 Config: values,
222 }
223 if err := r.WriteYaml(path.Join(appRootDir, configFileName), cfg); err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400224 return err
225 }
226 }
227 {
228 appKust := NewKustomization()
229 for _, t := range app.Templates {
230 appKust.AddResources(t.Name())
231 out, err := r.Writer(path.Join(appRootDir, t.Name()))
232 if err != nil {
233 return err
234 }
235 defer out.Close()
236 if err := t.Execute(out, values); err != nil {
237 return err
238 }
239 }
240 if err := r.WriteKustomization(path.Join(appRootDir, "kustomization.yaml"), appKust); err != nil {
241 return err
242 }
243 }
244 return r.CommitAndPush(fmt.Sprintf("install: %s", app.Name))
245}
246
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400247func (r *repoIO) FindAllInstances(root string, name string) ([]AppConfig, error) {
248 if !filepath.IsAbs(root) {
249 return nil, fmt.Errorf("Expected absolute path: %s", root)
250 }
251 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
252 if err != nil {
253 return nil, err
254 }
255 ret := make([]AppConfig, 0)
256 for _, app := range kust.Resources {
257 cfg, err := r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
258 if err != nil {
259 return nil, err
260 }
261 if cfg.Id == name {
262 ret = append(ret, cfg)
263 }
264 }
265 return ret, nil
266}
267
268func (r *repoIO) FindInstance(root string, name string) (AppConfig, error) {
269 if !filepath.IsAbs(root) {
270 return AppConfig{}, fmt.Errorf("Expected absolute path: %s", root)
271 }
272 kust, err := r.ReadKustomization(filepath.Join(root, "kustomization.yaml"))
273 if err != nil {
274 return AppConfig{}, err
275 }
276 for _, app := range kust.Resources {
277 if app == name {
278 return r.ReadAppConfig(filepath.Join(root, app, "config.yaml"))
279 }
280 }
281 return AppConfig{}, nil
282}
283
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400284func auth(signer ssh.Signer) *gitssh.PublicKeys {
285 return &gitssh.PublicKeys{
286 Signer: signer,
287 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
288 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
289 // TODO(giolekva): verify server public key
290 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
291 return nil
292 },
293 },
294 }
295}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400296
297func readYaml[T any](r io.Reader, o *T) error {
298 if contents, err := ioutil.ReadAll(r); err != nil {
299 return err
300 } else {
301 return yaml.UnmarshalStrict(contents, o)
302 }
303}