Skip to content

Commit 880eb2a

Browse files
committed
feat[installer]: apply white-label bundle (brand + logo + MSSP license) at install
1 parent e664c81 commit 880eb2a

11 files changed

Lines changed: 293 additions & 72 deletions

File tree

installer/branding/branding.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package branding
2+
3+
import (
4+
"strings"
5+
"sync"
6+
7+
"path/filepath"
8+
9+
"github.com/utmstack/UTMStack/installer/utils"
10+
)
11+
12+
const DefaultName = "UTMStack"
13+
14+
// Brand mirrors the text fields the backend branding accepts; image files
15+
// (logo, logoDark, favicon, reportLogo, reportCover) live alongside brand.yaml
16+
// and are discovered by name (see seed.go).
17+
type Brand struct {
18+
ProductName string `yaml:"productName"`
19+
}
20+
21+
var (
22+
brand *Brand
23+
brandOnce sync.Once
24+
)
25+
26+
func Dir() string {
27+
return filepath.Join(utils.GetMyPath(), "branding")
28+
}
29+
30+
func configPath() string {
31+
return filepath.Join(Dir(), "brand.yaml")
32+
}
33+
34+
func LicensePath() string {
35+
return filepath.Join(utils.GetMyPath(), "LICENSE")
36+
}
37+
38+
func Get() *Brand {
39+
brandOnce.Do(func() {
40+
b := &Brand{ProductName: DefaultName}
41+
if utils.CheckIfPathExist(configPath()) {
42+
_ = utils.ReadYAML(configPath(), b)
43+
if strings.TrimSpace(b.ProductName) == "" {
44+
b.ProductName = DefaultName
45+
}
46+
}
47+
brand = b
48+
})
49+
return brand
50+
}
51+
52+
func Name() string {
53+
return Get().ProductName
54+
}
55+
56+
func IsCustom() bool {
57+
return utils.CheckIfPathExist(configPath())
58+
}

installer/branding/seed.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package branding
2+
3+
import (
4+
"bytes"
5+
"crypto/tls"
6+
"encoding/base64"
7+
"fmt"
8+
"io"
9+
"mime/multipart"
10+
"net/http"
11+
"os"
12+
"path/filepath"
13+
"strings"
14+
"time"
15+
16+
"github.com/utmstack/UTMStack/installer/utils"
17+
)
18+
19+
func InstallLicense(updatesFolder string) (bool, error) {
20+
src := LicensePath()
21+
if !utils.CheckIfPathExist(src) {
22+
return false, nil
23+
}
24+
dst := filepath.Join(updatesFolder, "LICENSE")
25+
if utils.CheckIfPathExist(dst) {
26+
return false, nil
27+
}
28+
data, err := os.ReadFile(src)
29+
if err != nil {
30+
return false, err
31+
}
32+
if err := os.WriteFile(dst, data, 0644); err != nil {
33+
return false, err
34+
}
35+
return true, nil
36+
}
37+
38+
var assetFiles = map[string]string{
39+
"logo": "logo",
40+
"logoDark": "logo-dark",
41+
"favicon": "favicon",
42+
"reportLogo": "report-logo",
43+
"reportCover": "report-cover",
44+
}
45+
46+
var imageExts = []string{".svg", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".ico"}
47+
48+
func findAsset(base string) string {
49+
for _, ext := range imageExts {
50+
p := filepath.Join(Dir(), base+ext)
51+
if utils.CheckIfPathExist(p) {
52+
return p
53+
}
54+
}
55+
return ""
56+
}
57+
58+
func Seed(baseURL, internalKey string) error {
59+
if !IsCustom() {
60+
return nil
61+
}
62+
b := Get()
63+
64+
var buf bytes.Buffer
65+
w := multipart.NewWriter(&buf)
66+
67+
fields := map[string]string{
68+
"enabled": "true",
69+
"productName": b.ProductName,
70+
}
71+
for k, v := range fields {
72+
if err := w.WriteField(k, v); err != nil {
73+
return err
74+
}
75+
}
76+
77+
for slot, base := range assetFiles {
78+
p := findAsset(base)
79+
if p == "" {
80+
continue
81+
}
82+
if err := addFile(w, slot, p); err != nil {
83+
return err
84+
}
85+
}
86+
if err := w.Close(); err != nil {
87+
return err
88+
}
89+
90+
req, err := http.NewRequest(http.MethodPost, strings.TrimRight(baseURL, "/")+"/api/v1/branding/seed", &buf)
91+
if err != nil {
92+
return err
93+
}
94+
req.Header.Set("Content-Type", w.FormDataContentType())
95+
req.Header.Set("X-Internal-Key", internalKey)
96+
97+
client := &http.Client{
98+
Timeout: 30 * time.Second,
99+
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
100+
}
101+
resp, err := client.Do(req)
102+
if err != nil {
103+
return err
104+
}
105+
defer func() { _ = resp.Body.Close() }()
106+
if resp.StatusCode != http.StatusOK {
107+
body, _ := io.ReadAll(resp.Body)
108+
return fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
109+
}
110+
return nil
111+
}
112+
113+
// mimeForExt maps an image extension to its MIME type (for data URIs).
114+
func mimeForExt(ext string) string {
115+
switch strings.ToLower(ext) {
116+
case ".svg":
117+
return "image/svg+xml"
118+
case ".png":
119+
return "image/png"
120+
case ".jpg", ".jpeg":
121+
return "image/jpeg"
122+
case ".webp":
123+
return "image/webp"
124+
case ".gif":
125+
return "image/gif"
126+
case ".ico":
127+
return "image/x-icon"
128+
default:
129+
return "application/octet-stream"
130+
}
131+
}
132+
133+
// LogoDataURI returns the shipped logo encoded as a base64 data URI, so it can
134+
// be embedded directly in the nginx maintenance page (which is served by the
135+
// host nginx while the backend — and its /uploads — is down). Returns false
136+
// when no logo was shipped.
137+
func LogoDataURI() (string, bool) {
138+
p := findAsset("logo")
139+
if p == "" {
140+
return "", false
141+
}
142+
data, err := os.ReadFile(p)
143+
if err != nil {
144+
return "", false
145+
}
146+
return "data:" + mimeForExt(filepath.Ext(p)) + ";base64," + base64.StdEncoding.EncodeToString(data), true
147+
}
148+
149+
func addFile(w *multipart.Writer, field, path string) error {
150+
f, err := os.Open(path)
151+
if err != nil {
152+
return err
153+
}
154+
defer func() { _ = f.Close() }()
155+
fw, err := w.CreateFormFile(field, filepath.Base(path))
156+
if err != nil {
157+
return err
158+
}
159+
_, err = io.Copy(fw, f)
160+
return err
161+
}

installer/docker/compose.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ func (c *Compose) Populate(conf *config.Config, stack *StackConfig) error {
214214
"EVENT_PROCESSOR_HOST=event-processor-manager",
215215
"EVENT_PROCESSOR_PORT=9002",
216216
"SOC_AI_BASE_URL=http://event-processor-manager:8090",
217+
"UPLOAD_DIR=/uploads",
217218
}
218219

219220
// Disable TFA in dev and rc environments
@@ -232,6 +233,7 @@ func (c *Compose) Populate(conf *config.Config, stack *StackConfig) error {
232233
Volumes: []string{
233234
stack.DataSources + ":/etc/utmstack",
234235
conf.UpdatesFolder + ":/updates",
236+
utils.MakeDir(0777, conf.DataDir, "uploads") + ":/uploads",
235237
},
236238
Logging: &dLogging,
237239
Deploy: &Deploy{

installer/docker/stack.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ import (
66
"path/filepath"
77
"strings"
88
"sync"
9-
"time"
109

1110
sigar "github.com/cloudfoundry/gosigar"
1211
"github.com/shirou/gopsutil/v3/cpu"
13-
"github.com/threatwinds/logger"
1412
"github.com/utmstack/UTMStack/installer/config"
1513
"github.com/utmstack/UTMStack/installer/system"
1614
"github.com/utmstack/UTMStack/installer/utils"
@@ -144,25 +142,3 @@ func StackUP(tag string) error {
144142

145143
return nil
146144
}
147-
148-
func RemoveServices(services []string) error {
149-
for _, service := range services {
150-
if err := utils.RunCmd("docker", "service", "rm", service); err != nil {
151-
if !logger.Is(err, "not found") {
152-
return err
153-
}
154-
}
155-
}
156-
157-
if err := utils.RunCmd("systemctl", "restart", "docker"); err != nil {
158-
return err
159-
}
160-
161-
time.Sleep(60 * time.Second)
162-
163-
if err := utils.RunCmd("docker", "system", "prune", "-a", "-f"); err != nil {
164-
return err
165-
}
166-
167-
return nil
168-
}

installer/install.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"time"
66

7+
"github.com/utmstack/UTMStack/installer/branding"
78
"github.com/utmstack/UTMStack/installer/config"
89
"github.com/utmstack/UTMStack/installer/docker"
910
"github.com/utmstack/UTMStack/installer/setup"
@@ -12,7 +13,7 @@ import (
1213
)
1314

1415
func Install() error {
15-
fmt.Println("### Installing UTMStack ###")
16+
fmt.Printf("### Installing %s ###\n", branding.Name())
1617

1718
go updater.MonitorConnection(config.GetCMServer(), 30*time.Second, 3, &config.ConnectedToInternet)
1819

@@ -22,7 +23,7 @@ func Install() error {
2223
}
2324

2425
if isInstalledAlready {
25-
fmt.Println("UTMStack is already installed. If you want to re-install it, please remove the service UTMStackComponentsUpdater first.")
26+
fmt.Printf("%s is already installed. If you want to re-install it, please remove the service UTMStackComponentsUpdater first.\n", branding.Name())
2627
if err := utils.RestartService("UTMStackComponentsUpdater"); err != nil {
2728
return fmt.Errorf("error restarting service: %v", err)
2829
}
@@ -55,7 +56,7 @@ func Install() error {
5556
fmt.Println("You can also access to your Web-based Administration Interface at https://<your-server-ip>:9090 using your Linux system credentials.")
5657
fmt.Println("Detailed installation logs can be found at /var/log/utmstack-installer.log")
5758

58-
fmt.Println("### Thanks for using UTMStack ###")
59+
fmt.Printf("### Thanks for using %s ###\n", branding.Name())
5960

6061
return nil
6162
}

installer/main.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66

7+
"github.com/utmstack/UTMStack/installer/branding"
78
"github.com/utmstack/UTMStack/installer/updater"
89
)
910

@@ -17,7 +18,7 @@ func main() {
1718
case "--install", "-i":
1819
err := Install()
1920
if err != nil {
20-
fmt.Printf("\nerror installing UTMStack: %v", err)
21+
fmt.Printf("\nerror installing %s: %v", branding.Name(), err)
2122
os.Exit(1)
2223
}
2324

@@ -27,16 +28,16 @@ func main() {
2728
case "--version", "-v":
2829
version, err := updater.GetVersion()
2930
if err != nil {
30-
fmt.Printf("\nerror getting UTMStack version: %v", err)
31+
fmt.Printf("\nerror getting %s version: %v", branding.Name(), err)
3132
os.Exit(1)
3233
}
3334

34-
fmt.Printf("UTMStack version: %s, edition: %s\n", version.Version, version.Edition)
35+
fmt.Printf("%s version: %s, edition: %s\n", branding.Name(), version.Version, version.Edition)
3536

3637
case "--uninstall", "-u":
3738
err := Uninstall()
3839
if err != nil {
39-
fmt.Printf("\nerror uninstalling UTMStack: %v", err)
40+
fmt.Printf("\nerror uninstalling %s: %v", branding.Name(), err)
4041
os.Exit(1)
4142
}
4243

@@ -46,18 +47,19 @@ func main() {
4647
} else {
4748
err := Install()
4849
if err != nil {
49-
fmt.Printf("\nerror installing UTMStack: %v", err)
50+
fmt.Printf("\nerror installing %s: %v", branding.Name(), err)
5051
os.Exit(1)
5152
}
5253
}
5354
}
5455

5556
func help() {
56-
fmt.Println("### UTMStack ###")
57+
name := branding.Name()
58+
fmt.Printf("### %s ###\n", name)
5759
fmt.Println("Usage: installer <argument>")
5860
fmt.Println("Arguments:")
5961
fmt.Println(" --help, -h Show this help")
60-
fmt.Println(" --install, -i Install UTMStack")
61-
fmt.Println(" --uninstall, -u Uninstall UTMStack")
62-
fmt.Println(" --version, -v Show UTMStack version")
62+
fmt.Printf(" --install, -i Install %s\n", name)
63+
fmt.Printf(" --uninstall, -u Uninstall %s\n", name)
64+
fmt.Printf(" --version, -v Show %s version\n", name)
6365
}

installer/network/nginx.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package network
33
import (
44
"path"
55

6+
"github.com/utmstack/UTMStack/installer/branding"
67
"github.com/utmstack/UTMStack/installer/config"
78
"github.com/utmstack/UTMStack/installer/docker"
89
"github.com/utmstack/UTMStack/installer/templates"
@@ -41,7 +42,11 @@ func ConfigureNginx(stack *docker.StackConfig, distro string) error {
4142
return err
4243
}
4344

44-
err = utils.WriteToFile(path.Join("/", "etc", "nginx", "html", "custom_502.html"), templates.NginxCustomBadGateway)
45+
logo := "https://storage.googleapis.com/utmstack-updates/nginx/logo_UTMStack.svg"
46+
if uri, ok := branding.LogoDataURI(); ok {
47+
logo = uri
48+
}
49+
err = utils.WriteToFile(path.Join("/", "etc", "nginx", "html", "custom_502.html"), templates.NginxCustomBadGateway(branding.Name(), logo))
4550
if err != nil {
4651
config.Logger().ErrorF("error writing custom 502 page: %v", err)
4752
}

0 commit comments

Comments
 (0)