Adding Initial action (#1)
Co-authored-by: kjuulh <contact@kjuulh.io> Reviewed-on: https://git.front.kjuulh.io/kjuulh/kraken/pulls/1
This commit is contained in:
43
internal/services/actions/action.go
Normal file
43
internal/services/actions/action.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/kraken/internal/services/storage"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Predicate func(ctx context.Context, area *storage.Area) (bool, error)
|
||||
type ActionFunc func(ctx context.Context, area *storage.Area) error
|
||||
|
||||
type Action struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewAction(logger *zap.Logger) *Action {
|
||||
return &Action{logger: logger}
|
||||
}
|
||||
|
||||
func (a *Action) Run(ctx context.Context, area *storage.Area, predicate Predicate, action ActionFunc, dryrun bool) error {
|
||||
matches, err := predicate(ctx, area)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !matches {
|
||||
a.logger.Debug("repo doesn't match, skipping", zap.String("path", area.Path))
|
||||
return nil
|
||||
}
|
||||
|
||||
if dryrun {
|
||||
a.logger.Panic("dryrun selected, but not implemented yet")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = action(ctx, area)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
4
internal/services/jobs/models.go
Normal file
4
internal/services/jobs/models.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package jobs
|
||||
|
||||
type JobId struct {
|
||||
}
|
260
internal/services/providers/git.go
Normal file
260
internal/services/providers/git.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/kraken/internal/services/signer"
|
||||
"git.front.kjuulh.io/kjuulh/kraken/internal/services/storage"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapio"
|
||||
)
|
||||
|
||||
// Git is a native git provider, it can clone, pull
|
||||
// , push and as in abstraction on native git operations
|
||||
type Git struct {
|
||||
logger *zap.Logger
|
||||
gitConfig *GitConfig
|
||||
openPGP *signer.OpenPGP
|
||||
}
|
||||
|
||||
type GitRepo struct {
|
||||
repo *git.Repository
|
||||
}
|
||||
|
||||
type GitAuth string
|
||||
|
||||
const (
|
||||
GIT_AUTH_SSH GitAuth = "ssh"
|
||||
GIT_AUTH_USERNAME_PASSWORD GitAuth = "username_password"
|
||||
GIT_AUTH_ACCESS_TOKEN GitAuth = "access_token"
|
||||
GIT_AUTH_ANONYMOUS GitAuth = "anonymous"
|
||||
GIT_AUTH_SSH_AGENT GitAuth = "ssh_agent"
|
||||
)
|
||||
|
||||
type GitConfig struct {
|
||||
AuthOption GitAuth
|
||||
User string
|
||||
Password string
|
||||
AccessToken string
|
||||
SshPublicKeyFilePath string
|
||||
SshPrivateKeyPassword string
|
||||
}
|
||||
|
||||
func NewGit(logger *zap.Logger, gitConfig *GitConfig, openPGP *signer.OpenPGP) *Git {
|
||||
return &Git{logger: logger, gitConfig: gitConfig, openPGP: openPGP}
|
||||
}
|
||||
|
||||
func (g *Git) Clone(ctx context.Context, storageArea *storage.Area, repoUrl string) (*GitRepo, error) {
|
||||
g.logger.Debug(
|
||||
"cloning repository",
|
||||
zap.String("repoUrl", repoUrl),
|
||||
zap.String("path", storageArea.Path),
|
||||
)
|
||||
|
||||
auth, err := g.GetAuth()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cloneOptions := git.CloneOptions{
|
||||
URL: repoUrl,
|
||||
Auth: auth,
|
||||
RemoteName: "origin",
|
||||
ReferenceName: "refs/heads/main",
|
||||
SingleBranch: true,
|
||||
NoCheckout: false,
|
||||
Depth: 1,
|
||||
RecurseSubmodules: 1,
|
||||
Progress: g.getProgressWriter(),
|
||||
Tags: 0,
|
||||
InsecureSkipTLS: false,
|
||||
CABundle: []byte{},
|
||||
}
|
||||
|
||||
repo, err := git.PlainCloneContext(ctx, storageArea.Path, false, &cloneOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.logger.Debug("done cloning repo")
|
||||
|
||||
return &GitRepo{repo: repo}, nil
|
||||
}
|
||||
|
||||
func (g *Git) getProgressWriter() *zapio.Writer {
|
||||
return &zapio.Writer{
|
||||
Log: g.logger.With(zap.String("process", "go-git")),
|
||||
Level: zap.DebugLevel,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Git) Add(ctx context.Context, storageArea *storage.Area, gitRepo *GitRepo) (*git.Worktree, error) {
|
||||
worktree, err := gitRepo.repo.Worktree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = worktree.AddWithOptions(&git.AddOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g.logger.Debug("git status", zap.String("status", status.String()))
|
||||
|
||||
return worktree, nil
|
||||
}
|
||||
|
||||
func (g *Git) CreateBranch(ctx context.Context, gitRepo *GitRepo) error {
|
||||
worktree, err := gitRepo.repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
refSpec := plumbing.NewBranchReferenceName("kraken-apply")
|
||||
err = gitRepo.repo.CreateBranch(&config.Branch{
|
||||
Name: "kraken-apply",
|
||||
Remote: "origin",
|
||||
Merge: refSpec,
|
||||
Rebase: "",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create branch: %w", err)
|
||||
}
|
||||
|
||||
err = worktree.Checkout(&git.CheckoutOptions{
|
||||
Branch: plumbing.ReferenceName(refSpec.String()),
|
||||
Create: true,
|
||||
Force: false,
|
||||
Keep: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not checkout branch: %w", err)
|
||||
}
|
||||
|
||||
remoteRef := plumbing.NewRemoteReferenceName("origin", "kraken-apply")
|
||||
ref := plumbing.NewSymbolicReference(refSpec, remoteRef)
|
||||
err = gitRepo.repo.Storer.SetReference(ref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not set reference: %w", err)
|
||||
}
|
||||
|
||||
auth, err := g.GetAuth()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = worktree.PullContext(ctx, &git.PullOptions{
|
||||
RemoteName: "origin",
|
||||
ReferenceName: "refs/heads/main",
|
||||
SingleBranch: true,
|
||||
Depth: 1,
|
||||
Auth: auth,
|
||||
RecurseSubmodules: 1,
|
||||
Progress: g.getProgressWriter(),
|
||||
Force: true,
|
||||
InsecureSkipTLS: false,
|
||||
CABundle: []byte{},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not pull from origin: %w", err)
|
||||
}
|
||||
|
||||
g.logger.Debug("done creating branches")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Git) Commit(ctx context.Context, gitRepo *GitRepo) error {
|
||||
worktree, err := gitRepo.repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = worktree.Commit("some-commit", &git.CommitOptions{
|
||||
All: true,
|
||||
Author: &object.Signature{Name: "kraken", Email: "kraken@kasperhermansen.com", When: time.Now()},
|
||||
Committer: &object.Signature{Name: "kraken", Email: "kraken@kasperhermansen.com", When: time.Now()},
|
||||
SignKey: g.openPGP.SigningKey,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.logger.Debug("done commiting objects")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Git) Push(ctx context.Context, gitRepo *GitRepo) error {
|
||||
auth, err := g.GetAuth()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = gitRepo.repo.PushContext(ctx, &git.PushOptions{
|
||||
RemoteName: "origin",
|
||||
RefSpecs: []config.RefSpec{},
|
||||
Auth: auth,
|
||||
Progress: g.getProgressWriter(),
|
||||
Prune: false,
|
||||
Force: false,
|
||||
InsecureSkipTLS: false,
|
||||
CABundle: []byte{},
|
||||
RequireRemoteRefs: []config.RefSpec{},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.logger.Debug("done pushing branch")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Git) GetAuth() (transport.AuthMethod, error) {
|
||||
switch g.gitConfig.AuthOption {
|
||||
case GIT_AUTH_SSH:
|
||||
sshKey, err := ssh.NewPublicKeysFromFile(
|
||||
g.gitConfig.User,
|
||||
g.gitConfig.SshPublicKeyFilePath,
|
||||
g.gitConfig.SshPrivateKeyPassword,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sshKey, nil
|
||||
case GIT_AUTH_USERNAME_PASSWORD:
|
||||
return &http.BasicAuth{
|
||||
Username: g.gitConfig.User,
|
||||
Password: g.gitConfig.Password,
|
||||
}, nil
|
||||
case GIT_AUTH_ACCESS_TOKEN:
|
||||
return &http.BasicAuth{
|
||||
Username: "required-username",
|
||||
Password: g.gitConfig.AccessToken,
|
||||
}, nil
|
||||
case GIT_AUTH_ANONYMOUS:
|
||||
return nil, nil
|
||||
case GIT_AUTH_SSH_AGENT:
|
||||
return ssh.NewSSHAgentAuth(g.gitConfig.User)
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
81
internal/services/signer/openpgp.go
Normal file
81
internal/services/signer/openpgp.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package signer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/curre"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type OpenPGP struct {
|
||||
logger *zap.Logger
|
||||
SigningKey *openpgp.Entity
|
||||
config *OpenPgpConfig
|
||||
}
|
||||
|
||||
type OpenPgpConfig struct {
|
||||
PrivateKeyFilePath string
|
||||
PrivateKeyPassword string
|
||||
PrivateKeyIdentity string
|
||||
}
|
||||
|
||||
func NewOpenPGP(logger *zap.Logger, config *OpenPgpConfig) *OpenPGP {
|
||||
return &OpenPGP{
|
||||
logger: logger,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func NewOpenPGPApp(openPGP *OpenPGP) curre.Component {
|
||||
return curre.NewFunctionalComponent(&curre.FunctionalComponent{
|
||||
InitFunc: func(_ *curre.FunctionalComponent, ctx context.Context) error {
|
||||
keyring, err := buildKeyring(ctx, openPGP)
|
||||
if err != nil {
|
||||
openPGP.logger.Panic("could not build keyring", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
openPGP.SigningKey = keyring
|
||||
|
||||
return nil
|
||||
},
|
||||
StartFunc: func(fc *curre.FunctionalComponent, ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
StopFunc: func(fc *curre.FunctionalComponent, ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func buildKeyring(_ context.Context, openPGP *OpenPGP) (*openpgp.Entity, error) {
|
||||
content, err := os.ReadFile(openPGP.config.PrivateKeyFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := strings.NewReader(string(content))
|
||||
|
||||
es, err := openpgp.ReadArmoredKeyRing(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, key := range es {
|
||||
for k := range key.Identities {
|
||||
if strings.Contains(k, openPGP.config.PrivateKeyIdentity) {
|
||||
err = key.PrivateKey.Decrypt([]byte(openPGP.config.PrivateKeyPassword))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("could not find key matching identity")
|
||||
|
||||
}
|
7
internal/services/storage/models.go
Normal file
7
internal/services/storage/models.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package storage
|
||||
|
||||
type (
|
||||
Area struct {
|
||||
Path string
|
||||
}
|
||||
)
|
77
internal/services/storage/storage.go
Normal file
77
internal/services/storage/storage.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// The idea behind storage is that we have file dir, with a git repo.
|
||||
// This file repo can now take certain actions
|
||||
|
||||
type StorageConfig struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewDefaultStorageConfig() (*StorageConfig, error) {
|
||||
tempDir, err := os.MkdirTemp(os.TempDir(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StorageConfig{
|
||||
Path: path.Join(tempDir, "kraken"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
logger *zap.Logger
|
||||
cfg *StorageConfig
|
||||
}
|
||||
|
||||
func NewService(logger *zap.Logger, cfg *StorageConfig) *Service {
|
||||
return &Service{logger: logger, cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *Service) getStoragePath(ctx context.Context) string {
|
||||
return path.Join(s.cfg.Path, "storage")
|
||||
}
|
||||
|
||||
func (s *Service) InitializeStorage(ctx context.Context) error {
|
||||
return os.MkdirAll(s.getStoragePath(ctx), 0755)
|
||||
}
|
||||
|
||||
func (s *Service) CleanupStorage(ctx context.Context) error {
|
||||
doneRemovingChan := make(chan struct{}, 1)
|
||||
go func(ctx context.Context) {
|
||||
s.logger.Debug("Removing all temp storage")
|
||||
os.RemoveAll(s.getStoragePath(ctx))
|
||||
doneRemovingChan <- struct{}{}
|
||||
}(ctx)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.New("could not cleanup storage aborting")
|
||||
case <-doneRemovingChan:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateArea(ctx context.Context) (*Area, error) {
|
||||
dir, err := os.MkdirTemp(s.getStoragePath(ctx), "*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Area{
|
||||
Path: dir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) RemoveArea(ctx context.Context, area *Area) error {
|
||||
return os.RemoveAll(area.Path)
|
||||
}
|
Reference in New Issue
Block a user