Skip to content

Commit 4f910cb

Browse files
committed
feat[installer]: sort versions before updating. Add uninstall parameter
1 parent a69ea9d commit 4f910cb

7 files changed

Lines changed: 236 additions & 9 deletions

File tree

installer/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ func main() {
3737
v := fmt.Sprintf("%s-%s", version.Version, version.Edition)
3838
fmt.Println("UTMStack version:", v)
3939

40+
case "--uninstall", "-u":
41+
err := Uninstall()
42+
if err != nil {
43+
fmt.Printf("\nerror uninstalling UTMStack: %v", err)
44+
os.Exit(1)
45+
}
46+
4047
default:
4148
help()
4249
}
@@ -55,5 +62,6 @@ func help() {
5562
fmt.Println("Arguments:")
5663
fmt.Println(" --help, -h Show this help")
5764
fmt.Println(" --install, -i <ID> Install UTMStack (Requires organization ID)")
65+
fmt.Println(" --uninstall, -u Uninstall UTMStack")
5866
fmt.Println(" --version, -v Show UTMStack version")
5967
}

installer/uninstall.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"time"
7+
8+
"github.com/utmstack/UTMStack/installer/utils"
9+
)
10+
11+
func Uninstall() error {
12+
fmt.Println("### Uninstalling UTMStack ###")
13+
14+
fmt.Print("Checking if UTMStack is installed")
15+
isInstalled, err := utils.CheckIfServiceIsInstalled("UTMStackComponentsUpdater")
16+
if err != nil {
17+
return fmt.Errorf("error checking if service is installed: %v", err)
18+
}
19+
20+
if !isInstalled {
21+
fmt.Println(" [NOT FOUND]")
22+
fmt.Println("UTMStack service is not installed on this system.")
23+
24+
fmt.Print("Checking if Docker Swarm stack exists")
25+
err := utils.RunCmd("docker", "stack", "ls", "--format", "{{.Name}}")
26+
if err == nil {
27+
output, _ := utils.RunCmdWithOutput("docker", "stack", "ls", "--format", "{{.Name}}")
28+
if contains(output, "utmstack") {
29+
fmt.Println(" [FOUND]")
30+
fmt.Print("Removing UTMStack Docker Swarm stack")
31+
if err := utils.RunCmd("docker", "stack", "rm", "utmstack"); err != nil {
32+
fmt.Printf(" [ERROR]\nerror removing stack: %v\n", err)
33+
} else {
34+
fmt.Println(" [OK]")
35+
}
36+
} else {
37+
fmt.Println(" [NOT FOUND]")
38+
}
39+
} else {
40+
fmt.Println(" [NOT FOUND]")
41+
}
42+
43+
cleanupFolders()
44+
return nil
45+
}
46+
fmt.Println(" [OK]")
47+
48+
fmt.Print("Stopping UTMStackComponentsUpdater service")
49+
err = utils.StopService("UTMStackComponentsUpdater")
50+
if err != nil {
51+
fmt.Printf(" [ERROR]\nerror stopping service: %v\n", err)
52+
} else {
53+
fmt.Println(" [OK]")
54+
}
55+
56+
time.Sleep(2 * time.Second)
57+
58+
fmt.Print("Uninstalling UTMStackComponentsUpdater service")
59+
err = utils.UninstallService("UTMStackComponentsUpdater")
60+
if err != nil {
61+
return fmt.Errorf("error uninstalling service: %v", err)
62+
}
63+
fmt.Println(" [OK]")
64+
65+
fmt.Print("Removing UTMStack Docker Swarm stack")
66+
err = utils.RunCmd("docker", "stack", "rm", "utmstack")
67+
if err != nil {
68+
fmt.Printf(" [WARNING]\nerror removing stack (stack might not exist): %v\n", err)
69+
} else {
70+
fmt.Println(" [OK]")
71+
72+
fmt.Println("Waiting for services to be removed...")
73+
time.Sleep(30 * time.Second)
74+
75+
fmt.Print("Cleaning up Docker system")
76+
if err := utils.RunCmd("docker", "system", "prune", "-f", "--volumes"); err != nil {
77+
fmt.Printf(" [WARNING]\nerror pruning docker system: %v\n", err)
78+
} else {
79+
fmt.Println(" [OK]")
80+
}
81+
}
82+
83+
cleanupFolders()
84+
85+
fmt.Println("\n### UTMStack has been uninstalled successfully ###")
86+
fmt.Println("Note: The following items were NOT removed:")
87+
fmt.Println(" - Docker installation")
88+
fmt.Println(" - System packages (nginx, vlan, etc.)")
89+
fmt.Println(" - UTMStack data directory (/utmstack)")
90+
fmt.Println("\nIf you want to completely remove all data, you can manually delete:")
91+
fmt.Println(" - /utmstack directory")
92+
fmt.Println(" - /root/utmstack.yml configuration file")
93+
94+
return nil
95+
}
96+
97+
func cleanupFolders() {
98+
fmt.Print("Removing /utmstack/updates folder")
99+
updatesPath := "/utmstack/updates"
100+
if _, err := os.Stat(updatesPath); err == nil {
101+
if err := os.RemoveAll(updatesPath); err != nil {
102+
fmt.Printf(" [WARNING]\nerror removing updates folder: %v\n", err)
103+
} else {
104+
fmt.Println(" [OK]")
105+
}
106+
} else {
107+
fmt.Println(" [NOT FOUND]")
108+
}
109+
}
110+
111+
func contains(slice []string, item string) bool {
112+
for _, s := range slice {
113+
if s == item {
114+
return true
115+
}
116+
}
117+
return false
118+
}

installer/updater/client.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ func (c *UpdaterClient) CheckUpdate() error {
120120
return fmt.Errorf("error getting current version: %v", err)
121121
}
122122

123-
for _, update := range updates {
123+
sortedUpdates := SortVersions(updates)
124+
125+
for _, update := range sortedUpdates {
124126
if update["version"] != currentVersion.Version {
125127
err := c.UpdateToNewVersion(update["version"], update["edition"], update["changelog"])
126128
if err != nil {

installer/updater/logprocessor.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"syscall"
1313
"time"
1414

15-
"github.com/docker/docker/api/types"
1615
"github.com/docker/docker/api/types/container"
1716
"github.com/docker/docker/client"
1817
"github.com/utmstack/UTMStack/installer/config"
@@ -99,7 +98,7 @@ func CollectAndShipSwarmLogs() error {
9998
func createZip(
10099
ctx context.Context,
101100
cli *client.Client,
102-
containers []types.Container,
101+
containers []container.Summary,
103102
zipPath string,
104103
) error {
105104
file, err := os.Create(zipPath)

installer/updater/schemas.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ type InstanceDTOOutput struct {
1414
}
1515

1616
type InstanceDTOInput struct {
17-
Name string `json:"name"`
18-
Country string `json:"country"`
19-
Email string `json:"email"`
20-
Version string `json:"version"`
21-
Edition string `json:"edition"`
22-
CurrentIp string `json:"current_ip"`
17+
Name string `json:"name"`
18+
Country string `json:"country"`
19+
Email string `json:"email"`
20+
Version string `json:"version"`
21+
Edition string `json:"edition"`
22+
CurrentIp string `json:"current_ip"`
23+
MappingName string `json:"mapping_name,omitempty"`
2324
}
2425

2526
type InstanceInfo struct {

installer/updater/versions.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"regexp"
9+
"strconv"
910
"strings"
1011
"sync"
1112

@@ -107,3 +108,81 @@ func IsEnterpriseImage(serviceName string) (bool, error) {
107108
image := strings.TrimSpace(outBuf.String())
108109
return strings.Contains(image, "-enterprise"), nil
109110
}
111+
112+
// Version sorting functions
113+
type Version struct {
114+
Major int
115+
Minor int
116+
Patch int
117+
PrereleaseName string // alpha, beta, rc, or empty
118+
PrereleaseNum int // the number after alpha.X, beta.X, rc.X
119+
Original string
120+
}
121+
122+
func ParseVersion(versionStr string) Version {
123+
v := Version{Original: versionStr}
124+
125+
// Remove v prefix if present
126+
versionStr = strings.TrimPrefix(versionStr, "v")
127+
versionStr = strings.TrimPrefix(versionStr, "V")
128+
129+
// Parse version with regex: X.Y.Z or X.Y.Z-type.num
130+
re := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d+))?$`)
131+
matches := re.FindStringSubmatch(versionStr)
132+
133+
if len(matches) > 1 {
134+
v.Major, _ = strconv.Atoi(matches[1])
135+
}
136+
if len(matches) > 2 {
137+
v.Minor, _ = strconv.Atoi(matches[2])
138+
}
139+
if len(matches) > 3 {
140+
v.Patch, _ = strconv.Atoi(matches[3])
141+
}
142+
if len(matches) > 4 && matches[4] != "" {
143+
v.PrereleaseName = matches[4]
144+
}
145+
if len(matches) > 5 && matches[5] != "" {
146+
v.PrereleaseNum, _ = strconv.Atoi(matches[5])
147+
}
148+
149+
return v
150+
}
151+
152+
func CompareVersions(v1, v2 Version) int {
153+
// Compare major.minor.patch first
154+
if v1.Major != v2.Major {
155+
return v1.Major - v2.Major
156+
}
157+
if v1.Minor != v2.Minor {
158+
return v1.Minor - v2.Minor
159+
}
160+
if v1.Patch != v2.Patch {
161+
return v1.Patch - v2.Patch
162+
}
163+
164+
if v1.PrereleaseNum != v2.PrereleaseNum {
165+
return v1.PrereleaseNum - v2.PrereleaseNum
166+
}
167+
168+
return 0
169+
}
170+
171+
func SortVersions(versions []map[string]string) []map[string]string {
172+
if len(versions) <= 1 {
173+
return versions
174+
}
175+
176+
for i := range len(versions) - 1 {
177+
for j := range len(versions) - i - 1 {
178+
v1 := ParseVersion(versions[j]["version"])
179+
v2 := ParseVersion(versions[j+1]["version"])
180+
181+
if CompareVersions(v1, v2) > 0 {
182+
versions[j], versions[j+1] = versions[j+1], versions[j]
183+
}
184+
}
185+
}
186+
187+
return versions
188+
}

installer/utils/os.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@ func RunCmd(command string, arg ...string) error {
4949
return RunEnvCmd([]string{}, command, arg...)
5050
}
5151

52+
func RunCmdWithOutput(command string, arg ...string) ([]string, error) {
53+
cmd := exec.Command(command, arg...)
54+
55+
output, err := cmd.Output()
56+
if err != nil {
57+
return nil, fmt.Errorf("error running command: %v", err)
58+
}
59+
60+
lines := bytes.Split(output, []byte("\n"))
61+
result := make([]string, 0, len(lines))
62+
for _, line := range lines {
63+
trimmed := bytes.TrimSpace(line)
64+
if len(trimmed) > 0 {
65+
result = append(result, string(trimmed))
66+
}
67+
}
68+
69+
return result, nil
70+
}
71+
5272
func MakeDir(mode os.FileMode, arg ...string) string {
5373
path := ""
5474
for _, folder := range arg {

0 commit comments

Comments
 (0)