Skip to content

Commit 25403bb

Browse files
committed
feat(installer): add self-update capability and improve maintenance window
1 parent cc46958 commit 25403bb

File tree

5 files changed

+134
-18
lines changed

5 files changed

+134
-18
lines changed

installer/config/const.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const (
1515
LogCollectorEndpoint = "/api/v1/logcollectors/upload"
1616
GetLatestVersionEndpoint = "/api/v1/versions/latest"
1717

18+
GitHubReleasesURL = "https://github.com/utmstack/UTMStack/releases/download/%s/installer"
19+
1820
ImagesPath = "/utmstack/images"
1921

2022
RequiredMinCPUCores = 2

installer/install.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func Install(specificVersion string) error {
3030

3131
if isInstalledAlready {
3232
fmt.Println("UTMStack is already installed. If you want to re-install it, please remove the service UTMStackComponentsUpdater first.")
33+
if err := utils.RestartService("UTMStackComponentsUpdater"); err != nil {
34+
return fmt.Errorf("error restarting service: %v", err)
35+
}
3336
return nil
3437
}
3538

installer/updater/client.go

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ func (c *UpdaterClient) UpdateToNewVersion(version, edition, changelog string) e
140140
config.Logger().Info("Updating UTMStack to version %s-%s...", version, edition)
141141
config.Updating = true
142142

143+
// Update installer binary first (only in prod branch)
144+
cnf := config.GetConfig()
145+
if cnf.Branch == "prod" || cnf.Branch == "" {
146+
if err := c.UpdateInstaller(version); err != nil {
147+
config.Logger().ErrorF("error updating installer: %v", err)
148+
}
149+
}
150+
143151
err := docker.StackUP(version + "-" + edition)
144152
if err != nil {
145153
return fmt.Errorf("error updating UTMStack: %v", err)
@@ -153,11 +161,71 @@ func (c *UpdaterClient) UpdateToNewVersion(version, edition, changelog string) e
153161
config.Logger().Info("UTMStack updated to version %s-%s", version, edition)
154162
config.Updating = false
155163

156-
err = utils.RunCmd("docker", "system", "prune", "-f")
164+
err = utils.RunCmd("docker", "image", "prune", "-a", "-f")
165+
if err != nil {
166+
config.Logger().ErrorF("error cleaning up old Docker images after update: %v", err)
167+
}
168+
169+
// Restart service to load new installer binary
170+
if cnf.Branch == "prod" || cnf.Branch == "" {
171+
config.Logger().Info("Restarting service to load new installer binary...")
172+
go func() {
173+
time.Sleep(5 * time.Second)
174+
utils.RestartService("UTMStackComponentsUpdater")
175+
}()
176+
}
177+
178+
return nil
179+
}
180+
181+
func (c *UpdaterClient) UpdateInstaller(version string) error {
182+
config.Logger().Info("Updating installer to version %s...", version)
183+
184+
execPath, err := os.Executable()
185+
if err != nil {
186+
return fmt.Errorf("error getting executable path: %v", err)
187+
}
188+
189+
// Download new installer from GitHub
190+
url := fmt.Sprintf(config.GitHubReleasesURL, version)
191+
resp, err := http.Get(url)
192+
if err != nil {
193+
return fmt.Errorf("error downloading installer from %s: %v", url, err)
194+
}
195+
defer resp.Body.Close()
196+
197+
if resp.StatusCode != http.StatusOK {
198+
return fmt.Errorf("error downloading installer: status %d", resp.StatusCode)
199+
}
200+
201+
// Create temp file
202+
tmpFile, err := os.CreateTemp("", "installer-*")
157203
if err != nil {
158-
config.Logger().ErrorF("error cleaning up Docker system after update: %v", err)
204+
return fmt.Errorf("error creating temp file: %v", err)
205+
}
206+
tmpPath := tmpFile.Name()
207+
208+
// Download to temp file
209+
_, err = io.Copy(tmpFile, resp.Body)
210+
tmpFile.Close()
211+
if err != nil {
212+
os.Remove(tmpPath)
213+
return fmt.Errorf("error writing installer to temp file: %v", err)
214+
}
215+
216+
// Make executable
217+
if err := os.Chmod(tmpPath, 0755); err != nil {
218+
os.Remove(tmpPath)
219+
return fmt.Errorf("error making installer executable: %v", err)
220+
}
221+
222+
// Replace current binary
223+
if err := os.Rename(tmpPath, execPath); err != nil {
224+
os.Remove(tmpPath)
225+
return fmt.Errorf("error replacing installer binary: %v", err)
159226
}
160227

228+
config.Logger().Info("Installer updated successfully to version %s", version)
161229
return nil
162230
}
163231

installer/updater/window.go

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,97 @@
11
package updater
22

33
import (
4+
"encoding/json"
45
"time"
56

6-
"github.com/robfig/cron/v3"
77
"github.com/utmstack/UTMStack/installer/config"
88
)
99

10-
var windowConfig string
10+
type MaintenanceWindow struct {
11+
Days []int `json:"days"` // 0=Sunday, 1=Monday, ..., 6=Saturday
12+
StartTime string `json:"startTime"` // Format: "HH:MM"
13+
EndTime string `json:"endTime"` // Format: "HH:MM"
14+
}
15+
16+
var windowConfig *MaintenanceWindow
1117

1218
func UpdateWindowConfig() {
1319
for {
1420
window, err := getWindowMaintaince()
1521
if err != nil {
16-
// Only log error if it's not a maintenance error
1722
if !IsBackendMaintenanceError(err) {
1823
config.Logger().ErrorF("Error getting maintenance window config: %v", err)
1924
}
20-
// If backend is in maintenance, just skip this iteration silently
2125
}
2226

23-
if window != "" {
27+
if window != nil {
2428
windowConfig = window
25-
config.Logger().Info("Updated maintenance window config: %s", windowConfig)
2629
}
2730

2831
time.Sleep(config.CheckUpdatesEvery)
2932
}
3033
}
3134

3235
func IsInMaintenanceWindow() bool {
33-
if windowConfig == "" {
36+
if windowConfig == nil {
37+
return true
38+
}
39+
40+
if len(windowConfig.Days) == 0 {
41+
return true
42+
}
43+
44+
if windowConfig.StartTime == "" || windowConfig.EndTime == "" {
3445
return true
3546
}
3647

37-
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
48+
startTime, err := time.Parse("15:04", windowConfig.StartTime)
49+
if err != nil {
50+
config.Logger().ErrorF("Error parsing start time %s: %v", windowConfig.StartTime, err)
51+
return true
52+
}
3853

39-
schedule, err := parser.Parse(windowConfig)
54+
endTime, err := time.Parse("15:04", windowConfig.EndTime)
4055
if err != nil {
41-
config.Logger().ErrorF("Error parsing cron expression %s: %v", windowConfig, err)
56+
config.Logger().ErrorF("Error parsing end time %s: %v", windowConfig.EndTime, err)
57+
return true
58+
}
59+
60+
now := time.Now()
61+
62+
currentDay := int(now.Weekday())
63+
dayAllowed := false
64+
for _, day := range windowConfig.Days {
65+
if day == currentDay {
66+
dayAllowed = true
67+
break
68+
}
69+
}
70+
71+
if !dayAllowed {
4272
return false
4373
}
4474

45-
now := time.Now().Truncate(time.Minute)
46-
prev := schedule.Next(now.Add(-1 * time.Minute))
75+
currentTime, _ := time.Parse("15:04", now.Format("15:04"))
4776

48-
return prev.Equal(now)
77+
if startTime.Before(endTime) || startTime.Equal(endTime) {
78+
return !currentTime.Before(startTime) && !currentTime.After(endTime)
79+
}
80+
81+
return !currentTime.Before(startTime) || !currentTime.After(endTime)
4982
}
5083

51-
func getWindowMaintaince() (string, error) {
84+
func getWindowMaintaince() (*MaintenanceWindow, error) {
5285
backConf, err := getConfigFromBackend(8)
5386
if err != nil {
54-
return "", err
87+
return nil, err
88+
}
89+
90+
var window MaintenanceWindow
91+
err = json.Unmarshal([]byte(backConf[0].ConfParamValue), &window)
92+
if err != nil {
93+
return nil, err
5594
}
5695

57-
return backConf[0].ConfParamValue, nil
96+
return &window, nil
5897
}

installer/utils/services.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ func StopService(name string) error {
88
return RunCmd("systemctl", "stop", name)
99
}
1010

11+
func RestartService(name string) error {
12+
return RunCmd("systemctl", "restart", name)
13+
}
14+
1115
func UninstallService(name string) error {
1216
err := RunCmd("systemctl", "disable", name)
1317
if err != nil {

0 commit comments

Comments
 (0)