Files
streamctl/pkg/streamd/git_storage.go
2024-10-16 22:58:55 +01:00

243 lines
6.0 KiB
Go

package streamd
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"github.com/facebookincubator/go-belt/tool/logger"
"github.com/go-git/go-git/v5/plumbing"
"github.com/xaionaro-go/streamctl/pkg/observability"
"github.com/xaionaro-go/streamctl/pkg/repository"
"github.com/xaionaro-go/streamctl/pkg/streamd/config"
"github.com/xaionaro-go/streamctl/pkg/xsync"
)
const gitRepoPanelConfigFileName = "streampanel.yaml"
const gitCommitterName = "streampanel"
const gitCommitterEmail = "streampanel@noemail.invalid"
func (d *StreamD) sendConfigViaGIT(
ctx context.Context,
) error {
logger.Debugf(ctx, "sendConfigViaGIT")
defer logger.Debugf(ctx, "/sendConfigViaGIT")
return xsync.DoA1R1(ctx, &d.GitSyncerMutex, d.sendConfigViaGITNoLock, ctx)
}
func (d *StreamD) sendConfigViaGITNoLock(
ctx context.Context,
) error {
var newBytes bytes.Buffer
latestSyncCommit := d.Config.GitRepo.LatestSyncCommit
d.Config.GitRepo.LatestSyncCommit = ""
_, err := d.Config.WriteTo(&newBytes)
d.Config.GitRepo.LatestSyncCommit = latestSyncCommit
if err != nil {
return fmt.Errorf("unable to serialize the panel data: %w", err)
}
hash, err := d.GitStorage.Write(ctx, newBytes.Bytes())
if errors.As(err, &repository.ErrNeedsRebase{}) {
d.gitSync(ctx)
return nil
}
if err != nil {
return fmt.Errorf("unable to write the config to the git repo: %w", err)
}
if !hash.IsZero() {
d.setLastKnownGitCommitHash(hash)
if err := d.SaveConfigFunc(ctx, d.Config); err != nil {
return fmt.Errorf("unable to store the new commit hash in the config file: %w", err)
}
}
return nil
}
func (d *StreamD) gitSync(ctx context.Context) {
logger.Debugf(ctx, "gitSync")
defer logger.Debugf(ctx, "/gitSync")
xsync.DoA1(ctx, &d.GitSyncerMutex, d.gitSyncNoLock, ctx)
}
func (d *StreamD) gitSyncNoLock(ctx context.Context) {
logger.Debugf(ctx, "last_known_commit: %s", d.Config.GitRepo.LatestSyncCommit)
err := d.GitStorage.Pull(
ctx,
d.getLastKnownGitCommitHash(),
func(ctx context.Context, commitHash plumbing.Hash, b []byte) {
panelData := config.NewConfig()
_, err := panelData.Read(b)
if err != nil {
d.UI.DisplayError(fmt.Errorf("unable to read panel data: %w", err))
return
}
panelData.GitRepo.LatestSyncCommit = commitHash.String()
d.onConfigUpdateViaGIT(ctx, &panelData)
},
)
if err != nil {
d.UI.DisplayError(fmt.Errorf("unable to sync with the remote GIT repository: %w", err))
}
}
func (d *StreamD) setLastKnownGitCommitHash(newHash plumbing.Hash) {
d.Config.GitRepo.LatestSyncCommit = newHash.String()
}
func (d *StreamD) getLastKnownGitCommitHash() plumbing.Hash {
return plumbing.NewHash(d.Config.GitRepo.LatestSyncCommit)
}
func (d *StreamD) onConfigUpdateViaGIT(ctx context.Context, cfg *config.Config) {
d.Config = *cfg
err := d.SaveConfig(ctx)
if err != nil {
d.UI.DisplayError(fmt.Errorf("unable to save data: %w", err))
}
if d.GitInitialized {
d.UI.Restart(
ctx,
"Received an updated config from another device, please restart the application",
)
}
}
func (d *StreamD) initGitIfNeeded(ctx context.Context) {
logger.Debugf(ctx, "initGitIfNeeded: %#+v", d.Config.GitRepo)
if d.Config.GitRepo.Enable != nil && !*d.Config.GitRepo.Enable {
logger.Debugf(ctx, "git is disabled in the configuration")
return
}
newUserData := false
if d.Config.GitRepo.URL == "" || d.Config.GitRepo.PrivateKey == "" {
newUserData = true
}
for {
logger.Debugf(ctx, "attempting to configure a git storage")
if newUserData {
ok, url, privKey, err := d.UI.InputGitUserData(ctx)
if err != nil {
d.UI.DisplayError(fmt.Errorf("unable to input the git user data: %w", err))
continue
}
d.Config.GitRepo.Enable = ptr(ok)
d.Config.GitRepo.URL = url
d.Config.GitRepo.PrivateKey = string(privKey)
if err := d.SaveConfig(ctx); err != nil {
d.UI.DisplayError(err)
}
if !ok {
d.deinitGitStorage(ctx)
return
}
}
logger.Debugf(ctx, "newGitStorage")
gitStorage, err := repository.NewGit(
ctx,
d.Config.GitRepo.URL,
[]byte(d.Config.GitRepo.PrivateKey),
gitRepoPanelConfigFileName,
gitCommitterName,
gitCommitterEmail,
)
if err != nil {
d.UI.DisplayError(fmt.Errorf("unable to sync with the remote GIT repository: %w", err))
if newUserData {
continue
}
d.Config.GitRepo.Enable = ptr(false)
d.Config.GitRepo.URL = ""
d.Config.GitRepo.PrivateKey = ""
return
}
d.GitSyncerMutex.Do(ctx, func() {
d.deinitGitStorage(ctx)
if gitStorage == nil {
panic("gitStorage == nil")
}
d.GitStorage = gitStorage
})
break
}
if d.GitStorage == nil {
panic("d.GitStorage == nil")
}
d.startPeriodicGitSyncer(ctx)
d.GitInitialized = true
}
func (d *StreamD) deinitGitStorage(_ context.Context) {
if d.GitStorage != nil {
err := d.GitStorage.Close()
if err != nil {
d.UI.DisplayError(err)
}
d.GitStorage = nil
}
if d.CancelGitSyncer != nil {
d.CancelGitSyncer()
d.CancelGitSyncer = nil
}
}
func (d *StreamD) startPeriodicGitSyncer(ctx context.Context) {
logger.Debugf(ctx, "startPeriodicGitSyncer")
defer logger.Debugf(ctx, "/startPeriodicGitSyncer")
d.GitSyncerMutex.Do(ctx, func() {
if d.CancelGitSyncer != nil {
logger.Debugf(ctx, "git syncer is already started")
return
}
ctx, d.CancelGitSyncer = context.WithCancel(ctx)
})
d.gitSync(ctx)
observability.Go(ctx, func() {
err := d.sendConfigViaGIT(ctx)
if err != nil {
d.UI.DisplayError(
fmt.Errorf("unable to send the config to the remote git repository: %w", err),
)
}
ticker := time.NewTicker(time.Minute)
for {
select {
case <-ctx.Done():
d.GitSyncerMutex.Do(ctx, func() {
d.CancelGitSyncer = nil
})
return
case <-ticker.C:
}
d.gitSync(ctx)
}
})
}
func (d *StreamD) OBSOLETE_GitRelogin(ctx context.Context) error {
alreadyLoggedIn := d.GitStorage != nil
oldCfg := d.Config.GitRepo
d.Config.GitRepo = config.GitRepoConfig{}
d.initGitIfNeeded(ctx)
if d.GitStorage == nil {
d.Config.GitRepo = oldCfg
if alreadyLoggedIn {
d.initGitIfNeeded(ctx)
}
}
return nil
}