blob: 458f6889d5010f45a3db4b8c7e4f007e99ab5efe [file] [log] [blame]
package soft
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"io/ioutil"
"net"
"os"
"path/filepath"
"sync"
"time"
pio "github.com/giolekva/pcloud/core/installer/io"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/util"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/object"
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
"golang.org/x/crypto/ssh"
"sigs.k8s.io/yaml"
)
type RepoFS interface {
Reader(path string) (io.ReadCloser, error)
Writer(path string) (io.WriteCloser, error)
CreateDir(path string) error
RemoveDir(path string) error
ListDir(path string) ([]os.FileInfo, error)
}
type DoFn func(r RepoFS) (string, error)
type doOptions struct {
NoPull bool
NoCommit bool
Force bool
ToBranch string
NoLock bool
}
type DoOption func(*doOptions)
func WithNoLock() DoOption {
return func(o *doOptions) {
o.NoLock = true
}
}
func WithNoPull() DoOption {
return func(o *doOptions) {
o.NoPull = true
}
}
func WithNoCommit() DoOption {
return func(o *doOptions) {
o.NoCommit = true
}
}
func WithForce() DoOption {
return func(o *doOptions) {
o.Force = true
}
}
func WithCommitToBranch(branch string) DoOption {
return func(o *doOptions) {
o.ToBranch = branch
}
}
type pushOptions struct {
ToBranch string
Force bool
}
type PushOption func(*pushOptions)
func WithToBranch(branch string) PushOption {
return func(o *pushOptions) {
o.ToBranch = branch
}
}
func PushWithForce() PushOption {
return func(o *pushOptions) {
o.Force = true
}
}
type RepoIO interface {
RepoFS
FullAddress() string
Pull() error
CommitAndPush(message string, opts ...PushOption) (string, error)
Do(op DoFn, opts ...DoOption) (string, error)
}
type repoFS struct {
fs billy.Filesystem
}
func NewBillyRepoFS(fs billy.Filesystem) RepoFS {
return &repoFS{fs}
}
func (r *repoFS) Reader(path string) (io.ReadCloser, error) {
return r.fs.Open(path)
}
func (r *repoFS) Writer(path string) (io.WriteCloser, error) {
if err := r.CreateDir(filepath.Dir(path)); err != nil {
return nil, err
}
return r.fs.Create(path)
}
func (r *repoFS) CreateDir(path string) error {
return r.fs.MkdirAll(path, fs.ModePerm)
}
func (r *repoFS) RemoveDir(path string) error {
if err := util.RemoveAll(r.fs, path); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}
return nil
}
func (r *repoFS) ListDir(path string) ([]os.FileInfo, error) {
return r.fs.ReadDir(path)
}
type repoIO struct {
*repoFS
repo *Repository
signer ssh.Signer
l sync.Locker
}
func NewRepoIO(repo *Repository, signer ssh.Signer) (RepoIO, error) {
wt, err := repo.Worktree()
if err != nil {
return nil, err
}
return &repoIO{
&repoFS{wt.Filesystem},
repo,
signer,
&sync.Mutex{},
}, nil
}
func (r *repoIO) FullAddress() string {
return r.repo.Addr.FullAddress()
}
func (r *repoIO) Pull() error {
r.l.Lock()
defer r.l.Unlock()
return r.pullWithoutLock()
}
func (r *repoIO) pullWithoutLock() error {
wt, err := r.repo.Worktree()
if err != nil {
return nil
}
err = wt.Pull(&git.PullOptions{
Auth: auth(r.signer),
Force: true,
Progress: os.Stdout,
})
if err == nil {
return nil
}
if errors.Is(err, git.NoErrAlreadyUpToDate) {
return nil
}
// TODO(gio): check `remote repository is empty`
fmt.Printf("-- GIT PULL: %s\n", err.Error())
return nil
}
func (r *repoIO) CommitAndPush(message string, opts ...PushOption) (string, error) {
var o pushOptions
for _, i := range opts {
i(&o)
}
wt, err := r.repo.Worktree()
if err != nil {
return "", err
}
if err := wt.AddGlob("*"); err != nil {
return "", err
}
st, err := wt.Status()
if err != nil {
return "", err
}
if len(st) == 0 {
return "", nil // TODO(gio): maybe return ErrorNothingToCommit
}
fmt.Printf("@@@ %+v\n", st)
hash, err := wt.Commit(message, &git.CommitOptions{
Author: &object.Signature{
Name: "pcloud-installer",
When: time.Now(),
},
})
if err != nil {
return "", err
}
gopts := &git.PushOptions{
RemoteName: "origin",
Auth: auth(r.signer),
}
if o.ToBranch != "" {
gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", r.repo.Ref, o.ToBranch))}
}
if o.Force {
gopts.Force = true
}
fmt.Println(3333)
return hash.String(), r.repo.Push(gopts)
}
func (r *repoIO) Do(op DoFn, opts ...DoOption) (string, error) {
o := &doOptions{}
for _, i := range opts {
i(o)
}
if o.NoLock {
r.l.Lock()
defer r.l.Unlock()
}
if !o.NoPull {
if err := r.pullWithoutLock(); err != nil {
return "", err
}
}
msg, err := op(r)
if err != nil {
return "", err
}
if o.NoCommit {
return "", nil
}
popts := []PushOption{}
if o.Force {
popts = append(popts, PushWithForce())
}
if o.ToBranch != "" {
popts = append(popts, WithToBranch(o.ToBranch))
}
fmt.Println(2222)
return r.CommitAndPush(msg, popts...)
}
func auth(signer ssh.Signer) *gitssh.PublicKeys {
return &gitssh.PublicKeys{
Signer: signer,
HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
// TODO(giolekva): verify server public key
// fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
return nil
},
},
}
}
func ReadYaml[T any](repo RepoFS, path string, o *T) error {
r, err := repo.Reader(path)
if err != nil {
return err
}
defer r.Close()
if contents, err := ioutil.ReadAll(r); err != nil {
return err
} else {
return yaml.UnmarshalStrict(contents, o)
}
}
func WriteYaml(repo RepoFS, path string, data any) error {
if d, ok := data.(*pio.Kustomization); ok {
data = d
}
out, err := repo.Writer(path)
if err != nil {
return err
}
defer out.Close()
serialized, err := yaml.Marshal(data)
if err != nil {
return err
}
if _, err := out.Write(serialized); err != nil {
return err
}
return nil
}
func ReadJson[T any](repo RepoFS, path string, o *T) error {
r, err := repo.Reader(path)
if err != nil {
return err
}
defer r.Close()
return json.NewDecoder(r).Decode(o)
}
func WriteJson(repo RepoFS, path string, data any) error {
if d, ok := data.(*pio.Kustomization); ok {
data = d
}
w, err := repo.Writer(path)
if err != nil {
return err
}
e := json.NewEncoder(w)
e.SetIndent("", "\t")
return e.Encode(data)
}
func ReadKustomization(repo RepoFS, path string) (*pio.Kustomization, error) {
ret := &pio.Kustomization{}
if err := ReadYaml(repo, path, &ret); err != nil {
return nil, err
}
return ret, nil
}
func ReadFile(repo RepoFS, path string) ([]byte, error) {
r, err := repo.Reader(path)
if err != nil {
return nil, err
}
defer r.Close()
return io.ReadAll(r)
}
func WriteFile(repo RepoFS, path, contents string) error {
w, err := repo.Writer(path)
if err != nil {
return err
}
defer w.Close()
_, err = io.WriteString(w, contents)
return err
}