[release] v0.13.0-unstable8

This commit is contained in:
Yann Stepienik 2023-11-24 13:03:23 +00:00
parent 49d7dd7d4a
commit 9085b2587c
17 changed files with 326 additions and 162 deletions

View file

@ -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

View file

@ -78,7 +78,7 @@ export const checkHost = (host) => {
});
}
export const uploadBackground = (file) => {
export const uploadImage = (file) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({

View file

@ -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
};

View file

@ -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)',

View file

@ -327,8 +327,8 @@ const ConfigManagement = () => {
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);
});
}}

View file

@ -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,

View file

@ -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": <Chip label="Dead" color="error" />,
})[State.Status]}
</div>
<UploadButtons
accept='.jpg, .png, .gif, .jpeg, .webp, .bmp, .avif, .tiff, .svg'
label="icon"
OnChange={(e) => {
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();
});
});
}}
/>
</Stack>
<Stack spacing={2} style={{ width: '100%' }} >

View file

@ -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

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.13.0-unstable7",
"version": "0.13.0-unstable8",
"description": "",
"main": "test-server.js",
"bugs": {

View file

@ -126,6 +126,7 @@ func CRON() {
s.Every(1).Day().At("00:00").Do(func() {
utils.CleanupByDate("notifications")
utils.CleanupByDate("events")
imageCleanUp()
})
s.Start()
}()

View file

@ -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
}
}

View file

@ -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

View file

@ -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()
}
}

View file

@ -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)

204
src/image.go Normal file
View file

@ -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)
}
}
}
}

View file

@ -45,6 +45,9 @@ func main() {
utils.Log("Docker API version: " + version.APIVersion)
}
config := utils.GetMainConfig()
if !config.NewInstall {
utils.Log("Starting monitoring services...")
metrics.Init()
@ -65,5 +68,7 @@ func main() {
utils.Log("Starting server...")
}
StartServer()
}

View file

@ -188,6 +188,7 @@ type ProxyRouteConfig struct {
RestrictToConstellation bool
OverwriteHostHeader string
WhitelistInboundIPs []string
Icon string
}
type EmailConfig struct {