blob: b916d248cef38efa41d27d7947a314db1369b8c8 [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
gio3af43942024-04-16 08:13:50 +040033}
34
gio308105e2024-04-19 13:12:13 +040035type DoFn func(r RepoFS) (string, error)
36
37type doOptions struct {
38 NoCommit bool
gio0eaf2712024-04-14 13:08:46 +040039 Force bool
40 ToBranch string
gio308105e2024-04-19 13:12:13 +040041}
42
43type DoOption func(*doOptions)
44
45func WithNoCommit() DoOption {
46 return func(o *doOptions) {
47 o.NoCommit = true
48 }
49}
gio3af43942024-04-16 08:13:50 +040050
gio0eaf2712024-04-14 13:08:46 +040051func WithForce() DoOption {
52 return func(o *doOptions) {
53 o.Force = true
54 }
55}
56
57func WithCommitToBranch(branch string) DoOption {
58 return func(o *doOptions) {
59 o.ToBranch = branch
60 }
61}
62
63type pushOptions struct {
64 ToBranch string
65 Force bool
66}
67
68type PushOption func(*pushOptions)
69
70func WithToBranch(branch string) PushOption {
71 return func(o *pushOptions) {
72 o.ToBranch = branch
73 }
74}
75
76func PushWithForce() PushOption {
77 return func(o *pushOptions) {
78 o.Force = true
79 }
80}
81
gio3af43942024-04-16 08:13:50 +040082type RepoIO interface {
83 RepoFS
84 FullAddress() string
85 Pull() error
gio0eaf2712024-04-14 13:08:46 +040086 CommitAndPush(message string, opts ...PushOption) error
gio308105e2024-04-19 13:12:13 +040087 Do(op DoFn, opts ...DoOption) error
gio3af43942024-04-16 08:13:50 +040088}
89
90type repoFS struct {
91 fs billy.Filesystem
92}
93
gioe72b54f2024-04-22 10:44:41 +040094func NewBillyRepoFS(fs billy.Filesystem) RepoFS {
95 return &repoFS{fs}
96}
97
gio3af43942024-04-16 08:13:50 +040098func (r *repoFS) Reader(path string) (io.ReadCloser, error) {
99 return r.fs.Open(path)
100}
101
102func (r *repoFS) Writer(path string) (io.WriteCloser, error) {
103 if err := r.fs.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
104 return nil, err
105 }
106 return r.fs.Create(path)
107}
108
109func (r *repoFS) CreateDir(path string) error {
110 return r.fs.MkdirAll(path, fs.ModePerm)
111}
112
113func (r *repoFS) RemoveDir(path string) error {
114 if err := util.RemoveAll(r.fs, path); err != nil {
115 if errors.Is(err, fs.ErrNotExist) {
116 return nil
117 }
118 return err
119 }
120 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400121}
122
123type repoIO struct {
gio3af43942024-04-16 08:13:50 +0400124 *repoFS
gioe72b54f2024-04-22 10:44:41 +0400125 repo *Repository
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400126 signer ssh.Signer
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400127 l sync.Locker
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400128}
129
gioe72b54f2024-04-22 10:44:41 +0400130func NewRepoIO(repo *Repository, signer ssh.Signer) (RepoIO, error) {
gio3af43942024-04-16 08:13:50 +0400131 wt, err := repo.Worktree()
132 if err != nil {
133 return nil, err
134 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400135 return &repoIO{
gio3af43942024-04-16 08:13:50 +0400136 &repoFS{wt.Filesystem},
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400137 repo,
138 signer,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400139 &sync.Mutex{},
gio3af43942024-04-16 08:13:50 +0400140 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400141}
142
gio3af43942024-04-16 08:13:50 +0400143func (r *repoIO) FullAddress() string {
144 return r.repo.Addr.FullAddress()
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400145}
146
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400147func (r *repoIO) Pull() error {
148 r.l.Lock()
149 defer r.l.Unlock()
150 return r.pullWithoutLock()
151}
152
153func (r *repoIO) pullWithoutLock() error {
154 wt, err := r.repo.Worktree()
155 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400156 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400157 }
gio3cdee592024-04-17 10:15:56 +0400158 err = wt.Pull(&git.PullOptions{
gio0eaf2712024-04-14 13:08:46 +0400159 Auth: auth(r.signer),
160 Force: true,
161 Progress: os.Stdout,
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400162 })
gio3cdee592024-04-17 10:15:56 +0400163 if err == nil {
164 return nil
165 }
166 if errors.Is(err, git.NoErrAlreadyUpToDate) {
167 return nil
168 }
169 // TODO(gio): check `remote repository is empty`
gio0eaf2712024-04-14 13:08:46 +0400170 fmt.Println(err)
gio3cdee592024-04-17 10:15:56 +0400171 return nil
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400172}
173
gio0eaf2712024-04-14 13:08:46 +0400174func (r *repoIO) CommitAndPush(message string, opts ...PushOption) error {
175 var o pushOptions
176 for _, i := range opts {
177 i(&o)
178 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400179 wt, err := r.repo.Worktree()
180 if err != nil {
181 return err
182 }
183 if err := wt.AddGlob("*"); err != nil {
184 return err
185 }
186 if _, err := wt.Commit(message, &git.CommitOptions{
187 Author: &object.Signature{
188 Name: "pcloud-installer",
189 When: time.Now(),
190 },
191 }); err != nil {
192 return err
193 }
gio0eaf2712024-04-14 13:08:46 +0400194 gopts := &git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400195 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400196 Auth: auth(r.signer),
gio0eaf2712024-04-14 13:08:46 +0400197 }
198 if o.ToBranch != "" {
199 gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/heads/master:refs/heads/%s", o.ToBranch))}
200 }
201 if o.Force {
202 gopts.Force = true
203 }
204 return r.repo.Push(gopts)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400205}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400206
gio308105e2024-04-19 13:12:13 +0400207func (r *repoIO) Do(op DoFn, opts ...DoOption) error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400208 r.l.Lock()
209 defer r.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400210 if err := r.pullWithoutLock(); err != nil {
211 return err
212 }
gio308105e2024-04-19 13:12:13 +0400213 o := &doOptions{}
214 for _, i := range opts {
215 i(o)
216 }
gio3af43942024-04-16 08:13:50 +0400217 if msg, err := op(r); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400218 return err
gio3af43942024-04-16 08:13:50 +0400219 } else {
gio308105e2024-04-19 13:12:13 +0400220 if !o.NoCommit {
gio0eaf2712024-04-14 13:08:46 +0400221 popts := []PushOption{}
222 if o.Force {
223 popts = append(popts, PushWithForce())
224 }
225 if o.ToBranch != "" {
226 popts = append(popts, WithToBranch(o.ToBranch))
227 }
228 return r.CommitAndPush(msg, popts...)
gio308105e2024-04-19 13:12:13 +0400229 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400230 }
gio308105e2024-04-19 13:12:13 +0400231 return nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400232}
233
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400234func auth(signer ssh.Signer) *gitssh.PublicKeys {
235 return &gitssh.PublicKeys{
236 Signer: signer,
237 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
238 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
239 // TODO(giolekva): verify server public key
240 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
241 return nil
242 },
243 },
244 }
245}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400246
gio3af43942024-04-16 08:13:50 +0400247func ReadYaml[T any](repo RepoFS, path string, o *T) error {
248 r, err := repo.Reader(path)
249 if err != nil {
250 return err
251 }
252 defer r.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400253 if contents, err := ioutil.ReadAll(r); err != nil {
254 return err
255 } else {
256 return yaml.UnmarshalStrict(contents, o)
257 }
258}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400259
gio3af43942024-04-16 08:13:50 +0400260func WriteYaml(repo RepoFS, path string, data any) error {
gioe72b54f2024-04-22 10:44:41 +0400261 if d, ok := data.(*pio.Kustomization); ok {
gio3af43942024-04-16 08:13:50 +0400262 data = d
263 }
264 out, err := repo.Writer(path)
265 if err != nil {
266 return err
267 }
268 serialized, err := yaml.Marshal(data)
269 if err != nil {
270 return err
271 }
272 if _, err := out.Write(serialized); err != nil {
273 return err
274 }
275 return nil
276}
277
gio308105e2024-04-19 13:12:13 +0400278func ReadJson[T any](repo RepoFS, path string, o *T) error {
279 r, err := repo.Reader(path)
280 if err != nil {
281 return err
282 }
283 defer r.Close()
284 return json.NewDecoder(r).Decode(o)
285}
286
287func WriteJson(repo RepoFS, path string, data any) error {
gioe72b54f2024-04-22 10:44:41 +0400288 if d, ok := data.(*pio.Kustomization); ok {
gio308105e2024-04-19 13:12:13 +0400289 data = d
290 }
291 w, err := repo.Writer(path)
292 if err != nil {
293 return err
294 }
295 e := json.NewEncoder(w)
296 e.SetIndent("", "\t")
297 return e.Encode(data)
298}
299
gioe72b54f2024-04-22 10:44:41 +0400300func ReadKustomization(repo RepoFS, path string) (*pio.Kustomization, error) {
301 ret := &pio.Kustomization{}
gio3af43942024-04-16 08:13:50 +0400302 if err := ReadYaml(repo, path, &ret); err != nil {
303 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400304 }
305 return ret, nil
306}
gio0eaf2712024-04-14 13:08:46 +0400307
308func ReadFile(repo RepoFS, path string) ([]byte, error) {
309 r, err := repo.Reader(path)
310 if err != nil {
311 return nil, err
312 }
313 defer r.Close()
314 return io.ReadAll(r)
315}