[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

@ -316,10 +316,10 @@ const ConfigManagement = () => {
<MainCard title="Appearance">
<Grid container spacing={3}>
<Grid item xs={12}>
{!uploadingBackground && formik.values.Background && <img src=
{formik.values.Background} alt="preview seems broken. Please re-upload."
width={285} />}
{uploadingBackground && <Skeleton variant="rectangular" width={285} height={140} />}
{!uploadingBackground && formik.values.Background && <img src=
{formik.values.Background} alt="preview seems broken. Please re-upload."
width={285} />}
{uploadingBackground && <Skeleton variant="rectangular" width={285} height={140} />}
<Stack spacing={1} direction="row">
<UploadButtons
accept='.jpg, .png, .gif, .jpeg, .webp, .bmp, .avif, .tiff, .svg'
@ -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,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()
}

View file

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