blob: 7df75dc08c9781e337bb3cf6360ca8abff7b64af [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 {
39 NoCommit bool
gio0eaf2712024-04-14 13:08:46 +040040 Force bool
41 ToBranch string
gio308105e2024-04-19 13:12:13 +040042}
43
44type DoOption func(*doOptions)
45
46func WithNoCommit() DoOption {
47 return func(o *doOptions) {
48 o.NoCommit = true
49 }
50}
gio3af43942024-04-16 08:13:50 +040051
gio0eaf2712024-04-14 13:08:46 +040052func WithForce() DoOption {
53 return func(o *doOptions) {
54 o.Force = true
55 }
56}
57
58func WithCommitToBranch(branch string) DoOption {
59 return func(o *doOptions) {
60 o.ToBranch = branch
61 }
62}
63
64type pushOptions struct {
65 ToBranch string
66 Force bool
67}
68
69type PushOption func(*pushOptions)
70
71func WithToBranch(branch string) PushOption {
72 return func(o *pushOptions) {
73 o.ToBranch = branch
74 }
75}
76
77func PushWithForce() PushOption {
78 return func(o *pushOptions) {
79 o.Force = true
80 }
81}
82
gio3af43942024-04-16 08:13:50 +040083type RepoIO interface {
84 RepoFS
85 FullAddress() string
86 Pull() error
gio0eaf2712024-04-14 13:08:46 +040087 CommitAndPush(message string, opts ...PushOption) error
gio308105e2024-04-19 13:12:13 +040088 Do(op DoFn, opts ...DoOption) error
gio3af43942024-04-16 08:13:50 +040089}
90
91type repoFS struct {
92 fs billy.Filesystem
93}
94
gioe72b54f2024-04-22 10:44:41 +040095func NewBillyRepoFS(fs billy.Filesystem) RepoFS {
96 return &repoFS{fs}
97}
98
gio3af43942024-04-16 08:13:50 +040099func (r *repoFS) Reader(path string) (io.ReadCloser, error) {
100 return r.fs.Open(path)
101}
102
103func (r *repoFS) Writer(path string) (io.WriteCloser, error) {
104 if err := r.fs.MkdirAll(filepath.Dir(path), fs.ModePerm); err != nil {
105 return nil, err
106 }
107 return r.fs.Create(path)
108}
109
110func (r *repoFS) CreateDir(path string) error {
111 return r.fs.MkdirAll(path, fs.ModePerm)
112}
113
114func (r *repoFS) RemoveDir(path string) error {
115 if err := util.RemoveAll(r.fs, path); err != nil {
116 if errors.Is(err, fs.ErrNotExist) {
117 return nil
118 }
119 return err
120 }
121 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400122}
123
giof8843412024-05-22 16:38:05 +0400124func (r *repoFS) ListDir(path string) ([]os.FileInfo, error) {
125 return r.fs.ReadDir(path)
126}
127
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400128type repoIO struct {
gio3af43942024-04-16 08:13:50 +0400129 *repoFS
gioe72b54f2024-04-22 10:44:41 +0400130 repo *Repository
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400131 signer ssh.Signer
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400132 l sync.Locker
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400133}
134
gioe72b54f2024-04-22 10:44:41 +0400135func NewRepoIO(repo *Repository, signer ssh.Signer) (RepoIO, error) {
gio3af43942024-04-16 08:13:50 +0400136 wt, err := repo.Worktree()
137 if err != nil {
138 return nil, err
139 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400140 return &repoIO{
gio3af43942024-04-16 08:13:50 +0400141 &repoFS{wt.Filesystem},
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400142 repo,
143 signer,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400144 &sync.Mutex{},
gio3af43942024-04-16 08:13:50 +0400145 }, nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400146}
147
gio3af43942024-04-16 08:13:50 +0400148func (r *repoIO) FullAddress() string {
149 return r.repo.Addr.FullAddress()
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400150}
151
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400152func (r *repoIO) Pull() error {
153 r.l.Lock()
154 defer r.l.Unlock()
155 return r.pullWithoutLock()
156}
157
158func (r *repoIO) pullWithoutLock() error {
159 wt, err := r.repo.Worktree()
160 if err != nil {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400161 return nil
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400162 }
gio3cdee592024-04-17 10:15:56 +0400163 err = wt.Pull(&git.PullOptions{
gio0eaf2712024-04-14 13:08:46 +0400164 Auth: auth(r.signer),
165 Force: true,
166 Progress: os.Stdout,
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400167 })
gio3cdee592024-04-17 10:15:56 +0400168 if err == nil {
169 return nil
170 }
171 if errors.Is(err, git.NoErrAlreadyUpToDate) {
172 return nil
173 }
174 // TODO(gio): check `remote repository is empty`
giof5ffedb2024-06-19 14:14:43 +0400175 fmt.Printf("-- GIT PULL: %s\n", err.Error())
gio3cdee592024-04-17 10:15:56 +0400176 return nil
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400177}
178
gio0eaf2712024-04-14 13:08:46 +0400179func (r *repoIO) CommitAndPush(message string, opts ...PushOption) error {
180 var o pushOptions
181 for _, i := range opts {
182 i(&o)
183 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400184 wt, err := r.repo.Worktree()
185 if err != nil {
186 return err
187 }
188 if err := wt.AddGlob("*"); err != nil {
189 return err
190 }
gio03fd0c72024-06-18 12:31:42 +0400191 st, err := wt.Status()
192 if err != nil {
193 return err
194 }
195 if len(st) == 0 {
196 return nil // TODO(gio): maybe return ErrorNothingToCommit
197 }
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400198 if _, err := wt.Commit(message, &git.CommitOptions{
199 Author: &object.Signature{
200 Name: "pcloud-installer",
201 When: time.Now(),
202 },
203 }); err != nil {
204 return err
205 }
gio0eaf2712024-04-14 13:08:46 +0400206 gopts := &git.PushOptions{
Giorgi Lekveishvili87be4ae2023-06-11 23:41:09 +0400207 RemoteName: "origin",
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400208 Auth: auth(r.signer),
gio0eaf2712024-04-14 13:08:46 +0400209 }
210 if o.ToBranch != "" {
211 gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/heads/master:refs/heads/%s", o.ToBranch))}
212 }
213 if o.Force {
214 gopts.Force = true
215 }
216 return r.repo.Push(gopts)
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400217}
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400218
gio308105e2024-04-19 13:12:13 +0400219func (r *repoIO) Do(op DoFn, opts ...DoOption) error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400220 r.l.Lock()
221 defer r.l.Unlock()
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400222 if err := r.pullWithoutLock(); err != nil {
223 return err
224 }
gio308105e2024-04-19 13:12:13 +0400225 o := &doOptions{}
226 for _, i := range opts {
227 i(o)
228 }
gio3af43942024-04-16 08:13:50 +0400229 if msg, err := op(r); err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400230 return err
gio3af43942024-04-16 08:13:50 +0400231 } else {
gio308105e2024-04-19 13:12:13 +0400232 if !o.NoCommit {
gio0eaf2712024-04-14 13:08:46 +0400233 popts := []PushOption{}
234 if o.Force {
235 popts = append(popts, PushWithForce())
236 }
237 if o.ToBranch != "" {
238 popts = append(popts, WithToBranch(o.ToBranch))
239 }
240 return r.CommitAndPush(msg, popts...)
gio308105e2024-04-19 13:12:13 +0400241 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400242 }
gio308105e2024-04-19 13:12:13 +0400243 return nil
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400244}
245
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400246func auth(signer ssh.Signer) *gitssh.PublicKeys {
247 return &gitssh.PublicKeys{
248 Signer: signer,
249 HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
250 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
251 // TODO(giolekva): verify server public key
252 // fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
253 return nil
254 },
255 },
256 }
257}
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400258
gio3af43942024-04-16 08:13:50 +0400259func ReadYaml[T any](repo RepoFS, path string, o *T) error {
260 r, err := repo.Reader(path)
261 if err != nil {
262 return err
263 }
264 defer r.Close()
Giorgi Lekveishvili76951482023-06-30 23:25:09 +0400265 if contents, err := ioutil.ReadAll(r); err != nil {
266 return err
267 } else {
268 return yaml.UnmarshalStrict(contents, o)
269 }
270}
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400271
gio3af43942024-04-16 08:13:50 +0400272func WriteYaml(repo RepoFS, path string, data any) error {
gioe72b54f2024-04-22 10:44:41 +0400273 if d, ok := data.(*pio.Kustomization); ok {
gio3af43942024-04-16 08:13:50 +0400274 data = d
275 }
276 out, err := repo.Writer(path)
277 if err != nil {
278 return err
279 }
280 serialized, err := yaml.Marshal(data)
281 if err != nil {
282 return err
283 }
284 if _, err := out.Write(serialized); err != nil {
285 return err
286 }
287 return nil
288}
289
gio308105e2024-04-19 13:12:13 +0400290func ReadJson[T any](repo RepoFS, path string, o *T) error {
291 r, err := repo.Reader(path)
292 if err != nil {
293 return err
294 }
295 defer r.Close()
296 return json.NewDecoder(r).Decode(o)
297}
298
299func WriteJson(repo RepoFS, path string, data any) error {
gioe72b54f2024-04-22 10:44:41 +0400300 if d, ok := data.(*pio.Kustomization); ok {
gio308105e2024-04-19 13:12:13 +0400301 data = d
302 }
303 w, err := repo.Writer(path)
304 if err != nil {
305 return err
306 }
307 e := json.NewEncoder(w)
308 e.SetIndent("", "\t")
309 return e.Encode(data)
310}
311
gioe72b54f2024-04-22 10:44:41 +0400312func ReadKustomization(repo RepoFS, path string) (*pio.Kustomization, error) {
313 ret := &pio.Kustomization{}
gio3af43942024-04-16 08:13:50 +0400314 if err := ReadYaml(repo, path, &ret); err != nil {
315 return nil, err
Giorgi Lekveishvili4257b902023-07-07 17:08:42 +0400316 }
317 return ret, nil
318}
gio0eaf2712024-04-14 13:08:46 +0400319
320func ReadFile(repo RepoFS, path string) ([]byte, error) {
321 r, err := repo.Reader(path)
322 if err != nil {
323 return nil, err
324 }
325 defer r.Close()
326 return io.ReadAll(r)
327}