blob: dcf897db71e6f5161154bb74315bbf8bac246fbd [file] [log] [blame]
gioe72b54f2024-04-22 10:44:41 +04001package soft
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +04002
3import (
gio308105e2024-04-19 13:12:13 +04004 "encoding/json"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +04005 "errors"
gio0eaf2712024-04-14 13:08:46 +04006 "fmt"
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +04007 "io"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +04008 "io/fs"
Giorgi Lekveishvili76951482023-06-30 23:25:09 +04009 "io/ioutil"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040010 "net"
gio0eaf2712024-04-14 13:08:46 +040011 "os"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040012 "path/filepath"
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040013 "sync"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040014 "time"
15
gioe72b54f2024-04-22 10:44:41 +040016 pio "github.com/giolekva/pcloud/core/installer/io"
17
gio3af43942024-04-16 08:13:50 +040018 "github.com/go-git/go-billy/v5"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040019 "github.com/go-git/go-billy/v5/util"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040020 "github.com/go-git/go-git/v5"
gio0eaf2712024-04-14 13:08:46 +040021 "github.com/go-git/go-git/v5/config"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040022 "github.com/go-git/go-git/v5/plumbing/object"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040023 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040024 "golang.org/x/crypto/ssh"
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040025 "sigs.k8s.io/yaml"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040026)
27
gio3af43942024-04-16 08:13:50 +040028type RepoFS interface {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040029 Reader(path string) (io.ReadCloser, error)
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +040030 Writer(path string) (io.WriteCloser, error)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040031 CreateDir(path string) error
32 RemoveDir(path string) error
giof8843412024-05-22 16:38:05 +040033 ListDir(path string) ([]os.FileInfo, error)
gio3af43942024-04-16 08:13:50 +040034}
35
gio308105e2024-04-19 13:12:13 +040036type DoFn func(r RepoFS) (string, error)
37
38type doOptions struct {
giof71a0832024-06-27 14:45:45 +040039 NoPull bool
gio308105e2024-04-19 13:12:13 +040040 NoCommit bool
gio0eaf2712024-04-14 13:08:46 +040041 Force bool
42 ToBranch string
gio308105e2024-04-19 13:12:13 +040043}
44
45type DoOption func(*doOptions)
46
giof71a0832024-06-27 14:45:45 +040047func WithNoPull() DoOption {
48 return func(o *doOptions) {
49 o.NoPull = true
50 }
51}
52
gio308105e2024-04-19 13:12:13 +040053func WithNoCommit() DoOption {
54 return func(o *doOptions) {
55 o.NoCommit = true
56 }
57}
gio3af43942024-04-16 08:13:50 +040058
gio0eaf2712024-04-14 13:08:46 +040059func WithForce() DoOption {
60 return func(o *doOptions) {
61 o.Force = true
62 }
63}
64
65func WithCommitToBranch(branch string) DoOption {
66 return func(o *doOptions) {
67 o.ToBranch = branch
68 }
69}
70
71type pushOptions struct {
72 ToBranch string
73 Force bool
74}
75
76type PushOption func(*pushOptions)
77
78func WithToBranch(branch string) PushOption {
79 return func(o *pushOptions) {
80 o.ToBranch = branch
81 }
82}
83
84func PushWithForce() PushOption {
85 return func(o *pushOptions) {
86 o.Force = true
87 }
88}
89
gio3af43942024-04-16 08:13:50 +040090type RepoIO interface {
91 RepoFS
92 FullAddress() string
93 Pull() error
gio0eaf2712024-04-14 13:08:46 +040094 CommitAndPush(message string, opts ...PushOption) error
gio308105e2024-04-19 13:12:13 +040095 Do(op DoFn, opts ...DoOption) error
gio3af43942024-04-16 08:13:50 +040096}
97
98type repoFS struct {
99 fs billy.Filesystem
100}
101
gioe72b54f2024-04-22 10:44:41 +0400102func NewBillyRepoFS(fs billy.Filesystem) RepoFS {
103 return &repoFS{fs}
104}
105
gio3af43942024-04-16 08:13:50 +0400106func (r *repoFS) Reader(path string) (io.ReadCloser, error) {
107 return r.fs.Open(path)
108}
109
110func (r *repoFS) Writer(path string) (io.WriteCloser, error) {
111 if err := r.fs.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
112 return nil, err
113 }
114 return r.fs.Create(path)
115}
116
117func (r *repoFS) CreateDir(path string) error {
118 return r.fs.MkdirAll(path, fs.ModePerm)
119}
120
121func (r *repoFS) RemoveDir(path string) error {
122 if err := util.RemoveAll(r.fs, path); err != nil {
123 if errors.Is(err, fs.ErrNotExist) {
124 return nil
125 }
126 return err
127 }
128 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400129}
130
giof8843412024-05-22 16:38:05 +0400131func (r *repoFS) ListDir(path string) ([]os.FileInfo, error) {
132 return r.fs.ReadDir(path)
133}
134
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400135type repoIO struct {
gio3af43942024-04-16 08:13:50 +0400136 *repoFS
gioe72b54f2024-04-22 10:44:41 +0400137 repo *Repository
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400138 signer ssh.Signer
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400139 l sync.Locker
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400140}
141
gioe72b54f2024-04-22 10:44:41 +0400142func NewRepoIO(repo *Repository, signer ssh.Signer) (RepoIO, error) {
gio3af43942024-04-16 08:13:50 +0400143 wt, err := repo.Worktree()
144 if err != nil {
145 return nil, err
146 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400147 return &repoIO{
gio3af43942024-04-16 08:13:50 +0400148 &repoFS{wt.Filesystem},
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400149 repo,
150 signer,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400151 &sync.Mutex{},
gio3af43942024-04-16 08:13:50 +0400152 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400153}
154
gio3af43942024-04-16 08:13:50 +0400155func (r *repoIO) FullAddress() string {
156 return r.repo.Addr.FullAddress()
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400157}
158
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400159func (r *repoIO) Pull() error {
160 r.l.Lock()
161 defer r.l.Unlock()
162 return r.pullWithoutLock()
163}
164
165func (r *repoIO) pullWithoutLock() error {
166 wt, err := r.repo.Worktree()
167 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400168 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400169 }
gio3cdee592024-04-17 10:15:56 +0400170 err = wt.Pull(&git.PullOptions{
gio0eaf2712024-04-14 13:08:46 +0400171 Auth: auth(r.signer),
172 Force: true,
173 Progress: os.Stdout,
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400174 })
gio3cdee592024-04-17 10:15:56 +0400175 if err == nil {
176 return nil
177 }
178 if errors.Is(err, git.NoErrAlreadyUpToDate) {
179 return nil
180 }
181 // TODO(gio): check `remote repository is empty`
giof5ffedb2024-06-19 14:14:43 +0400182 fmt.Printf("-- GIT PULL: %s\n", err.Error())
gio3cdee592024-04-17 10:15:56 +0400183 return nil
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400184}
185
gio0eaf2712024-04-14 13:08:46 +0400186func (r *repoIO) CommitAndPush(message string, opts ...PushOption) error {
187 var o pushOptions
188 for _, i := range opts {
189 i(&o)
190 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400191 wt, err := r.repo.Worktree()
192 if err != nil {
193 return err
194 }
195 if err := wt.AddGlob("*"); err != nil {
196 return err
197 }
gio03fd0c72024-06-18 12:31:42 +0400198 st, err := wt.Status()
199 if err != nil {
200 return err
201 }
202 if len(st) == 0 {
203 return nil // TODO(gio): maybe return ErrorNothingToCommit
204 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400205 if _, err := wt.Commit(message, &git.CommitOptions{
206 Author: &object.Signature{
207 Name: "pcloud-installer",
208 When: time.Now(),
209 },
210 }); err != nil {
211 return err
212 }
gio0eaf2712024-04-14 13:08:46 +0400213 gopts := &git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400214 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400215 Auth: auth(r.signer),
gio0eaf2712024-04-14 13:08:46 +0400216 }
217 if o.ToBranch != "" {
218 gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/heads/master:refs/heads/%s", o.ToBranch))}
219 }
220 if o.Force {
221 gopts.Force = true
222 }
223 return r.repo.Push(gopts)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400224}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400225
gio308105e2024-04-19 13:12:13 +0400226func (r *repoIO) Do(op DoFn, opts ...DoOption) error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400227 r.l.Lock()
228 defer r.l.Unlock()
gio308105e2024-04-19 13:12:13 +0400229 o := &doOptions{}
230 for _, i := range opts {
231 i(o)
232 }
giof71a0832024-06-27 14:45:45 +0400233 if !o.NoPull {
234 if err := r.pullWithoutLock(); err != nil {
235 return err
236 }
237 }
gio3af43942024-04-16 08:13:50 +0400238 if msg, err := op(r); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400239 return err
gio3af43942024-04-16 08:13:50 +0400240 } else {
gio308105e2024-04-19 13:12:13 +0400241 if !o.NoCommit {
gio0eaf2712024-04-14 13:08:46 +0400242 popts := []PushOption{}
243 if o.Force {
244 popts = append(popts, PushWithForce())
245 }
246 if o.ToBranch != "" {
247 popts = append(popts, WithToBranch(o.ToBranch))
248 }
249 return r.CommitAndPush(msg, popts...)
gio308105e2024-04-19 13:12:13 +0400250 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400251 }
gio308105e2024-04-19 13:12:13 +0400252 return nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400253}
254
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400255func auth(signer ssh.Signer) *gitssh.PublicKeys {
256 return &gitssh.PublicKeys{
257 Signer: signer,
258 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
259 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
260 // TODO(giolekva): verify server public key
261 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
262 return nil
263 },
264 },
265 }
266}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400267
gio3af43942024-04-16 08:13:50 +0400268func ReadYaml[T any](repo RepoFS, path string, o *T) error {
269 r, err := repo.Reader(path)
270 if err != nil {
271 return err
272 }
273 defer r.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400274 if contents, err := ioutil.ReadAll(r); err != nil {
275 return err
276 } else {
277 return yaml.UnmarshalStrict(contents, o)
278 }
279}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400280
gio3af43942024-04-16 08:13:50 +0400281func WriteYaml(repo RepoFS, path string, data any) error {
gioe72b54f2024-04-22 10:44:41 +0400282 if d, ok := data.(*pio.Kustomization); ok {
gio3af43942024-04-16 08:13:50 +0400283 data = d
284 }
285 out, err := repo.Writer(path)
286 if err != nil {
287 return err
288 }
289 serialized, err := yaml.Marshal(data)
290 if err != nil {
291 return err
292 }
293 if _, err := out.Write(serialized); err != nil {
294 return err
295 }
296 return nil
297}
298
gio308105e2024-04-19 13:12:13 +0400299func ReadJson[T any](repo RepoFS, path string, o *T) error {
300 r, err := repo.Reader(path)
301 if err != nil {
302 return err
303 }
304 defer r.Close()
305 return json.NewDecoder(r).Decode(o)
306}
307
308func WriteJson(repo RepoFS, path string, data any) error {
gioe72b54f2024-04-22 10:44:41 +0400309 if d, ok := data.(*pio.Kustomization); ok {
gio308105e2024-04-19 13:12:13 +0400310 data = d
311 }
312 w, err := repo.Writer(path)
313 if err != nil {
314 return err
315 }
316 e := json.NewEncoder(w)
317 e.SetIndent("", "\t")
318 return e.Encode(data)
319}
320
gioe72b54f2024-04-22 10:44:41 +0400321func ReadKustomization(repo RepoFS, path string) (*pio.Kustomization, error) {
322 ret := &pio.Kustomization{}
gio3af43942024-04-16 08:13:50 +0400323 if err := ReadYaml(repo, path, &ret); err != nil {
324 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400325 }
326 return ret, nil
327}
gio0eaf2712024-04-14 13:08:46 +0400328
329func ReadFile(repo RepoFS, path string) ([]byte, error) {
330 r, err := repo.Reader(path)
331 if err != nil {
332 return nil, err
333 }
334 defer r.Close()
335 return io.ReadAll(r)
336}