From 068a0b1733ebb71f65c064581d75413f6efea2aa Mon Sep 17 00:00:00 2001 From: Raoul Hecky Date: Mon, 9 Oct 2023 15:46:08 +0200 Subject: [PATCH] More work --- .gitignore | 2 + Makefile | 15 ++- app/app.go | 15 +++ app/update.go | 37 +++++ calaos-container.toml | 11 +- cmd/calaos-os/api/api.go | 230 ++++++++++++++++++++++++++++++++ cmd/calaos-os/calaos-os.go | 141 ++++++++++++++++++++ debian/calaos-container.install | 1 + go.mod | 1 + go.sum | 6 + models/images/images.go | 14 ++ models/models.go | 16 ++- models/update.go | 158 ++++++++++++++++++++++ 13 files changed, 644 insertions(+), 3 deletions(-) create mode 100644 app/update.go create mode 100644 cmd/calaos-os/api/api.go create mode 100644 cmd/calaos-os/calaos-os.go create mode 100644 models/images/images.go create mode 100644 models/update.go diff --git a/.gitignore b/.gitignore index df617da..a9aa736 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ debian/calaos-container.postrm.debhelper debian/calaos-container.substvars debian/debhelper-build-stamp debian/files +calaos-os.releases +calaos-container.dev.toml \ No newline at end of file diff --git a/Makefile b/Makefile index e266dc6..f72b9f2 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,11 @@ GOCMD=go GOTEST=$(GOCMD) test GOVET=$(GOCMD) vet BINARY_NAME=calaos-container +BINARY_NAME_TOOL=calaos-os VERSION?=1.0.0 +TOP_DIR := $(dir $(abspath $(firstword $(MAKEFILE_LIST)))) + GREEN := $(shell tput -Txterm setaf 2) YELLOW := $(shell tput -Txterm setaf 3) WHITE := $(shell tput -Txterm setaf 7) @@ -11,6 +14,7 @@ CYAN := $(shell tput -Txterm setaf 6) RESET := $(shell tput -Txterm sgr0) .PHONY: all test build +.ONESHELL: all: build @@ -27,7 +31,16 @@ help: ## Show this help. }' $(MAKEFILE_LIST) ## Build: -build: ## Build the project and put the output binary in bin/ +build: build-server build-tools ## Build the project and put the output binary in bin/ + @mkdir -p bin + +build-tools: + @mkdir -p bin + @cd cmd/calaos-os + $(GOCMD) build -v -o $(TOP_DIR)/bin/$(BINARY_NAME_TOOL) . + @cd $(TOP_DIR) + +build-server: @mkdir -p bin $(GOCMD) build -v -o bin/$(BINARY_NAME) . diff --git a/app/app.go b/app/app.go index 97cb929..bdadb62 100644 --- a/app/app.go +++ b/app/app.go @@ -99,6 +99,21 @@ func NewApp() (a *AppServer, err error) { return a.apiNetIntfList(c) }) + //Force an update check + api.Get("/update/check", func(c *fiber.Ctx) error { + return a.apiUpdateCheck(c) + }) + + //Get available updates + api.Get("/update/available", func(c *fiber.Ctx) error { + return a.apiUpdateAvail(c) + }) + + //Get currently installed images + api.Get("/update/images", func(c *fiber.Ctx) error { + return a.apiUpdateImages(c) + }) + return } diff --git a/app/update.go b/app/update.go new file mode 100644 index 0000000..e7c9518 --- /dev/null +++ b/app/update.go @@ -0,0 +1,37 @@ +package app + +import ( + "github.com/calaos/calaos-container/config" + "github.com/calaos/calaos-container/models" + "github.com/gofiber/fiber/v2" +) + +func (a *AppServer) apiUpdateCheck(c *fiber.Ctx) (err error) { + + err = models.CheckUpdates() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(models.NewVersions) +} + +func (a *AppServer) apiUpdateAvail(c *fiber.Ctx) (err error) { + return c.Status(fiber.StatusOK).JSON(models.NewVersions) +} + +func (a *AppServer) apiUpdateImages(c *fiber.Ctx) (err error) { + + m, err := models.LoadFromDisk(config.Config.String("general.version_file")) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(m) +} diff --git a/calaos-container.toml b/calaos-container.toml index bd9ea49..fc1e00a 100644 --- a/calaos-container.toml +++ b/calaos-container.toml @@ -1,4 +1,13 @@ [general] #Port to listen for Web port = 8000 -address = "127.0.0.1" \ No newline at end of file +address = "127.0.0.1" + +# URL for releases +url_releases = "https://releases.calaos.fr/v4/images" +#url_releases = "https://releases.calaos.fr/v4/images-dev" + +version_file = "/etc/calaos-os.releases" + +#duration between update checks +update_time = "12h" diff --git a/cmd/calaos-os/api/api.go b/cmd/calaos-os/api/api.go new file mode 100644 index 0000000..008a3db --- /dev/null +++ b/cmd/calaos-os/api/api.go @@ -0,0 +1,230 @@ +package api + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/calaos/calaos-container/models/images" +) + +const ( + CalaosCtHost = "localhost:8000" +) + +type apiOptions struct { + timeout time.Duration + insecure bool + proxy func(*http.Request) (*url.URL, error) + transport func(*http.Transport) +} + +// Optional parameter, used to configure timeouts on API calls. +func SetTimeout(timeout time.Duration) func(*apiOptions) { + return func(opts *apiOptions) { + opts.timeout = timeout + } +} + +// Optional parameter for testing only. Bypasses all TLS certificate validation. +func SetInsecure() func(*apiOptions) { + return func(opts *apiOptions) { + opts.insecure = true + } +} + +// Optional parameter, used to configure an HTTP Connect proxy +// server for all outbound communications. +func SetProxy(proxy func(*http.Request) (*url.URL, error)) func(*apiOptions) { + return func(opts *apiOptions) { + opts.proxy = proxy + } +} + +// SetTransport enables additional control over the HTTP transport used to connect to the API. +func SetTransport(transport func(*http.Transport)) func(*apiOptions) { + return func(opts *apiOptions) { + opts.transport = transport + } +} + +type CalaosApi struct { + host string + userAgent string + apiClient *http.Client +} + +// SetCustomHTTPClient allows one to set a completely custom http client that +// will be used to make network calls to the duo api +func (capi *CalaosApi) SetCustomHTTPClient(c *http.Client) { + capi.apiClient = c +} + +// Build and return a CalaosApi struct +func NewCalaosApi(host string, options ...func(*apiOptions)) *CalaosApi { + opts := apiOptions{ + proxy: http.ProxyFromEnvironment, + } + for _, o := range options { + o(&opts) + } + + tr := &http.Transport{ + Proxy: opts.proxy, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: opts.insecure, + }, + } + if opts.transport != nil { + opts.transport(tr) + } + + return &CalaosApi{ + host: host, + userAgent: "calaos-os_api/1.0", + apiClient: &http.Client{ + Timeout: opts.timeout, + Transport: tr, + }, + } +} + +// Make a CalaosApi Rest API call +// Example: api.Call("POST", "/xxxxx/xxxxx", nil) +func (capi *CalaosApi) call(method string, uri string, params url.Values, body interface{}) (*http.Response, []byte, error) { + + url := url.URL{ + Scheme: "http", + Host: capi.host, + Path: uri, + RawQuery: params.Encode(), + } + headers := make(map[string]string) + headers["User-Agent"] = capi.userAgent + + var requestBody io.ReadCloser = nil + if body != nil { + headers["Content-Type"] = "application/json" + + b, err := json.Marshal(body) + if err != nil { + return nil, nil, err + } + requestBody = io.NopCloser(bytes.NewReader(b)) + } + + return capi.makeHttpCall(method, url, headers, requestBody) +} + +// Make a CalaosApi Rest API call using token +// Example: api.CallWithToken("GET", "/api/xxxx", token, params) +func (capi *CalaosApi) callWithToken(method string, uri string, token string, params url.Values, body interface{}) (*http.Response, []byte, error) { + + url := url.URL{ + Scheme: "http", + Host: capi.host, + Path: uri, + RawQuery: params.Encode(), + } + headers := make(map[string]string) + headers["User-Agent"] = capi.userAgent + headers["Authorization"] = "Bearer " + token + + var requestBody io.ReadCloser = nil + if body != nil { + headers["Content-Type"] = "application/json" + + b, err := json.Marshal(body) + if err != nil { + return nil, nil, err + } + requestBody = io.NopCloser(bytes.NewReader(b)) + } + + return capi.makeHttpCall(method, url, headers, requestBody) +} + +func (capi *CalaosApi) makeHttpCall( + method string, + url url.URL, + headers map[string]string, + body io.ReadCloser) (*http.Response, []byte, error) { + + request, err := http.NewRequest(method, url.String(), nil) + if err != nil { + return nil, nil, err + } + + //set headers + for k, v := range headers { + request.Header.Set(k, v) + } + + if body != nil { + request.Body = body + } + + resp, err := capi.apiClient.Do(request) + var bodyBytes []byte + if err != nil { + return resp, bodyBytes, err + } + + bodyBytes, err = io.ReadAll(resp.Body) + defer resp.Body.Close() + + return resp, bodyBytes, err +} + +// UpdateCheck forces an update check +func (capi *CalaosApi) UpdateCheck(token string) (imgs *images.ImageMap, err error) { + + _, body, err := capi.callWithToken("GET", "/api/update/check", token, nil, nil) + if err != nil { + return + } + + imgs = &images.ImageMap{} + if err = json.Unmarshal(body, imgs); err != nil { + return nil, fmt.Errorf("UpdateCheck failed: %v", err) + } + + return +} + +// UpdateAvailable returns available updates +func (capi *CalaosApi) UpdateAvailable(token string) (imgs *images.ImageMap, err error) { + + _, body, err := capi.callWithToken("GET", "/api/update/available", token, nil, nil) + if err != nil { + return + } + + imgs = &images.ImageMap{} + if err = json.Unmarshal(body, imgs); err != nil { + return nil, fmt.Errorf("UpdateAvailable failed: %v", err) + } + + return +} + +// UpdateImages returns currently installed images +func (capi *CalaosApi) UpdateImages(token string) (imgs *images.ImageMap, err error) { + + _, body, err := capi.callWithToken("GET", "/api/update/images", token, nil, nil) + if err != nil { + return + } + + imgs = &images.ImageMap{} + if err = json.Unmarshal(body, imgs); err != nil { + return nil, fmt.Errorf("UpdateImages failed: %v", err) + } + + return +} diff --git a/cmd/calaos-os/calaos-os.go b/cmd/calaos-os/calaos-os.go new file mode 100644 index 0000000..b319396 --- /dev/null +++ b/cmd/calaos-os/calaos-os.go @@ -0,0 +1,141 @@ +package main + +//This is the calaos-os CLI tool that interacts with calaos-container backend +//It's main purpose is to start/manage updates from CLI + +import ( + "fmt" + "os" + "strings" + + "github.com/calaos/calaos-container/cmd/calaos-os/api" + "github.com/fatih/color" + cli "github.com/jawher/mow.cli" + "github.com/jedib0t/go-pretty/v6/table" +) + +const ( + CharStar = "\u2737" + CharAbort = "\u2718" + CharCheck = "\u2714" + CharWarning = "\u26A0" + CharArrow = "\u2012\u25b6" + CharVertLine = "\u2502" + + TOKEN_FILE = "/run/calaos/calaos-ct.token" +) + +var ( + blue = color.New(color.FgBlue).SprintFunc() + errorRed = color.New(color.FgRed).SprintFunc() + errorBgRed = color.New(color.BgRed, color.FgBlack).SprintFunc() + green = color.New(color.FgGreen).SprintFunc() + cyan = color.New(color.FgCyan).SprintFunc() + bgCyan = color.New(color.FgWhite).SprintFunc() +) + +func exit(err error, exit int) { + fmt.Println(errorRed(CharAbort), err) + cli.Exit(exit) +} + +func main() { + a := cli.App("calaos-os", "Calaos OS tool") + + a.Spec = "" + + a.Command("list", "list installed images/pkg and updates", cmdList) + a.Command("check-update", "check for any available updates", cmdCheck) + a.Command("upgrade", "update images/pkg to the latest availble", cmdUpgrade) + + if err := a.Run(os.Args); err != nil { + exit(err, 1) + } +} + +func getToken() (string, error) { + content, err := os.ReadFile(TOKEN_FILE) + if err != nil { + return "", fmt.Errorf("unable to read token: %v", err) + } + return strings.TrimSpace(string(content)), nil +} + +func cmdList(cmd *cli.Cmd) { + cmd.Spec = "" + cmd.Action = func() { + a := api.NewCalaosApi(api.CalaosCtHost) + + token, err := getToken() + if err != nil { + exit(err, 1) + } + + imgs, err := a.UpdateImages(token) + if err != nil { + exit(err, 1) + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Image", "Version", "Source"}) + + for _, e := range *imgs { + t.AppendRow(table.Row{ + e.Name, + e.CurrentVerion, + e.Source, + }) + } + + t.SetStyle(table.StyleLight) + t.Render() + } +} + +func cmdCheck(cmd *cli.Cmd) { + cmd.Spec = "" + cmd.Action = func() { + fmt.Printf("%s Checking for updates...\n", CharArrow) + a := api.NewCalaosApi(api.CalaosCtHost) + + token, err := getToken() + if err != nil { + exit(err, 1) + } + + imgs, err := a.UpdateCheck(token) + if err != nil { + exit(err, 1) + } + + if len(*imgs) == 0 { + fmt.Printf("%s Already up to date.\n", CharCheck) + return + } + + fmt.Printf("Updates available:\n") + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Image", "Current version", "New version", "Source"}) + + for _, e := range *imgs { + t.AppendRow(table.Row{ + e.Name, + e.CurrentVerion, + e.Version, + e.Source, + }) + } + + t.SetStyle(table.StyleLight) + t.Render() + } +} + +func cmdUpgrade(cmd *cli.Cmd) { + cmd.Spec = "" + cmd.Action = func() { + } +} diff --git a/debian/calaos-container.install b/debian/calaos-container.install index 43df0ff..65f2832 100644 --- a/debian/calaos-container.install +++ b/debian/calaos-container.install @@ -1,4 +1,5 @@ bin/calaos-container usr/bin +bin/calaos-os usr/bin scripts/start_calaos_home.sh usr/bin calaos-container.toml etc env/calaos.sh etc/profile.d diff --git a/go.mod b/go.mod index 9fc087e..5575560 100644 --- a/go.mod +++ b/go.mod @@ -79,6 +79,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/imdario/mergo v0.3.15 // indirect + github.com/jedib0t/go-pretty/v6 v6.4.8 github.com/jinzhu/copier v0.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index 1203716..7b84418 100644 --- a/go.sum +++ b/go.sum @@ -604,6 +604,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jawher/mow.cli v1.2.0 h1:e6ViPPy+82A/NFF/cfbq3Lr6q4JHKT9tyHwTCcUQgQw= github.com/jawher/mow.cli v1.2.0/go.mod h1:y+pcA3jBAdo/GIZx/0rFjw/K2bVEODP9rfZOfaiq8Ko= +github.com/jedib0t/go-pretty/v6 v6.4.8 h1:HiNzyMSEpsBaduKhmK+CwcpulEeBrTmxutz4oX/oWkg= +github.com/jedib0t/go-pretty/v6 v6.4.8/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -693,6 +695,7 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= @@ -836,6 +839,7 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV 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/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -952,6 +956,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/sylabs/sif/v2 v2.11.1 h1:d09yPukVa8b74wuy+QTA4Is3w8MH0UjO/xlWQUuFzpY= @@ -1281,6 +1286,7 @@ golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220823224334-20c2bfdbfe24/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= diff --git a/models/images/images.go b/models/images/images.go new file mode 100644 index 0000000..a44e322 --- /dev/null +++ b/models/images/images.go @@ -0,0 +1,14 @@ +package images + +type Image struct { + Name string `json:"name"` + Source string `json:"source"` + Version string `json:"version"` + CurrentVerion string `json:"current_version"` +} + +type ImageList struct { + Images []Image `json:"images"` +} + +type ImageMap map[string]Image diff --git a/models/models.go b/models/models.go index c588811..113adff 100644 --- a/models/models.go +++ b/models/models.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "os" + "sync" logger "github.com/calaos/calaos-container/log" + cimg "github.com/calaos/calaos-container/models/images" "github.com/sirupsen/logrus" @@ -16,6 +18,13 @@ import ( var ( logging *logrus.Entry + + quitCheckUpdate chan interface{} + wgDone sync.WaitGroup + muCheck sync.Mutex + + //Stored new available versions + NewVersions cimg.ImageMap ) func init() { @@ -25,12 +34,17 @@ func init() { // Init models func Init() (err error) { + quitCheckUpdate = make(chan interface{}) + go checkForUpdatesLoop() + wgDone.Add(1) + return } // Shutdown models func Shutdown() { - + close(quitCheckUpdate) + wgDone.Wait() } /* diff --git a/models/update.go b/models/update.go new file mode 100644 index 0000000..5b64739 --- /dev/null +++ b/models/update.go @@ -0,0 +1,158 @@ +package models + +import ( + "encoding/json" + "io" + "net/http" + "os" + "time" + + "github.com/calaos/calaos-container/config" + "github.com/calaos/calaos-container/models/images" +) + +func checkForUpdatesLoop() { + defer wgDone.Done() + + //Parse duration from config + updateTime, err := time.ParseDuration(config.Config.String("general.update_time")) + if err != nil { + logging.Fatalf("Failed to parse update_time duration: %v", err) + return + } + + for { + select { + case <-quitCheckUpdate: + logging.Debugln("Exit checkForUpdates goroutine") + return + case <-time.After(updateTime): + if muCheck.TryLock() { + defer muCheck.Unlock() + + checkForUpdates() + } + return + } + } +} + +// CheckUpdates manually check for updates online +func CheckUpdates() error { + muCheck.Lock() + defer muCheck.Unlock() + + return checkForUpdates() +} + +/* +{ + "images": [ + { + "name": "calaos_home", + "image": "ghcr.io/calaos/calaos_home:4.2.6", + "version": "4.2.6" + }, + { + "name": "calaos_base", + "image": "ghcr.io/calaos/calaos_base:4.8.1", + "version": "4.8.1" + } + ] +} +*/ + +func checkForUpdates() error { + logging.Infoln("Checking for updates") + + localImageMap, err := LoadFromDisk(config.Config.String("general.version_file")) + if err != nil { + logging.Errorln("Error loading local JSON:", err) + return err + } + + urlImageMap, err := downloadFromURL(config.Config.String("general.url_releases")) + if err != nil { + logging.Errorln("Error downloading JSON from URL:", err) + return err + } + + NewVersions = compareVersions(localImageMap, urlImageMap) + + logging.Info("New Versions:") + for name, newVersion := range NewVersions { + v, found := localImageMap[name] + localVersion := "N/A" + if found { + localVersion = v.Version + } + logging.Infof("%s: %s --> %s\n", name, localVersion, newVersion.Version) + } + + return nil +} + +func LoadFromDisk(filePath string) (images.ImageMap, error) { + _, err := os.Stat(filePath) + if err != nil { + // File does not exist, return an empty ImageMap without error + return make(images.ImageMap), nil + } + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var imageList images.ImageList + if err := json.Unmarshal(data, &imageList); err != nil { + return nil, err + } + + imageMap := make(images.ImageMap) + for _, img := range imageList.Images { + imageMap[img.Name] = img + } + + return imageMap, nil +} + +func downloadFromURL(url string) (images.ImageMap, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var imageList images.ImageList + if err := json.Unmarshal(data, &imageList); err != nil { + return nil, err + } + + imageMap := make(images.ImageMap) + for _, img := range imageList.Images { + imageMap[img.Name] = img + } + + return imageMap, nil +} + +func compareVersions(localMap, urlMap images.ImageMap) images.ImageMap { + newVersions := make(images.ImageMap) + + for name, urlImage := range urlMap { + localImage, found := localMap[name] + if !found || localImage.Version != urlImage.Version { + img := urlImage + img.CurrentVerion = localImage.Version + newVersions[name] = img + } + } + + return newVersions +}