diff --git a/changelog.md b/changelog.md index fe4add7..5cc5b73 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,8 @@ ## Version 0.13.0 - Display containers as stacks - New Delete modal to delete services entirely + - Upload custom icons to containers + - improve backup file, by splitting cosmos out to a separate docker-compose.yml file - Cosmos-networks now have specific names instead for a generic names - Fix issue where search bar reset when deleting volume/network - Fix breadcrumbs in subpaths @@ -9,7 +11,8 @@ - Edit container user and devices from UI - Fix bug where Cosmos Constellation's UDP ports by a TCP one - Support array command and single device in docker-compose import - - Add default alert.. by default + - Add default alerts... by default (was missing from the default config) + - disable few features liks Constellation, Backup and Monitoring when in install mode to reduce logs and prevent issues with the DB ## Version 0.12.6 - Fix a security issue with cross-domain APIs availability diff --git a/client/src/api/index.demo.jsx b/client/src/api/index.demo.jsx index 1fe79ce..4fc0406 100644 --- a/client/src/api/index.demo.jsx +++ b/client/src/api/index.demo.jsx @@ -78,7 +78,7 @@ export const checkHost = (host) => { }); } -export const uploadBackground = (file) => { +export const uploadImage = (file) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ diff --git a/client/src/api/index.jsx b/client/src/api/index.jsx index 7e0a3ac..ab14564 100644 --- a/client/src/api/index.jsx +++ b/client/src/api/index.jsx @@ -199,10 +199,10 @@ let getDNS = (host) => { }); } -let uploadBackground = (file) => { +let uploadImage = (file, name) => { const formData = new FormData(); - formData.append('background', file); - return wrap(fetch('/cosmos/api/background', { + formData.append('image', file); + return wrap(fetch('/cosmos/api/upload/' + name, { method: 'POST', body: formData })); @@ -229,7 +229,7 @@ if(isDemo) { isOnline = indexDemo.isOnline; checkHost = indexDemo.checkHost; getDNS = indexDemo.getDNS; - uploadBackground = indexDemo.uploadBackground; + uploadImage = indexDemo.uploadImage; constellation = constellationDemo; metrics = metricsDemo; } @@ -247,5 +247,5 @@ export { checkHost, getDNS, metrics, - uploadBackground + uploadImage }; \ No newline at end of file diff --git a/client/src/pages/config/routes/routeoverview.jsx b/client/src/pages/config/routes/routeoverview.jsx index ae2e90e..4d04d90 100644 --- a/client/src/pages/config/routes/routeoverview.jsx +++ b/client/src/pages/config/routes/routeoverview.jsx @@ -13,6 +13,7 @@ import { CosmosCheckbox } from '../users/formShortcuts'; import { Field } from 'formik'; import MiniPlotComponent from '../../dashboard/components/mini-plot'; import ImageWithPlaceholder from '../../../components/imageWithPlaceholder'; +import UploadButtons from '../../../components/fileUpload'; const info = { backgroundColor: 'rgba(0, 0, 0, 0.1)', diff --git a/client/src/pages/config/users/configman.jsx b/client/src/pages/config/users/configman.jsx index bbe03dc..87fcfe9 100644 --- a/client/src/pages/config/users/configman.jsx +++ b/client/src/pages/config/users/configman.jsx @@ -316,10 +316,10 @@ const ConfigManagement = () => { - {!uploadingBackground && formik.values.Background && preview seems broken. Please re-upload.} - {uploadingBackground && } + {!uploadingBackground && formik.values.Background && preview seems broken. Please re-upload.} + {uploadingBackground && } { OnChange={(e) => { setUploadingBackground(true); const file = e.target.files[0]; - API.uploadBackground(file).then((data) => { - formik.setFieldValue('Background', "/cosmos/api/background/" + data.data.extension.replace(".", "")); + API.uploadImage(file, "background").then((data) => { + formik.setFieldValue('Background', data.data.path); setUploadingBackground(false); }); }} diff --git a/client/src/pages/servapps/containers/newServiceForm.jsx b/client/src/pages/servapps/containers/newServiceForm.jsx index dcc94d6..6974232 100644 --- a/client/src/pages/servapps/containers/newServiceForm.jsx +++ b/client/src/pages/servapps/containers/newServiceForm.jsx @@ -69,9 +69,9 @@ const NewDockerServiceForm = () => { image: containerInfo.Config.Image, environment: containerInfo.Config.Env, labels: containerInfo.Config.Labels, - devices: containerInfo.HostConfig.Devices.map((device) => { + devices: containerInfo.HostConfig.Devices ? containerInfo.HostConfig.Devices.map((device) => { return `${device.PathOnHost}:${device.PathInContainer}:`; - }), + }) : [], expose: containerInfo.Config.ExposedPorts, tty: containerInfo.Config.Tty, stdin_open: containerInfo.Config.OpenStdin, diff --git a/client/src/pages/servapps/containers/overview.jsx b/client/src/pages/servapps/containers/overview.jsx index 79ae7f3..f993a23 100644 --- a/client/src/pages/servapps/containers/overview.jsx +++ b/client/src/pages/servapps/containers/overview.jsx @@ -10,6 +10,7 @@ import RestartModal from '../../config/users/restart'; import GetActions from '../actionBar'; import { ServAppIcon } from '../../../utils/servapp-icon'; import MiniPlotComponent from '../../dashboard/components/mini-plot'; +import UploadButtons from '../../../components/fileUpload'; const info = { backgroundColor: 'rgba(0, 0, 0, 0.1)', @@ -87,6 +88,25 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s "dead": , })[State.Status]} + { + const file = e.target.files[0]; + setIsUpdating(true); + API.uploadImage(file, "servapp-" + Name.replace('/', '')).then((data) => { + API.docker.updateContainer(Name.replace('/', ''), { + labels: { + ...Config.Labels, + "cosmos-icon": data.data.path, + } + }) + .then(() => { + refreshAll(); + }); + }); + }} + /> diff --git a/client/src/pages/servapps/containers/setup.jsx b/client/src/pages/servapps/containers/setup.jsx index bc4b79b..4ce17ef 100644 --- a/client/src/pages/servapps/containers/setup.jsx +++ b/client/src/pages/servapps/containers/setup.jsx @@ -71,11 +71,11 @@ const DockerContainerSetup = ({ noCard, containerInfo, installer, OnChange, refr labels: Object.keys(containerInfo.Config.Labels).map((key) => { return { key, value: containerInfo.Config.Labels[key] }; }), - devices: containerInfo.HostConfig.Devices.map((device) => { + devices: containerInfo.HostConfig.Devices ? containerInfo.HostConfig.Devices.map((device) => { return (typeof device == "string") ? { key: device.split(":")[0], value: (device.split(":")[1] || device.split(":")[0]) } : { key: device.PathOnHost, value: device.PathInContainer }; - }), + }) : [], interactive: containerInfo.Config.Tty && containerInfo.Config.OpenStdin, }} enableReinitialize diff --git a/package.json b/package.json index 265c024..a65b089 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.13.0-unstable7", + "version": "0.13.0-unstable8", "description": "", "main": "test-server.js", "bugs": { diff --git a/src/CRON.go b/src/CRON.go index 087cdcd..97195ef 100644 --- a/src/CRON.go +++ b/src/CRON.go @@ -126,6 +126,7 @@ func CRON() { s.Every(1).Day().At("00:00").Do(func() { utils.CleanupByDate("notifications") utils.CleanupByDate("events") + imageCleanUp() }) s.Start() }() diff --git a/src/background.go b/src/background.go deleted file mode 100644 index b096d0f..0000000 --- a/src/background.go +++ /dev/null @@ -1,117 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "net/http" - "path/filepath" - "io" - "encoding/json" - - "github.com/gorilla/mux" - "github.com/azukaar/cosmos-server/src/utils" -) - -var validExtensions = map[string]bool{ - ".jpg": true, - ".jpeg": true, - ".png": true, - ".gif": true, - ".bmp": true, - ".svg": true, - ".webp": true, - ".tiff": true, - ".avif": true, -} - -func UploadBackground(w http.ResponseWriter, req *http.Request) { - if utils.AdminOnly(w, req) != nil { - return - } - - if(req.Method == "POST") { - // parse the form data - err := req.ParseMultipartForm(1 << 20) - if err != nil { - utils.HTTPError(w, "Error parsing form data", http.StatusInternalServerError, "FORM001") - return - } - - // retrieve the file part of the form - file, header, err := req.FormFile("background") - if err != nil { - utils.HTTPError(w, "Error retrieving file from form data", http.StatusInternalServerError, "FORM002") - return - } - defer file.Close() - - // get the file extension - ext := filepath.Ext(header.Filename) - - if !validExtensions[ext] { - utils.HTTPError(w, "Invalid file extension " + ext, http.StatusBadRequest, "FILE001") - return - } - - // create a new file in the config directory - dst, err := os.Create(utils.CONFIGFOLDER + "background" + ext) - if err != nil { - utils.HTTPError(w, "Error creating destination file", http.StatusInternalServerError, "FILE004") - return - } - defer dst.Close() - - // copy the uploaded file to the destination file - if _, err := io.Copy(dst, file); err != nil { - utils.HTTPError(w, "Error writing to destination file", http.StatusInternalServerError, "FILE005") - return - } - - // return a response to the client - json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "OK", - "data": map[string]interface{}{ - "filename": header.Filename, - "size": header.Size, - "extension": ext, - }, - }) - - } else { - utils.Error("UploadBackground: Method not allowed - " + req.Method, nil) - utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") - return - } -} - -func GetBackground(w http.ResponseWriter, req *http.Request) { - if utils.LoggedInOnly(w, req) != nil { - return - } - - vars := mux.Vars(req) - ext := vars["ext"] - - if !validExtensions["." + ext] { - utils.HTTPError(w, "Invalid file extension", http.StatusBadRequest, "FILE001") - return - } - - if(req.Method == "GET") { - // get the background image - bg, err := ioutil.ReadFile(utils.CONFIGFOLDER + "background." + ext) - if err != nil { - utils.HTTPError(w, "Error reading background image", http.StatusInternalServerError, "FILE003") - return - } - - // return a response to the client - w.Header().Set("Content-Type", "image/" + ext) - w.Write(bg) - - } else { - utils.Error("GetBackground: Method not allowed - " + req.Method, nil) - utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") - return - } -} \ No newline at end of file diff --git a/src/docker/api_blueprint.go b/src/docker/api_blueprint.go index 0872288..e7ec40f 100644 --- a/src/docker/api_blueprint.go +++ b/src/docker/api_blueprint.go @@ -1013,8 +1013,6 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string) func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]ContainerCreateRequestContainer, error) { startOrder := []ContainerCreateRequestContainer{} - utils.Debug(fmt.Sprintf("ReOrderServices: start: %s", serviceMap)) - for len(serviceMap) > 0 { // Keep track of whether we've added any services in this iteration changed := false diff --git a/src/docker/export.go b/src/docker/export.go index fd6dbaf..d342108 100644 --- a/src/docker/export.go +++ b/src/docker/export.go @@ -7,6 +7,8 @@ import ( "strconv" "strings" "bytes" + "gopkg.in/yaml.v2" + "os" "github.com/azukaar/cosmos-server/src/utils" "github.com/docker/docker/api/types" @@ -18,12 +20,17 @@ import ( var ExportError = "" func ExportDocker() { + config := utils.GetMainConfig() + if config.NewInstall { + return + } + ExportError = "" errD := Connect() if errD != nil { ExportError = "Export Docker - cannot connect - " + errD.Error() - utils.Error("ExportDocker - connect - ", errD) + utils.MajorError("ExportDocker - connect - ", errD) return } @@ -32,7 +39,7 @@ func ExportDocker() { // List containers containers, err := DockerClient.ContainerList(DockerContext, types.ContainerListOptions{}) if err != nil { - utils.Error("ExportDocker - Cannot list containers", err) + utils.MajorError("ExportDocker - Cannot list containers", err) ExportError = "Export Docker - Cannot list containers - " + err.Error() return } @@ -44,7 +51,7 @@ func ExportDocker() { // Fetch detailed info of each container detailedInfo, err := DockerClient.ContainerInspect(DockerContext, container.ID) if err != nil { - utils.Error("Export Docker - Cannot inspect container" + container.Names[0], err) + utils.MajorError("Export Docker - Cannot inspect container" + container.Names[0], err) ExportError = "Export Docker - Cannot inspect container" + container.Names[0] + " - " + err.Error() return } @@ -118,11 +125,11 @@ func ExportDocker() { // Networks Networks: func() map[string]ContainerCreateRequestServiceNetwork { networks := make(map[string]ContainerCreateRequestServiceNetwork) - for netName, netConfig := range detailedInfo.NetworkSettings.Networks { + for netName, _ := range detailedInfo.NetworkSettings.Networks { networks[netName] = ContainerCreateRequestServiceNetwork{ - Aliases: netConfig.Aliases, - IPV4Address: netConfig.IPAddress, - IPV6Address: netConfig.GlobalIPv6Address, + // Aliases: netConfig.Aliases, + // IPV4Address: netConfig.IPAddress, + // IPV6Address: netConfig.GlobalIPv6Address, } } return networks @@ -175,7 +182,7 @@ func ExportDocker() { // List networks networks, err := DockerClient.NetworkList(DockerContext, types.NetworkListOptions{}) if err != nil { - utils.Error("Export Docker - Cannot list networks", err) + utils.MajorError("Export Docker - Cannot list networks", err) ExportError = "Export Docker - Cannot list networks - " + err.Error() return } @@ -191,7 +198,7 @@ func ExportDocker() { // Fetch detailed info of each network detailedInfo, err := DockerClient.NetworkInspect(DockerContext, network.ID, types.NetworkInspectOptions{}) if err != nil { - utils.Error("Export Docker - Cannot inspect network", err) + utils.MajorError("Export Docker - Cannot inspect network", err) ExportError = "Export Docker - Cannot inspect network - " + err.Error() return } @@ -217,6 +224,47 @@ func ExportDocker() { finalBackup.Networks[detailedInfo.Name] = network } + // remove cosmos from services + if os.Getenv("HOSTNAME") != "" { + cosmos := services[os.Getenv("HOSTNAME")] + delete(services, os.Getenv("HOSTNAME")) + + // export separately cosmos + // Create a buffer to hold the JSON output + var buf bytes.Buffer + + // Create a new yaml encoder that writes to the buffer + encoder := yaml.NewEncoder(&buf) + + // Set escape HTML to false to avoid escaping special characters + // encoder.SetEscapeHTML(false) + //format + // encoder.SetIndent("", " ") + + // Use the encoder to write the structured data to the buffer + toExport := map[string]map[string]ContainerCreateRequestContainer { + "services": map[string]ContainerCreateRequestContainer { + os.Getenv("HOSTNAME"): cosmos, + }, + } + + err = encoder.Encode(toExport) + if err != nil { + utils.MajorError("Export Docker - Cannot marshal docker backup", err) + ExportError = "Export Docker - Cannot marshal docker backup - " + err.Error() + } + + // The JSON data is now in buf.Bytes() + yamlData := buf.Bytes() + + // Write the JSON data to a file + err = ioutil.WriteFile(utils.CONFIGFOLDER + "cosmos.docker-compose.yaml", yamlData, 0644) + if err != nil { + utils.MajorError("Export Docker - Cannot save docker backup", err) + ExportError = "Export Docker - Cannot save docker backup - " + err.Error() + } + } + // Convert the services map to your finalBackup struct finalBackup.Services = services @@ -234,7 +282,7 @@ func ExportDocker() { // Use the encoder to write the structured data to the buffer err = encoder.Encode(finalBackup) if err != nil { - utils.Error("Export Docker - Cannot marshal docker backup", err) + utils.MajorError("Export Docker - Cannot marshal docker backup", err) ExportError = "Export Docker - Cannot marshal docker backup - " + err.Error() } @@ -244,7 +292,7 @@ func ExportDocker() { // Write the JSON data to a file err = ioutil.WriteFile(utils.CONFIGFOLDER + "backup.cosmos-compose.json", jsonData, 0644) if err != nil { - utils.Error("Export Docker - Cannot save docker backup", err) + utils.MajorError("Export Docker - Cannot save docker backup", err) ExportError = "Export Docker - Cannot save docker backup - " + err.Error() } } \ No newline at end of file diff --git a/src/httpServer.go b/src/httpServer.go index 3a014a2..12a9244 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -392,8 +392,8 @@ func InitServer() *mux.Router { srapi.HandleFunc("/api/markets", market.MarketGet) - srapi.HandleFunc("/api/background", UploadBackground) - srapi.HandleFunc("/api/background/{ext}", GetBackground) + srapi.HandleFunc("/api/upload/{name}", UploadImage) + srapi.HandleFunc("/api/image/{name}", GetImage) srapi.HandleFunc("/api/get-backup", configapi.BackupFileApiGet) diff --git a/src/image.go b/src/image.go new file mode 100644 index 0000000..5a9d745 --- /dev/null +++ b/src/image.go @@ -0,0 +1,204 @@ +package main + +import ( + "io/ioutil" + "os" + "net/http" + "path/filepath" + "io" + "encoding/json" + "strings" + "fmt" + + "github.com/gorilla/mux" + "github.com/azukaar/cosmos-server/src/utils" + "github.com/azukaar/cosmos-server/src/docker" +) + +var validExtensions = map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".png": true, + ".gif": true, + ".bmp": true, + ".svg": true, + ".webp": true, + ".tiff": true, + ".avif": true, +} + +func UploadImage(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + vars := mux.Vars(req) + originalName := vars["name"] + + name := originalName + "-" + utils.GenerateRandomString(6) + + // if name includes / or .. + if filepath.Clean(name) != name || strings.Contains(name, "/") { + utils.HTTPError(w, "Invalid file name", http.StatusBadRequest, "FILE002") + return + } + + if(req.Method == "POST") { + // if the uploads directory does not exist, create it + if _, err := os.Stat(utils.CONFIGFOLDER + "/uploads"); os.IsNotExist(err) { + os.Mkdir(utils.CONFIGFOLDER + "/uploads", 0750) + } + + // parse the form data + err := req.ParseMultipartForm(1 << 20) + if err != nil { + utils.HTTPError(w, "Error parsing form data", http.StatusInternalServerError, "FORM001") + return + } + + // retrieve the file part of the form + file, header, err := req.FormFile("image") + if err != nil { + utils.HTTPError(w, "Error retrieving file from form data", http.StatusInternalServerError, "FORM002") + return + } + defer file.Close() + + // get the file extension + ext := filepath.Ext(header.Filename) + + if !validExtensions[ext] { + utils.HTTPError(w, "Invalid file extension " + ext, http.StatusBadRequest, "FILE001") + return + } + + // create a new file in the config directory + dst, err := os.Create(utils.CONFIGFOLDER + "/uploads/" + name + ext) + if err != nil { + utils.HTTPError(w, "Error creating destination file", http.StatusInternalServerError, "FILE004") + return + } + defer dst.Close() + + // copy the uploaded file to the destination file + if _, err := io.Copy(dst, file); err != nil { + utils.HTTPError(w, "Error writing to destination file", http.StatusInternalServerError, "FILE005") + return + } + + // return a response to the client + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": map[string]interface{}{ + "path": "/cosmos/api/image/" + name + ext, + "filename": header.Filename, + "size": header.Size, + "extension": ext, + }, + }) + + } else { + utils.Error("UploadBackground: Method not allowed - " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func GetImage(w http.ResponseWriter, req *http.Request) { + if utils.LoggedInOnly(w, req) != nil { + return + } + + utils.Log("API: GetImage") + + vars := mux.Vars(req) + name := vars["name"] + + // if name includes / or .. + if filepath.Clean(name) != name || strings.Contains(name, "/") { + utils.Error("GetBackground: Invalid file name - " + name, nil) + utils.HTTPError(w, "Invalid file name", http.StatusBadRequest, "FILE002") + return + } + + // get the file extension + ext := filepath.Ext(name) + + if !validExtensions[ext] { + utils.Error("GetBackground: Invalid file extension - " + ext, nil) + utils.HTTPError(w, "Invalid file extension", http.StatusBadRequest, "FILE001") + return + } + + if(req.Method == "GET") { + // get the background image + bg, err := ioutil.ReadFile(utils.CONFIGFOLDER + "/uploads/" + name) + if err != nil { + utils.Error("GetBackground: Error reading image - " + name, err) + utils.HTTPError(w, "Error reading image", http.StatusInternalServerError, "FILE003") + return + } + + // return a response to the client + w.Header().Set("Content-Type", "image/" + ext) + w.Write(bg) + + } else { + utils.Error("GetBackground: Method not allowed - " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func imageCleanUp() { + utils.Log("Image cleanup") + + config := utils.GetMainConfig() + images := map[string]bool{} + images[config.HomepageConfig.Background] = true + + for _, route := range config.HTTPConfig.ProxyConfig.Routes { + if(route.Icon != "") { + images[route.Icon] = true + } + } + + // get containers + containers, err := docker.ListContainers() + + if err != nil { + utils.Error("Image cleanup: Error getting containers", err) + return + } + + for _, container := range containers { + if(container.Labels["cosmos-icon"] != "") { + images[container.Labels["cosmos-icon"]] = true + } + } + + fmt.Println(images) + + // if the uploads directory does not exist, return + if _, err := os.Stat(utils.CONFIGFOLDER + "/uploads"); os.IsNotExist(err) { + return + } + + // get the files in the uploads directory + files, err := ioutil.ReadDir(utils.CONFIGFOLDER + "/uploads") + if err != nil { + utils.Error("Image cleanup: Error reading directory", err) + return + } + + // loop through the files + base := "/cosmos/api/image/" + for _, f := range files { + if(!images[base + f.Name()]) { + err := os.Remove(utils.CONFIGFOLDER + "/uploads/" + f.Name()) + if err != nil { + utils.Error("Image cleanup: Error removing file", err) + } + } + } +} \ No newline at end of file diff --git a/src/index.go b/src/index.go index 7cd65b4..4ad39d9 100644 --- a/src/index.go +++ b/src/index.go @@ -45,25 +45,30 @@ func main() { utils.Log("Docker API version: " + version.APIVersion) } - utils.Log("Starting monitoring services...") + config := utils.GetMainConfig() + if !config.NewInstall { - metrics.Init() + utils.Log("Starting monitoring services...") - utils.Log("Starting market services...") + metrics.Init() - market.Init() - - utils.Log("Starting OpenID services...") + utils.Log("Starting market services...") - authorizationserver.Init() + market.Init() + + utils.Log("Starting OpenID services...") - utils.Log("Starting constellation services...") + authorizationserver.Init() - constellation.InitDNS() - - constellation.Init() + utils.Log("Starting constellation services...") - utils.Log("Starting server...") + constellation.InitDNS() + + constellation.Init() + + utils.Log("Starting server...") + + } StartServer() } diff --git a/src/utils/types.go b/src/utils/types.go index 3c985a8..366df92 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -188,6 +188,7 @@ type ProxyRouteConfig struct { RestrictToConstellation bool OverwriteHostHeader string WhitelistInboundIPs []string + Icon string } type EmailConfig struct {