diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f01104..90839c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,8 @@ jobs: file: ./shidai.Dockerfile push: true tags: ghcr.io/kiracore/sekin/shidai:${{ steps.create_tag.outputs.new_tag }} + build-args: | + VERSION=${{ steps.create_tag.outputs.new_tag }} labels: org.opencontainers.image.authors="kira.network" org.opencontainers.image.url="https://github.com/KiraCore/sekin" diff --git a/.gitignore b/.gitignore index dd8f2c8..805a5c9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ syslog-data/* test/* src/shidai/test/* shidai/* +.vscode \ No newline at end of file diff --git a/src/shidai/go.mod b/src/shidai/go.mod index 7bc2be0..54d872c 100644 --- a/src/shidai/go.mod +++ b/src/shidai/go.mod @@ -84,6 +84,9 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github v17.0.0+incompatible // indirect + github.com/google/go-github/v47 v47.1.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect @@ -139,6 +142,7 @@ require ( golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect diff --git a/src/shidai/go.sum b/src/shidai/go.sum index d97339e..7c38d59 100644 --- a/src/shidai/go.sum +++ b/src/shidai/go.sum @@ -537,6 +537,12 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v47 v47.1.0 h1:Cacm/WxQBOa9lF0FT0EMjZ2BWMetQ1TQfyurn4yF1z8= +github.com/google/go-github/v47 v47.1.0/go.mod h1:VPZBXNbFSJGjyjFRUKo9vZGawTajnWzC/YjGw/oFKi0= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -1394,6 +1400,8 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/src/shidai/internal/api/api.go b/src/shidai/internal/api/api.go index 8124de5..4fd9480 100644 --- a/src/shidai/internal/api/api.go +++ b/src/shidai/internal/api/api.go @@ -1,10 +1,13 @@ package api import ( + "context" + "github.com/gin-gonic/gin" "github.com/kiracore/sekin/src/shidai/internal/commands" "github.com/kiracore/sekin/src/shidai/internal/logger" "github.com/kiracore/sekin/src/shidai/internal/types" + "github.com/kiracore/sekin/src/shidai/internal/update" "go.uber.org/zap" ) @@ -24,8 +27,10 @@ func Serve() { router.GET("/status", infraStatus()) router.GET("/dashboard", getDashboardHandler()) - go backgroundUpdate() + updateContext := context.Background() + go backgroundUpdate() + go update.UpdateRunner(updateContext) if err := router.Run(":8282"); err != nil { log.Error("Failed to start the server", zap.Error(err)) } diff --git a/src/shidai/internal/dashboard/dashboard.go b/src/shidai/internal/dashboard/dashboard.go deleted file mode 100644 index addd498..0000000 --- a/src/shidai/internal/dashboard/dashboard.go +++ /dev/null @@ -1,87 +0,0 @@ -package dashboard - -import ( - "encoding/json" - "io" - "net/http" - "os" - "time" - - "github.com/kiracore/sekin/src/shidai/internal/logger" - "github.com/kiracore/sekin/src/shidai/internal/types" - "go.uber.org/zap" -) - -var ( - log *zap.Logger = logger.GetLogger() -) - -func fetchAndUpdateDashboard() { - ticker := time.NewTicker(10 * time.Minute) // Update every 10 minutes - defer ticker.Stop() - - for range ticker.C { - newData, err := fetchDataFromEndpoint("https://api.yourdomain.com/dashboard") - if err != nil { - log.Error("Failed to fetch data", zap.Error(err)) - continue - } - - if err := UpdateDashboardData(*newData); err != nil { - log.Error("Failed to update dashboard data", zap.Error(err)) - } - } -} - -func fetchDataFromEndpoint(url string) (*types.Dashboard, error) { - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var newData types.Dashboard - if err := json.Unmarshal(body, &newData); err != nil { - return nil, err - } - - return &newData, nil -} - -func UpdateDashboardData(newData types.Dashboard) error { - lock.Lock() - defer lock.Unlock() - - // Update the cache; this part depends on how newData should be merged with existing data. - dashboardCache = newData - - // Marshal the updated dashboard data into JSON - bytes, err := json.Marshal(dashboardCache) - if err != nil { - log.Error("Failed to marshal dashboard cache", zap.Error(err)) - return err - } - - // Attempt to write the JSON data to the file, with retries - maxRetries := 3 - for i := 0; i < maxRetries; i++ { - if err := os.WriteFile(dataFile, bytes, 0644); err != nil { - log.Error("Failed to write dashboard cache to file", zap.String("file", dataFile), zap.Error(err)) - if i < maxRetries-1 { - log.Info("Retrying file write", zap.Int("attempt", i+2)) - time.Sleep(time.Second) // Simple backoff strategy - continue - } - return err - } - break - } - - log.Info("Dashboard cache updated and saved successfully", zap.String("file", dataFile)) - return nil -} diff --git a/src/shidai/internal/logger/logger.go b/src/shidai/internal/logger/logger.go index a5c0cbb..6b79ce8 100644 --- a/src/shidai/internal/logger/logger.go +++ b/src/shidai/internal/logger/logger.go @@ -4,7 +4,6 @@ import ( "fmt" "log/syslog" "os" - "path/filepath" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -16,20 +15,11 @@ func init() { // File path for backup logging logFilePath := "/syslog-data/syslog-ng/logs/shidai_backup.log" - - logFileFolder := filepath.Dir(logFilePath) - - if _, err := os.Stat(logFileFolder); os.IsNotExist(err) { - err = os.MkdirAll(logFileFolder, 0644) - if err != nil { - panic(fmt.Sprintf("Unable to create logs folder %s. %s", logFileFolder, err.Error())) - } - } - + var logFileErrorCheck bool // Create the log file if it does not exist, or open it in append mode if it does logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - panic("Unable to create/open the log file: " + err.Error()) + logFileErrorCheck = true } // Setting up network syslog writer @@ -45,15 +35,25 @@ func init() { jsonEncoder := zapcore.NewJSONEncoder(encoderConfig) plaintextEncoder := zapcore.NewConsoleEncoder(encoderConfig) - // Create cores for each output - fileCore := zapcore.NewCore(jsonEncoder, zapcore.AddSync(logFile), zap.NewAtomicLevelAt(zapcore.InfoLevel)) - syslogCore := zapcore.NewCore(plaintextEncoder, zapcore.AddSync(syslogWriter), zap.NewAtomicLevelAt(zapcore.DebugLevel)) - - // Combine cores - combinedCore := zapcore.NewTee(fileCore, syslogCore) + var combinedCore zapcore.Core + if !logFileErrorCheck { + fmt.Println("LOGGER: is running with file writer") + // Create cores for each output + fileCore := zapcore.NewCore(jsonEncoder, zapcore.AddSync(logFile), zap.NewAtomicLevelAt(zapcore.InfoLevel)) + syslogCore := zapcore.NewCore(plaintextEncoder, zapcore.AddSync(syslogWriter), zap.NewAtomicLevelAt(zapcore.DebugLevel)) + + // Combine cores + combinedCore = zapcore.NewTee(fileCore, syslogCore) + } else { + fmt.Println("LOGGER: is running without file writer") + stdoutCore := zapcore.NewCore(plaintextEncoder, zapcore.AddSync(os.Stdout), zap.NewAtomicLevelAt(zapcore.DebugLevel)) + syslogCore := zapcore.NewCore(plaintextEncoder, zapcore.AddSync(syslogWriter), zap.NewAtomicLevelAt(zapcore.DebugLevel)) + combinedCore = zapcore.NewTee(syslogCore, stdoutCore) + } // Create the logger with the combined cores log = zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + } func GetLogger() *zap.Logger { return log } diff --git a/src/shidai/internal/types/types.go b/src/shidai/internal/types/types.go index 011a718..68b0ba8 100644 --- a/src/shidai/internal/types/types.go +++ b/src/shidai/internal/types/types.go @@ -6,6 +6,12 @@ import ( ) type ( + SekinPackagesVersion struct { + Sekai string + Interx string + Shidai string + } + InfraFiles map[string]string AppInfo struct { @@ -273,6 +279,8 @@ const ( DirPermRO os.FileMode = 0555 DirPermWR os.FileMode = 0755 + + UPDATER_BIN_PATH = "/updater" ) var ( diff --git a/src/shidai/internal/update/github_helper/githubHelper.go b/src/shidai/internal/update/github_helper/githubHelper.go new file mode 100644 index 0000000..af1feea --- /dev/null +++ b/src/shidai/internal/update/github_helper/githubHelper.go @@ -0,0 +1,9 @@ +package githubhelper + +import "github.com/kiracore/sekin/src/shidai/internal/types" + +type GithubTestHelper struct{} + +func (GithubTestHelper) GetLatestSekinVersion() (*types.SekinPackagesVersion, error) { + return &types.SekinPackagesVersion{Sekai: "v0.3.45", Interx: "v0.4.49", Shidai: "v0.9.0"}, nil +} diff --git a/src/shidai/internal/update/update.go b/src/shidai/internal/update/update.go new file mode 100644 index 0000000..b654446 --- /dev/null +++ b/src/shidai/internal/update/update.go @@ -0,0 +1,226 @@ +package update + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/kiracore/sekin/src/shidai/internal/logger" + "github.com/kiracore/sekin/src/shidai/internal/types" + githubhelper "github.com/kiracore/sekin/src/shidai/internal/update/github_helper" + "go.uber.org/zap" +) + +var log = logger.GetLogger() // Initialize the logger instance at the package level + +const ( + Lower = "LOWER" + Higher = "HIGHER" + Same = "SAME" +) + +type ComparisonResult struct { + Sekai string + Interx string + Shidai string +} + +type Github interface { + GetLatestSekinVersion() (*types.SekinPackagesVersion, error) +} + +// Update check runner (run in goroutine) +func UpdateRunner(ctx context.Context) { + normalUpdateInterval := time.Hour * 24 + errorUpdateInterval := time.Hour * 3 + + ticker := time.NewTicker(normalUpdateInterval) + defer ticker.Stop() + gh := githubhelper.GithubTestHelper{} + + // TODO: should we run update immediately after start or after 24h + // err := UpdateOrUpgrade(gh) + // if err != nil { + // log.Warn("Error when executing update:", zap.Error(err)) + // } + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := UpdateOrUpgrade(gh) + if err != nil { + log.Warn("Error when executing update:", zap.Error(err)) + ticker.Reset(errorUpdateInterval) + } else { + ticker.Reset(normalUpdateInterval) + } + } + + } + +} + +// checks for updates and executes updates if needed (auto-update only for shidai) +func UpdateOrUpgrade(gh Github) error { + log.Info("Checking for update") + latest, err := gh.GetLatestSekinVersion() + if err != nil { + return err + } + + current, err := getCurrentVersions() + if err != nil { + return err + } + + results, err := Compare(current, latest) + if err != nil { + return err + } + + log.Debug("SEKIN VERSIONS:", zap.Any("latest", latest), zap.Any("current", current)) + log.Debug("RESULT:", zap.Any("result", results)) + + if results.Shidai == Lower { + err = executeUpdaterBin() + if err != nil { + return err + } + } else { + log.Info("shidai update not required:", zap.Any("results", results)) + } + return nil +} + +func getCurrentVersions() (*types.SekinPackagesVersion, error) { + out, err := http.Get("http://localhost:8282/status") + if err != nil { + return nil, err + } + defer out.Body.Close() + + b, err := io.ReadAll(out.Body) + if err != nil { + return nil, err + } + var status types.StatusResponse + + err = json.Unmarshal(b, &status) + if err != nil { + // fmt.Println(string(b)) + return nil, err + } + + pkgVersions := types.SekinPackagesVersion{ + Sekai: strings.ReplaceAll(status.Sekai.Version, "\n", ""), + Interx: strings.ReplaceAll(status.Interx.Version, "\n", ""), + Shidai: strings.ReplaceAll(status.Shidai.Version, "\n", ""), + } + + return &pkgVersions, nil +} + +func executeUpdaterBin() error { + cmd := exec.Command(types.UPDATER_BIN_PATH) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to execute binary: %w, output: %s", err, output) + } + return nil +} + +func ParseVersion(version string) (major, minor, patch int, err error) { + parts := strings.TrimPrefix(version, "v") + components := strings.Split(parts, ".") + if len(components) != 3 { + return 0, 0, 0, fmt.Errorf("invalid version format: %s", version) + } + + major, err = strconv.Atoi(components[0]) + if err != nil { + return 0, 0, 0, err + } + + minor, err = strconv.Atoi(components[1]) + if err != nil { + return 0, 0, 0, err + } + + patch, err = strconv.Atoi(components[2]) + if err != nil { + return 0, 0, 0, err + } + + return major, minor, patch, nil +} + +// version has to be in format "v0.4.49" +// CompareVersions compares two version strings and returns 1 if v1 > v2, -1 if v1 < v2, and 0 if they are equal. +// +// if v1 > v2 = higher, if v1 < v2 = lower else equal +func CompareVersions(v1, v2 string) (string, error) { + major1, minor1, patch1, err := ParseVersion(v1) + if err != nil { + return "", err + } + + major2, minor2, patch2, err := ParseVersion(v2) + if err != nil { + return "", err + } + + if major1 > major2 { + return Higher, nil + } else if major1 < major2 { + return Lower, nil + } + + if minor1 > minor2 { + return Higher, nil + } else if minor1 < minor2 { + return Lower, nil + } + + if patch1 > patch2 { + return Higher, nil + } else if patch1 < patch2 { + return Lower, nil + } + + return Same, nil +} + +// version has to be in format "v0.4.49" +// CompareVersions compares two version strings and returns 1 if v1 > v2, -1 if v1 < v2, and 0 if they are equal. +// +// if v1 > v2 = higher, if v1 < v2 = lower else equal +// +// Compare compares two SekinPackagesVersion instances and returns the differences, including version comparison. +func Compare(current, latest *types.SekinPackagesVersion) (ComparisonResult, error) { + var result ComparisonResult + var err error + + result.Sekai, err = CompareVersions(current.Sekai, latest.Sekai) + if err != nil { + return result, err + } + + result.Interx, err = CompareVersions(current.Interx, latest.Interx) + if err != nil { + return result, err + } + + result.Shidai, err = CompareVersions(current.Shidai, latest.Shidai) + if err != nil { + return result, err + } + + return result, nil +} diff --git a/src/updater/cmd/main.go b/src/updater/cmd/main.go new file mode 100644 index 0000000..25a33fa --- /dev/null +++ b/src/updater/cmd/main.go @@ -0,0 +1,10 @@ +package main + +import upgrademanager "github.com/kiracore/sekin/src/updater/internal/upgrade_manager" + +func main() { + err := upgrademanager.GetUpgrade() + if err != nil { + panic(err) + } +} diff --git a/src/updater/go.mod b/src/updater/go.mod new file mode 100644 index 0000000..12e96ce --- /dev/null +++ b/src/updater/go.mod @@ -0,0 +1,35 @@ +module github.com/kiracore/sekin/src/updater + +go 1.22.2 + +require ( + github.com/docker/docker v27.0.3+incompatible + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/sdk v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/time v0.5.0 // indirect + gotest.tools/v3 v3.5.1 // indirect +) diff --git a/src/updater/go.sum b/src/updater/go.sum new file mode 100644 index 0000000..ac3ed53 --- /dev/null +++ b/src/updater/go.sum @@ -0,0 +1,125 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= +github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/src/updater/internal/types/types.go b/src/updater/internal/types/types.go new file mode 100644 index 0000000..404664a --- /dev/null +++ b/src/updater/internal/types/types.go @@ -0,0 +1,25 @@ +package types + +type SekinPackagesVersion struct { + Sekai string + Interx string + Shidai string +} + +type ( + AppInfo struct { + Version string `json:"version"` + Infra bool `json:"infra"` + } + + StatusResponse struct { + Sekai AppInfo `json:"sekai"` + Interx AppInfo `json:"interx"` + Shidai AppInfo `json:"shidai"` + Syslog AppInfo `json:"syslog-ng"` + } +) + +type UpgradePlan struct { + Plan interface{} `json:"plan"` +} diff --git a/src/updater/internal/upgrade_manager/docker/docker.go b/src/updater/internal/upgrade_manager/docker/docker.go new file mode 100644 index 0000000..3aee05b --- /dev/null +++ b/src/updater/internal/upgrade_manager/docker/docker.go @@ -0,0 +1,27 @@ +package docker + +import ( + "context" + "fmt" + + "github.com/docker/docker/client" +) + +func CheckContainerState(cli *client.Client, containerID string) (string, error) { + ctx := context.Background() + + // Get the container's JSON representation + containerJSON, err := cli.ContainerInspect(ctx, containerID) + if err != nil { + return "", fmt.Errorf("error inspecting container: %v", err) + } + + // Check the container state + state := containerJSON.State + if state == nil { + return "", fmt.Errorf("container state is nil") + } + + // Return the container state status + return state.Status, nil +} diff --git a/src/updater/internal/upgrade_manager/docker_compose/dockerCompose.go b/src/updater/internal/upgrade_manager/docker_compose/dockerCompose.go new file mode 100644 index 0000000..4fc0776 --- /dev/null +++ b/src/updater/internal/upgrade_manager/docker_compose/dockerCompose.go @@ -0,0 +1,39 @@ +package dockercompose + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// runs docker-compose -f up -d --no-deps +func DockerComposeUpService(home, composeFilePath string, serviceName ...string) error { + absComposeFilePath, err := filepath.Abs(composeFilePath) + if err != nil { + return fmt.Errorf("could not get absolute path of compose file: %v", err) + } + + cmdArgs := []string{"-f", absComposeFilePath, "up", "-d", "--no-deps", "--remove-orphans"} + cmdArgs = append(cmdArgs, serviceName...) + + log.Printf("Trying to run <%v>", strings.Join(cmdArgs, " ")) + cmd := exec.Command("docker-compose", cmdArgs...) + + if _, err := os.Stat(home); os.IsNotExist(err) { + return fmt.Errorf("home directory does not exist: %v", err) + } + cmd.Dir = home + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Run() + if err != nil { + return fmt.Errorf("error running docker-compose command: %v", err) + } + + return nil +} diff --git a/src/updater/internal/upgrade_manager/update/update.go b/src/updater/internal/upgrade_manager/update/update.go new file mode 100644 index 0000000..ccd95d6 --- /dev/null +++ b/src/updater/internal/upgrade_manager/update/update.go @@ -0,0 +1,191 @@ +package update + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + + "github.com/kiracore/sekin/src/updater/internal/types" + "github.com/kiracore/sekin/src/updater/internal/utils" +) + +const ( + Lower = "LOWER" + Higher = "HIGHER" + Same = "SAME" +) + +type ComparisonResult struct { + Sekai string + Interx string + Shidai string +} + +type GithubTestHelper struct{} + +func (GithubTestHelper) GetLatestSekinVersion() (*types.SekinPackagesVersion, error) { + return &types.SekinPackagesVersion{Sekai: "v0.3.45", Interx: "v0.4.49", Shidai: "v1.9.0"}, nil +} + +func CheckUpgradePlan(path string) (*types.UpgradePlan, error) { + exist := utils.FileExists(path) + if !exist { + return nil, fmt.Errorf("file <%v> does not exist", path) + } + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var plan types.UpgradePlan + err = json.Unmarshal(b, &plan) + if err != nil { + return nil, err + } + return &plan, nil +} + +func CheckShidaiUpdate() (latestShidaiVersion *string, err error) { + log.Println("Checking for update") + gh := GithubTestHelper{} + latest, err := gh.GetLatestSekinVersion() + if err != nil { + return nil, err + } + + current, err := getCurrentVersions() + if err != nil { + return nil, err + } + + results, err := compare(current, latest) + if err != nil { + return nil, err + } + + log.Printf("Version status:\nCurrent: %+v\nLatest: %+v\nResults: %+v", current, latest, results) + + if results.Shidai == Lower { + log.Printf("shidai has newer version: <%v>, current: <%v>\n", latest.Shidai, current.Shidai) + return &latest.Shidai, nil + } else { + log.Printf("shidai is up to date: <%v>\n", latest.Shidai) + return nil, nil + } +} + +func getCurrentVersions() (*types.SekinPackagesVersion, error) { + out, err := http.Get("http://localhost:8282/status") + if err != nil { + return nil, err + } + defer out.Body.Close() + + b, err := io.ReadAll(out.Body) + if err != nil { + return nil, err + } + var status types.StatusResponse + + err = json.Unmarshal(b, &status) + if err != nil { + // fmt.Println(string(b)) + return nil, err + } + + pkgVersions := types.SekinPackagesVersion{ + Sekai: strings.ReplaceAll(status.Sekai.Version, "\n", ""), + Interx: strings.ReplaceAll(status.Interx.Version, "\n", ""), + Shidai: strings.ReplaceAll(status.Shidai.Version, "\n", ""), + } + + return &pkgVersions, nil +} + +// if v1 > v2 = higher, if v1 < v2 = lower else equal +func compare(current, latest *types.SekinPackagesVersion) (ComparisonResult, error) { + var result ComparisonResult + var err error + + result.Sekai, err = compareVersions(current.Sekai, latest.Sekai) + if err != nil { + return result, err + } + + result.Interx, err = compareVersions(current.Interx, latest.Interx) + if err != nil { + return result, err + } + + result.Shidai, err = compareVersions(current.Shidai, latest.Shidai) + if err != nil { + return result, err + } + + return result, nil +} + +// version has to be in format "v0.4.49" +// CompareVersions compares two version strings and returns 1 if v1 > v2, -1 if v1 < v2, and 0 if they are equal. +// +// if v1 > v2 = higher, if v1 < v2 = lower else equal +func compareVersions(v1, v2 string) (string, error) { + major1, minor1, patch1, err := parseVersion(v1) + if err != nil { + return "", err + } + + major2, minor2, patch2, err := parseVersion(v2) + if err != nil { + return "", err + } + + if major1 > major2 { + return Higher, nil + } else if major1 < major2 { + return Lower, nil + } + + if minor1 > minor2 { + return Higher, nil + } else if minor1 < minor2 { + return Lower, nil + } + + if patch1 > patch2 { + return Higher, nil + } else if patch1 < patch2 { + return Lower, nil + } + + return Same, nil +} + +func parseVersion(version string) (major, minor, patch int, err error) { + parts := strings.TrimPrefix(version, "v") + components := strings.Split(parts, ".") + if len(components) != 3 { + return 0, 0, 0, fmt.Errorf("invalid version format: %s", version) + } + + major, err = strconv.Atoi(components[0]) + if err != nil { + return 0, 0, 0, err + } + + minor, err = strconv.Atoi(components[1]) + if err != nil { + return 0, 0, 0, err + } + + patch, err = strconv.Atoi(components[2]) + if err != nil { + return 0, 0, 0, err + } + + return major, minor, patch, nil +} diff --git a/src/updater/internal/upgrade_manager/upgrade/upgrade.go b/src/updater/internal/upgrade_manager/upgrade/upgrade.go new file mode 100644 index 0000000..9823dbd --- /dev/null +++ b/src/updater/internal/upgrade_manager/upgrade/upgrade.go @@ -0,0 +1,281 @@ +package upgrade + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "reflect" + "time" + + "github.com/docker/docker/client" + "github.com/kiracore/sekin/src/updater/internal/types" + "github.com/kiracore/sekin/src/updater/internal/upgrade_manager/docker" + dockercompose "github.com/kiracore/sekin/src/updater/internal/upgrade_manager/docker_compose" + "github.com/kiracore/sekin/src/updater/internal/utils" + "gopkg.in/yaml.v2" +) + +const SekinComposeFileURL_main_branch string = "https://raw.githubusercontent.com/KiraCore/sekin/main/compose.yml" + +const ShidaiServiceName string = "shidai" +const ShidaiContainerName string = "sekin-" + ShidaiServiceName + "-1" + +func ExecuteUpgradePlan(plan *types.UpgradePlan) error { + log.Printf("Executing upgrade plan: %+v", plan) + return nil +} + +func UpgradeShidai(sekinHome, version string) error { + log.Printf("Trying to upgrade shidai, path: <%v>", sekinHome) + + composeFilePath := filepath.Join(sekinHome, "compose.yml") + backupComposeFilePath := filepath.Join(sekinHome, "compose.yml.bak") + + // exist := utils.FileExists(backupComposeFilePath) + // if !exist { + err := utils.CopyFile(composeFilePath, backupComposeFilePath) + if err != nil { + return err + } + // } else { + // log.Printf("WARNING: backup file already exist, configuring from old backup file") + // } + + bakData, err := os.ReadFile(backupComposeFilePath) + if err != nil { + return nil + } + + var bakCompose map[string]interface{} + err = yaml.Unmarshal(bakData, &bakCompose) + if err != nil { + fmt.Println("Error unmarshalling YAML:", err) + return err + } + + currentSekaiImage, err := ReadComposeYMLField(bakCompose, "sekai", "image") + if err != nil { + fmt.Println("Error reading field:", err) + return err + } + log.Printf("sekai image: %s\n", currentSekaiImage) + + currentInterxImage, err := ReadComposeYMLField(bakCompose, "interx", "image") + if err != nil { + fmt.Println("Error reading field:", err) + return err + } + log.Printf("sekai image: %s\n", currentInterxImage) + + err = downloadLatestSekinComposeFile(SekinComposeFileURL_main_branch, composeFilePath) + if err != nil { + return err + } + + latestData, err := os.ReadFile(composeFilePath) + if err != nil { + return nil + } + + var latestCompose map[string]interface{} + err = yaml.Unmarshal(latestData, &latestCompose) + if err != nil { + fmt.Println("Error unmarshalling YAML:", err) + return err + } + + UpdateComposeYMLField(latestCompose, "sekai", "image", currentSekaiImage) + UpdateComposeYMLField(latestCompose, "interx", "image", currentInterxImage) + + updatedData, err := yaml.Marshal(&latestCompose) + if err != nil { + log.Println("Error marshalling YAML:", err) + return err + } + + var originalPerm os.FileMode = 0644 // Default permission if the file doesn't exist + if fileInfo, err := os.Stat(composeFilePath); err == nil { + originalPerm = fileInfo.Mode() + } + + err = os.WriteFile(composeFilePath, updatedData, originalPerm) + if err != nil { + log.Println("Error writing file:", err) + return err + } + diff, err := CompareYAMLFiles(backupComposeFilePath, composeFilePath) + if err != nil { + return err + } + log.Println("DIFF", diff) + + err = dockercompose.DockerComposeUpService(sekinHome, composeFilePath) + if err != nil { + log.Printf("ERROR when trying to run <%v> compose file: %v ", composeFilePath, err) + return err + } + + var check bool = false + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + log.Printf("Error creating Docker client: %v", err) + return err + } + + attempts := 3 + for i := range attempts { + + status, err := docker.CheckContainerState(cli, ShidaiContainerName) + if err != nil { + fmt.Println("Error checking container status:", err) + return err + } + if status == "running!" { + log.Println("Container updated successfully") + check = true + break + } else { + //if not "running" + log.Printf("WARNING: shidai status is %v, trying again. Attempt %v out of %v", status, i+1, attempts) + check = false + time.Sleep(time.Second) + } + } + + if check { + // deleting backup file after successful upgrade + log.Println("SHIDAI updated successfully") + err = utils.DeleteFile(backupComposeFilePath) + if err != nil { + fmt.Println("Error deleting backup file:", err) + return err + } + return nil + } else { + log.Printf("WARNING: unable to update shidai trying to run backup compose file.") + err = dockercompose.DockerComposeUpService(sekinHome, composeFilePath) + if err != nil { + log.Printf("ERROR: when trying to run <%v> backup compose file: %v ", backupComposeFilePath, err) + return fmt.Errorf("ERROR when trying to run <%v> backup compose file: %w", backupComposeFilePath, err) + } + log.Println("Deleting new compose file") + err := utils.DeleteFile(composeFilePath) + if err != nil { + log.Printf("ERROR: when trying to delete old compose file: <%v>. Error: %v ", composeFilePath, err) + return fmt.Errorf("ERROR when trying to delete old compose file: <%v>. Error: %v ", composeFilePath, err) + } + err = utils.RenameFile(backupComposeFilePath, composeFilePath) + if err != nil { + return fmt.Errorf("ERROR: when renaming backup file to old name: %w", err) + } + log.Println("WARNING: unable to run new version of shidai, update rollback to previous version") + return nil + } +} + +func ReadComposeYMLField(compose map[string]interface{}, serviceName, fieldName string) (string, error) { + if services, ok := compose["services"].(map[interface{}]interface{}); ok { + if service, ok := services[serviceName].(map[interface{}]interface{}); ok { + if value, ok := service[fieldName].(string); ok { + return value, nil + } + return "", fmt.Errorf("field %s not found in service %s", fieldName, serviceName) + } + return "", fmt.Errorf("service %s not found", serviceName) + } + return "", fmt.Errorf("services section not found in compose file") +} + +func UpdateComposeYMLField(compose map[string]interface{}, serviceName, fieldName, newValue string) { + if services, ok := compose["services"].(map[interface{}]interface{}); ok { + if service, ok := services[serviceName].(map[interface{}]interface{}); ok { + service[fieldName] = newValue + } + } +} + +func downloadLatestSekinComposeFile(composeFileURL, filepath string) error { + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + resp, err := http.Get(composeFileURL) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + + return nil +} + +// TODO: this is only for testing purpose, delete after +func CompareYAMLFiles(file1Path, file2Path string) ([]string, error) { + file1Data, err := os.ReadFile(file1Path) + if err != nil { + return nil, fmt.Errorf("error reading file1: %v", err) + } + file2Data, err := os.ReadFile(file2Path) + if err != nil { + return nil, fmt.Errorf("error reading file2: %v", err) + } + var file1Map map[interface{}]interface{} + err = yaml.Unmarshal(file1Data, &file1Map) + if err != nil { + return nil, fmt.Errorf("error unmarshalling file1: %v", err) + } + var file2Map map[interface{}]interface{} + err = yaml.Unmarshal(file2Data, &file2Map) + if err != nil { + return nil, fmt.Errorf("error unmarshalling file2: %v", err) + } + differences := compareMaps(file1Map, file2Map, "") + return differences, nil +} +func compareMaps(map1, map2 map[interface{}]interface{}, prefix string) []string { + var differences []string + for key, value1 := range map1 { + keyStr := fmt.Sprintf("%s.%v", prefix, key) + value2, ok := map2[key] + if !ok { + differences = append(differences, fmt.Sprintf("Missing key in file2: %s", keyStr)) + continue + } + + switch v1 := value1.(type) { + case map[interface{}]interface{}: + if v2, ok := value2.(map[interface{}]interface{}); ok { + differences = append(differences, compareMaps(v1, v2, keyStr)...) + } else { + differences = append(differences, fmt.Sprintf("Type mismatch at key: %s", keyStr)) + } + default: + if !reflect.DeepEqual(v1, value2) { + differences = append(differences, fmt.Sprintf("Different value at key: %s (file1: %v, file2: %v)", keyStr, v1, value2)) + } + } + } + + for key := range map2 { + if _, ok := map1[key]; !ok { + keyStr := fmt.Sprintf("%s.%v", prefix, key) + differences = append(differences, fmt.Sprintf("Missing key in file1: %s", keyStr)) + } + } + + return differences +} diff --git a/src/updater/internal/upgrade_manager/upgradeManager.go b/src/updater/internal/upgrade_manager/upgradeManager.go new file mode 100644 index 0000000..a286d26 --- /dev/null +++ b/src/updater/internal/upgrade_manager/upgradeManager.go @@ -0,0 +1,39 @@ +package upgrademanager + +import ( + "github.com/kiracore/sekin/src/updater/internal/upgrade_manager/update" + "github.com/kiracore/sekin/src/updater/internal/upgrade_manager/upgrade" + "github.com/kiracore/sekin/src/updater/internal/utils" +) + +const update_plan string = "./upgradePlan.json" + +const sekin_home string = "/home/km/sekin" + +func GetUpgrade() error { + exist := utils.FileExists(update_plan) + if exist { + plan, err := update.CheckUpgradePlan(update_plan) + if err != nil { + return err + } + defer utils.DeleteFile(update_plan) + err = upgrade.ExecuteUpgradePlan(plan) + if err != nil { + return err + } + } else { + newVersion, err := update.CheckShidaiUpdate() + if err != nil { + return err + } + if newVersion != nil { + err := upgrade.UpgradeShidai(sekin_home, *newVersion) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/src/updater/internal/utils/utils.go b/src/updater/internal/utils/utils.go new file mode 100644 index 0000000..b719104 --- /dev/null +++ b/src/updater/internal/utils/utils.go @@ -0,0 +1,78 @@ +package utils + +import ( + "fmt" + "io" + "log" + "os" +) + +func FileExists(filePath string) bool { + info, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + isFile := !info.IsDir() + return isFile +} + +func DeleteFile(filePath string) error { + log.Printf("attempting to delete file <%v>", filePath) + err := os.Remove(filePath) + if err != nil { + log.Printf("failed to delete file <%v>", filePath) + return fmt.Errorf("failed to delete file %s: %w", filePath, err) + } + + log.Printf("successfully deleted the file <%v>", filePath) + return nil +} + +func CopyFile(src, dst string) error { + // Open the source file + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + // Get the source file's mode (permissions) + sourceFileInfo, err := sourceFile.Stat() + if err != nil { + return err + } + sourceMode := sourceFileInfo.Mode() + + // Create the destination file with the same permissions as the source file + destinationFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, sourceMode) + if err != nil { + return err + } + defer destinationFile.Close() + + // Copy the contents of the source file to the destination file + _, err = io.Copy(destinationFile, sourceFile) + if err != nil { + return err + } + + return nil +} + +func UpdateComposeYmlField(compose map[string]interface{}, serviceName, fieldName, newValue string) { + if services, ok := compose["services"].(map[interface{}]interface{}); ok { + if service, ok := services[serviceName].(map[interface{}]interface{}); ok { + service[fieldName] = newValue + } + } +} + +// RenameFile renames a file from oldName to newName +func RenameFile(oldName, newName string) error { + // Use os.Rename to rename the file + err := os.Rename(oldName, newName) + if err != nil { + return fmt.Errorf("error renaming file: %v", err) + } + return nil +}