Browse Source

[release] v0.5.11

Yann Stepienik 2 years ago
parent
commit
c97ebed936

+ 6 - 1
changelog.md

@@ -1,4 +1,9 @@
-## version 0.5.1 -> 0.5.7
+## versiom 0.5.11
+- Improve docker-compose import support for alternative syntaxes
+- Improve docker service creation when using force secure label (fixes few containers not liking restarting too fast when created)
+- Add toggle for using insecure HTTPS targets (fixes Unifi controller)
+
+## version 0.5.1 -> 0.5.10
 - Add Wilcard certificates support
 - Auto switch to Mongo 4 if CPU has no ADX
 - Improve setup for certificates on new install

+ 7 - 0
client/src/pages/config/routes/routeman.jsx

@@ -46,6 +46,7 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
           Target: routeConfig.Target,
           UseHost: routeConfig.UseHost,
           Host: routeConfig.Host,
+          AcceptInsecureHTTPSTarget: routeConfig.AcceptInsecureHTTPSTarget === true,
           UsePathPrefix: routeConfig.UsePathPrefix,
           PathPrefix: routeConfig.PathPrefix,
           StripPathPrefix: routeConfig.StripPathPrefix,
@@ -167,6 +168,12 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
                       />
                   }
 
+                  {formik.values.Target.startsWith('https://') && <CosmosCheckbox
+                    name="AcceptInsecureHTTPSTarget"
+                    label="Accept Insecure HTTPS Target (not recommended)"
+                    formik={formik}
+                  />}
+
                   <CosmosFormDivider title={'Source'} />
 
                   <Grid item xs={12}>

+ 199 - 135
client/src/pages/servapps/containers/docker-compose.jsx

@@ -1,7 +1,7 @@
 // material-ui
 import * as React from 'react';
 import { Alert, Button, Stack, Typography } from '@mui/material';
-import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined } from '@ant-design/icons';
+import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined, SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined } from '@ant-design/icons';
 import Table from '@mui/material/Table';
 import TableBody from '@mui/material/TableBody';
 import TableCell from '@mui/material/TableCell';
@@ -28,13 +28,13 @@ import NewDockerService from './newService';
 import yaml from 'js-yaml';
 
 function checkIsOnline() {
-    API.isOnline().then((res) => {
-        window.location.reload();
-    }).catch((err) => {
-        setTimeout(() => {
-            checkIsOnline();
-        }, 1000);
-    });
+  API.isOnline().then((res) => {
+    window.location.reload();
+  }).catch((err) => {
+    setTimeout(() => {
+      checkIsOnline();
+    }, 1000);
+  });
 }
 
 const preStyle = {
@@ -63,30 +63,25 @@ const preStyle = {
   marginRight: '0',
 }
 
-const DockerComposeImport = ({refresh}) => {
-    const [step, setStep] = useState(0);
-    const [isLoading, setIsLoading] = useState(false);
-    const [openModal, setOpenModal] = useState(false);
-    const [dockerCompose, setDockerCompose] = useState('');
-    const [service, setService] = useState({});
-    const [ymlError, setYmlError] = useState('');
-
-    useEffect(() => {
-      if(dockerCompose === '') {
-        return;
-      }
+const DockerComposeImport = ({ refresh }) => {
+  const [step, setStep] = useState(0);
+  const [isLoading, setIsLoading] = useState(false);
+  const [openModal, setOpenModal] = useState(false);
+  const [dockerCompose, setDockerCompose] = useState('');
+  const [service, setService] = useState({});
+  const [ymlError, setYmlError] = useState('');
+
+  useEffect(() => {
+    if (dockerCompose === '') {
+      return;
+    }
+
+    setYmlError('');
+    let doc;
+    let newService = {};
+    try {
+      doc = yaml.load(dockerCompose);
 
-      setYmlError('');
-      let doc;
-      let newService = {};
-      try {
-        doc = yaml.load(dockerCompose);
-      } catch (e) {
-        console.log(e);
-        setYmlError(e.message);
-        return;
-      }
-      
       if (typeof doc === 'object' && doc !== null && Object.keys(doc).length > 0 &&
         !doc.services && !doc.networks && !doc.volumes) {
         doc = {
@@ -94,52 +89,66 @@ const DockerComposeImport = ({refresh}) => {
         }
       }
 
-      
+
       // convert to the proper format
-      if(doc.services) {
+      if (doc.services) {
         Object.keys(doc.services).forEach((key) => {
+
           // convert volumes
-          if(doc.services[key].volumes) {
-            let volumes = [];
-            doc.services[key].volumes.forEach((volume) => {
-              let volumeSplit = volume.split(':');
-              let volumeObj = {
-                Source: volumeSplit[0],
-                Target: volumeSplit[1],
-                Type: volume[0] === '/' ? 'bind' : 'volume',
-              };
-              volumes.push(volumeObj);
-            });
-            doc.services[key].volumes = volumes;
+          if (doc.services[key].volumes) {
+            if(Array.isArray(doc.services[key].volumes)) {
+              let volumes = [];
+              doc.services[key].volumes.forEach((volume) => {
+                if (typeof volume === 'object') {
+                  volumes.push(volume);
+                } else {
+                  let volumeSplit = volume.split(':');
+                  let volumeObj = {
+                    Source: volumeSplit[0],
+                    Target: volumeSplit[1],
+                    Type: volume[0] === '/' ? 'bind' : 'volume',
+                  };
+                  volumes.push(volumeObj);
+                }
+              });
+              doc.services[key].volumes = volumes;
+            }
           }
 
           // convert expose
-          if(doc.services[key].expose) {
+          if (doc.services[key].expose) {
             doc.services[key].expose = doc.services[key].expose.map((port) => {
-              return ''+port;
+              return '' + port;
             })
           }
 
           //convert user
-          if(doc.services[key].user) {
+          if (doc.services[key].user) {
             doc.services[key].user = '' + doc.services[key].user;
           }
 
           // convert labels: 
-          if(doc.services[key].labels) {
-            if(Array.isArray(doc.services[key].labels)) {
+          if (doc.services[key].labels) {
+            if (Array.isArray(doc.services[key].labels)) {
               let labels = {};
               doc.services[key].labels.forEach((label) => {
                 const [key, value] = label.split('=');
-                labels[''+key] = ''+value;
+                labels['' + key] = '' + value;
+              });
+              doc.services[key].labels = labels;
+            }
+            if (typeof doc.services[key].labels == 'object') {
+              let labels = {};
+              Object.keys(doc.services[key].labels).forEach((keylabel) => {
+                labels['' + keylabel] = '' + doc.services[key].labels[keylabel];
               });
               doc.services[key].labels = labels;
             }
           }
-          
+
           // convert environment
-          if(doc.services[key].environment) {
-            if(!Array.isArray(doc.services[key].environment)) {
+          if (doc.services[key].environment) {
+            if (!Array.isArray(doc.services[key].environment)) {
               let environment = [];
               Object.keys(doc.services[key].environment).forEach((keyenv) => {
                 environment.push(keyenv + '=' + doc.services[key].environment[keyenv]);
@@ -149,100 +158,155 @@ const DockerComposeImport = ({refresh}) => {
           }
 
           // convert network
-          if(doc.services[key].networks) {
-            if(Array.isArray(doc.services[key].networks)) {
+          if (doc.services[key].networks) {
+            if (Array.isArray(doc.services[key].networks)) {
               let networks = {};
               doc.services[key].networks.forEach((network) => {
-                networks[''+network] = {};
+                if (typeof network === 'object') {
+                  networks['' + network.name] = network;
+                }
+                else
+                  networks['' + network] = {};
               });
               doc.services[key].networks = networks;
             }
           }
 
           // ensure container_name
-          if(!doc.services[key].container_name) {
+          if (!doc.services[key].container_name) {
             doc.services[key].container_name = key;
           }
         });
       }
 
-      setService(doc);
-    }, [dockerCompose]);
-
-    return <>
-        <Dialog open={openModal} onClose={() => setOpenModal(false)}>
-            <DialogTitle>Import Docker Compose</DialogTitle>
-            <DialogContent>
-                <DialogContentText>
-                  {step === 0 && <Stack spacing={2}>
-                    <Alert severity="warning" icon={<WarningOutlined />}>
-                      This is a highly experimental feature. It is recommended to use with caution.
-                    </Alert>
-
-                    <UploadButtons
-                      accept='.yml,.yaml'
-                      OnChange={(e) => {
-                        const file = e.target.files[0];
-                        const reader = new FileReader();
-                        reader.onload = (e) => {
-                          setDockerCompose(e.target.result);
-                        };
-                        reader.readAsText(file);
-                      }}
-                    />
-
-                    <div style={{color: 'red'}}>
-                    {ymlError}
-                    </div>
-                    
-                    <TextField
-                      multiline
-                      placeholder='Paste your docker-compose.yml here or use the file upload button.'
-                      fullWidth
-                      value={dockerCompose}
-                      onChange={(e) => setDockerCompose(e.target.value)}
-                      sx={preStyle}
-                      InputProps={{
-                        sx: {
-                          color: '#EEE',
-                        }
-                      }}
-                      rows={20}></TextField>
-                  </Stack>}
-                  {step === 1 && <Stack spacing={2}>
-                    <NewDockerService service={service} refresh={refresh}/>
-                  </Stack>}
-                </DialogContentText>
-            </DialogContent>
-            {!isLoading && <DialogActions>
-                <Button onClick={() => {
-                  setOpenModal(false);
-                  setStep(0);
-                  setDockerCompose('');
-                  setYmlError('');
-                }}>Close</Button>
-                <Button onClick={() => {
-                  if(step === 0) {
-                    setStep(1);
-                  } else {
-                    setStep(0);
-                  }
-                }}>
-                  {step === 0 && 'Next'}
-                  {step === 1 && 'Back'}
-                </Button>
-            </DialogActions>}
-        </Dialog>
-
-        <ResponsiveButton
-            color="primary"
-            onClick={() => setOpenModal(true)}
-            variant="outlined"
-            startIcon={<ArrowUpOutlined />}
-        >
-            Import Docker Compose
-        </ResponsiveButton>
-    </>;
+      // convert networks
+      if (doc.networks) {
+        if (Array.isArray(doc.networks)) {
+          let networks = {};
+          doc.networks.forEach((network) => {
+            if (typeof network === 'object') {
+              networks['' + network.name] = network;
+            }
+            else
+              networks['' + network] = {};
+          });
+          doc.networks = networks;
+        } else {
+          let networks = {};
+          Object.keys(doc.networks).forEach((key) => {
+            networks['' + key] = doc.networks[key] || {};
+          });
+          doc.networks = networks;
+        }
+      }
+
+      // convert volumes
+      if (doc.volumes) {
+        if (Array.isArray(doc.volumes)) {
+          let volumes = {};
+          doc.volumes.forEach((volume) => {
+            if(!volume) {
+              volume = {};
+            }
+            if (typeof volume === 'object') {
+              volumes['' + volume.name] = volume;
+            }
+            else
+              volumes['' + volume] = {};
+          });
+          doc.volumes = volumes;
+        } else {
+          let volumes = {};
+          Object.keys(doc.volumes).forEach((key) => {
+            volumes['' + key] = doc.volumes[key] || {};
+          });
+          doc.volumes = volumes;
+        }
+      }
+
+    } catch (e) {
+      console.log(e);
+      setYmlError(e.message);
+      return;
+    }
+
+    setService(doc);
+  }, [dockerCompose]);
+
+  return <>
+    <Dialog open={openModal} onClose={() => setOpenModal(false)}>
+      <DialogTitle>Import Docker Compose</DialogTitle>
+      <DialogContent>
+        <DialogContentText>
+          {step === 0 && <Stack spacing={2}>
+            <Alert severity="warning" icon={<WarningOutlined />}>
+              This is a highly experimental feature. It is recommended to use with caution.
+            </Alert>
+
+            <UploadButtons
+              accept='.yml,.yaml'
+              OnChange={(e) => {
+                const file = e.target.files[0];
+                const reader = new FileReader();
+                reader.onload = (e) => {
+                  setDockerCompose(e.target.result);
+                };
+                reader.readAsText(file);
+              }}
+            />
+
+            <div style={{ color: 'red' }}>
+              {ymlError}
+            </div>
+
+            <TextField
+              multiline
+              placeholder='Paste your docker-compose.yml here or use the file upload button.'
+              fullWidth
+              value={dockerCompose}
+              onChange={(e) => setDockerCompose(e.target.value)}
+              sx={preStyle}
+              InputProps={{
+                sx: {
+                  color: '#EEE',
+                }
+              }}
+              rows={20}></TextField>
+          </Stack>}
+          {step === 1 && <Stack spacing={2}>
+            <NewDockerService service={service} refresh={refresh} />
+          </Stack>}
+        </DialogContentText>
+      </DialogContent>
+      {!isLoading && <DialogActions>
+        <Button onClick={() => {
+          setOpenModal(false);
+          setStep(0);
+          setDockerCompose('');
+          setYmlError('');
+        }}>Close</Button>
+        <Button onClick={() => {
+          if (step === 0) {
+            setStep(1);
+          } else {
+            setStep(0);
+          }
+        }}>
+          {step === 0 && 'Next'}
+          {step === 1 && 'Back'}
+        </Button>
+      </DialogActions>}
+    </Dialog>
+
+    <ResponsiveButton
+      color="primary"
+      onClick={() => setOpenModal(true)}
+      variant="outlined"
+      startIcon={<ArrowUpOutlined />}
+    >
+      Import Docker Compose
+    </ResponsiveButton>
+  </>;
 };
 
 export default DockerComposeImport;

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cosmos-server",
-  "version": "0.5.0-unstable",
+  "version": "0.5.10",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "cosmos-server",
-      "version": "0.5.0-unstable",
+      "version": "0.5.10",
       "dependencies": {
         "@ant-design/colors": "^6.0.0",
         "@ant-design/icons": "^4.7.0",

+ 4 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "cosmos-server",
-  "version": "0.5.10",
+  "version": "0.5.11",
   "description": "",
   "main": "test-server.js",
   "bugs": {
@@ -56,10 +56,10 @@
     "client": "vite",
     "client-build": "vite build --base=/ui/",
     "start": "env COSMOS_HOSTNAME=localhost CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
-    "build": " sh build.sh",
+    "build": "sh build.sh",
     "dev": "npm run build && npm run start",
-    "dockerdevbuild": "sh build.sh && npm run client-build && docker build --tag cosmos-dev .",
-    "dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
+    "dockerdevbuild": "sh build.sh && docker build --tag cosmos-dev .",
+    "dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host  --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
     "dockerdev": "npm run dockerdevbuild && npm run dockerdevrun",
     "demo": "vite build --base=/ui/ --mode demo",
     "devdemo": "vite --mode demo"

+ 1 - 1
readme.md

@@ -110,7 +110,7 @@ Authentication is very hard (how do you check the password match? What encryptio
 Installation is simple using Docker:
 
 ```
-docker run -d -p 80:80 -p 443:443 --name cosmos-server -h cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/cosmos:/config azukaar/cosmos-server:latest
+docker run -d -p 80:80 -p 443:443 --name cosmos-server -h cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /:/mnt/host -v /var/lib/cosmos:/config azukaar/cosmos-server:latest
 ```
 
 Once installed, simply go to `http://your-server-ip` and follow the instructions of the setup wizard.

+ 80 - 12
src/docker/api_blueprint.go

@@ -22,6 +22,12 @@ import (
 	"github.com/azukaar/cosmos-server/src/utils"
 )
 
+type ContainerCreateRequestServiceNetwork struct {
+	Aliases []string `json:"aliases,omitempty"`
+	IPV4Address string `json:"ipv4_address,omitempty"`
+	IPV6Address string `json:"ipv6_address,omitempty"`
+}
+
 type ContainerCreateRequestContainer struct {
 	Name 			string            `json:"container_name"`
 	Image       string            `json:"image"`
@@ -29,12 +35,8 @@ type ContainerCreateRequestContainer struct {
 	Labels      map[string]string `json:"labels"`
 	Ports       []string          `json:"ports"`
 	Volumes     []mount.Mount          `json:"volumes"`
-	Networks    map[string]struct {
-		Aliases []string `json:"aliases,omitempty"`
-		IPV4Address string `json:"ipv4_address,omitempty"`
-		IPV6Address string `json:"ipv6_address,omitempty"`
-	} `json:"networks"`
-	Routes 			   []utils.ProxyRouteConfig          `json:"routes"`
+	Networks    map[string]ContainerCreateRequestServiceNetwork `json:"networks"`
+	Routes 			[]utils.ProxyRouteConfig          `json:"routes"`
 
 	RestartPolicy  string            `json:"restart,omitempty"`
 	Devices        []string          `json:"devices"`
@@ -100,7 +102,7 @@ type ContainerCreateRequestNetwork struct {
 
 type DockerServiceCreateRequest struct {
 	Services map[string]ContainerCreateRequestContainer `json:"services"`
-	Volumes []ContainerCreateRequestVolume `json:"volumes"`
+	Volumes map[string]ContainerCreateRequestVolume `json:"volumes"`
 	Networks map[string]ContainerCreateRequestNetwork `json:"networks"`
 }
 
@@ -137,6 +139,9 @@ func Rollback(actions []DockerServiceCreateRollback , w http.ResponseWriter, flu
 				flusher.Flush()
 			}
 		case "network":
+			if os.Getenv("HOSTNAME") != "" {
+				DockerClient.NetworkDisconnect(DockerContext, action.Name, os.Getenv("HOSTNAME"), true)
+			}
 			err := DockerClient.NetworkRemove(DockerContext, action.Name)
 			if err != nil {
 				utils.Error("Rollback: Network", err)
@@ -220,6 +225,48 @@ func CreateService(w http.ResponseWriter, req *http.Request, serviceRequest Dock
 	var rollbackActions []DockerServiceCreateRollback
 	var err error
 
+	// check if services have the cosmos-force-network-secured label
+	for serviceName, service := range serviceRequest.Services {
+		utils.Log(fmt.Sprintf("Checking service %s...", serviceName))
+		fmt.Fprintf(w, "Checking service %s...\n", serviceName)
+		flusher.Flush()
+
+		if service.Labels["cosmos-force-network-secured"] == "true" {
+			utils.Log(fmt.Sprintf("Forcing secure %s...", serviceName))
+			fmt.Fprintf(w, "Forcing secure %s...\n", serviceName)
+			flusher.Flush()
+	
+			newNetwork, errNC := CreateCosmosNetwork()
+			if errNC != nil {
+				utils.Error("CreateService: Network", err)
+				fmt.Fprintf(w, "[ERROR] Network %s cant be created\n", newNetwork)
+				flusher.Flush()
+				Rollback(rollbackActions, w, flusher)
+				return err
+			}
+
+			service.Labels["cosmos-network-name"] = newNetwork
+
+			AttachNetworkToCosmos(newNetwork)
+
+			if service.Networks == nil {
+				service.Networks = make(map[string]ContainerCreateRequestServiceNetwork)
+			}
+
+			service.Networks[newNetwork] = ContainerCreateRequestServiceNetwork{}
+
+			rollbackActions = append(rollbackActions, DockerServiceCreateRollback{
+				Action: "remove",
+				Type:   "network",
+				Name:   newNetwork,
+			})
+			
+			utils.Log(fmt.Sprintf("Created secure network %s", newNetwork))
+			fmt.Fprintf(w, "Created secure network %s\n", newNetwork)
+			flusher.Flush()
+		}
+	}
+
 	// Create networks
 	for networkToCreateName, networkToCreate := range serviceRequest.Networks {
 		utils.Log(fmt.Sprintf("Creating network %s...", networkToCreateName))
@@ -391,11 +438,32 @@ func CreateService(w http.ResponseWriter, req *http.Request, serviceRequest Dock
 		// Create missing folders for bind mounts
 		for _, newmount := range container.Volumes {
 			if newmount.Type == mount.TypeBind {
-				if _, err := os.Stat(newmount.Source); os.IsNotExist(err) {
-					err := os.MkdirAll(newmount.Source, 0755)
+				newSource := newmount.Source
+
+				if os.Getenv("HOSTNAME") != "" {
+					if _, err := os.Stat("/mnt/host"); os.IsNotExist(err) {
+						utils.Error("CreateService: Unable to create directory for bind mount in the host directory. Please mount the host / in Cosmos with  -v /:/mnt/host to enable folder creations, or create the bind folder yourself", err)
+						fmt.Fprintf(w, "[ERROR] Unable to create directory for bind mount in the host directory. Please mount the host / in Cosmos with  -v /:/mnt/host to enable folder creations, or create the bind folder yourself: "+err.Error())
+						flusher.Flush()
+						Rollback(rollbackActions, w, flusher)
+						return err
+					}
+					newSource = "/mnt/host" + newSource
+				}
+						
+				utils.Log(fmt.Sprintf("Checking directory %s for bind mount", newSource))
+				fmt.Fprintf(w, "Checking directory %s for bind mount\n", newSource)
+				flusher.Flush()
+
+				if _, err := os.Stat(newSource); os.IsNotExist(err) {
+					utils.Log(fmt.Sprintf("Not found. Creating directory %s for bind mount", newSource))
+					fmt.Fprintf(w, "Not found. Creating directory %s for bind mount\n", newSource)
+					flusher.Flush()
+	
+					err := os.MkdirAll(newSource, 0755)
 					if err != nil {
-						utils.Error("CreateService: Unable to create directory for bind mount", err)
-						fmt.Fprintf(w, "[ERROR] Unable to create directory for bind mount: "+err.Error())
+						utils.Error("CreateService: Unable to create directory for bind mount. Make sure parent directories exist, and that Cosmos has permissions to create directories in the host directory", err)
+						fmt.Fprintf(w, "[ERROR] Unable to create directory for bind mount. Make sure parent directories exist, and that Cosmos has permissions to create directories in the host directory for bind mount: "+err.Error())
 						flusher.Flush()
 						Rollback(rollbackActions, w, flusher)
 						return err
@@ -411,7 +479,7 @@ func CreateService(w http.ResponseWriter, req *http.Request, serviceRequest Dock
 						} else {
 							uid, _ := strconv.Atoi(userInfo.Uid)
 							gid, _ := strconv.Atoi(userInfo.Gid)
-							err = os.Chown(newmount.Source, uid, gid)
+							err = os.Chown(newSource, uid, gid)
 							if err != nil {
 								utils.Error("CreateService: Unable to change ownership of directory", err)
 								fmt.Fprintf(w, "[ERROR] Unable to change ownership of directory: "+err.Error())

+ 0 - 29
src/docker/api_updateContainer.go

@@ -4,8 +4,6 @@ import (
 	"encoding/json"
 	"net/http"
 	"os"
-	"os/user"
-	"strconv"
 
 	"github.com/azukaar/cosmos-server/src/utils"
 	containerType "github.com/docker/docker/api/types/container"
@@ -86,33 +84,6 @@ func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
 			}
 		}
 		if(form.Volumes != nil) {
-			// Create missing folders for bind mounts
-			for _, newmount := range form.Volumes {
-				if newmount.Type == mount.TypeBind {
-					if _, err := os.Stat(newmount.Source); os.IsNotExist(err) {
-						err := os.MkdirAll(newmount.Source, 0755)
-						if err != nil {
-							utils.Error("UpdateService: Unable to create directory for bind mount", err)
-							utils.HTTPError(w, "Unable to create directory for bind mount: "+err.Error(), http.StatusInternalServerError, "DS004")
-							return
-						}
-			
-						// Change the ownership of the directory to the container.User
-						userInfo, err := user.Lookup(container.Config.User)
-						if err != nil {
-							utils.Error("UpdateService: Unable to lookup user", err)
-						} else {
-							uid, _ := strconv.Atoi(userInfo.Uid)
-							gid, _ := strconv.Atoi(userInfo.Gid)
-							err = os.Chown(newmount.Source, uid, gid)
-							if err != nil {
-								utils.Error("UpdateService: Unable to change ownership of directory", err)
-							}
-						}	
-					}
-				}
-			}
-
 			container.HostConfig.Mounts = form.Volumes
 			container.HostConfig.Binds = []string{}
 		}

+ 48 - 0
src/docker/docker.go

@@ -5,12 +5,17 @@ import (
 	"errors"
 	"time"
 	"bufio"
+	"os"
+	"os/user"
+	"fmt"
 	"strings"
+	"strconv"
 	"github.com/azukaar/cosmos-server/src/utils" 
 
 	"github.com/docker/docker/client"
 	// natting "github.com/docker/go-connections/nat"
 	"github.com/docker/docker/api/types/container"
+	mountType "github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types"
 )
 
@@ -111,6 +116,49 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON, noLock
 	oldContainer := newConfig
 
 	if(oldContainerID != "") {
+		// create missing folders
+		
+		for _, newmount := range newConfig.HostConfig.Mounts {
+			if newmount.Type == mountType.TypeBind {
+				newSource := newmount.Source
+
+				if os.Getenv("HOSTNAME") != "" {
+					if _, err := os.Stat("/mnt/host"); os.IsNotExist(err) {
+						utils.Error("EditContainer: Unable to create directory for bind mount in the host directory. Please mount the host / in Cosmos with  -v /:/mnt/host to enable folder creations, or create the bind folder yourself", err)
+						return "", errors.New("Unable to create directory for bind mount in the host directory. Please mount the host / in Cosmos with  -v /:/mnt/host to enable folder creations, or create the bind folder yourself")
+					}
+					newSource = "/mnt/host" + newSource
+				}
+						
+				utils.Log(fmt.Sprintf("Checking directory %s for bind mount", newSource))
+
+				if _, err := os.Stat(newSource); os.IsNotExist(err) {
+					utils.Log(fmt.Sprintf("Not found. Creating directory %s for bind mount", newSource))
+	
+					err := os.MkdirAll(newSource, 0755)
+					if err != nil {
+						utils.Error("EditContainer: Unable to create directory for bind mount", err)
+						return "", errors.New("Unable to create directory for bind mount. Make sure parent directories exist, and that Cosmos has permissions to create directories in the host directory")
+					}
+		
+					if newConfig.Config.User != "" {
+						// Change the ownership of the directory to the container.User
+						userInfo, err := user.Lookup(newConfig.Config.User)
+						if err != nil {
+							utils.Error("EditContainer: Unable to lookup user", err)
+						} else {
+							uid, _ := strconv.Atoi(userInfo.Uid)
+							gid, _ := strconv.Atoi(userInfo.Gid)
+							err = os.Chown(newSource, uid, gid)
+							if err != nil {
+								utils.Error("EditContainer: Unable to change ownership of directory", err)
+							}
+						}	
+					}
+				}
+			}
+		}
+
 		utils.Log("EditContainer - Container updating. Retriveing currently running " + oldContainerID)
 
 		var err error

+ 9 - 2
src/proxy/routeTo.go

@@ -4,12 +4,13 @@ import (
 	"net/http"
 	"net/http/httputil" 
 	"net/url"
+	"crypto/tls"
 	spa "github.com/roberthodgen/spa-server"
 	"github.com/azukaar/cosmos-server/src/utils"
 )
 
 // NewProxy takes target host and creates a reverse proxy
-func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
+func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool) (*httputil.ReverseProxy, error) {
 	url, err := url.Parse(targetHost)
 	if err != nil {
 			return nil, err
@@ -17,6 +18,12 @@ func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
 
 	proxy := httputil.NewSingleHostReverseProxy(url)
 
+	if AcceptInsecureHTTPSTarget && url.Scheme == "https" {
+		proxy.Transport = &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		}
+	}
+
 	proxy.ModifyResponse = func(resp *http.Response) error {
 		utils.Debug("Response from backend: " + resp.Status)
 		utils.Debug("URL was " + resp.Request.URL.String())
@@ -35,7 +42,7 @@ func RouteTo(route utils.ProxyRouteConfig) http.Handler {
 	routeType := route.Mode
 
 	if(routeType == "SERVAPP" || routeType == "PROXY") {
-		proxy, err := NewProxy(destination)
+		proxy, err := NewProxy(destination, route.AcceptInsecureHTTPSTarget)
 		if err != nil {
 				utils.Error("Create Route", err)
 		}

+ 1 - 0
src/utils/types.go

@@ -148,6 +148,7 @@ type ProxyRouteConfig struct {
 	Mode ProxyMode
 	BlockCommonBots bool
 	BlockAPIAbuse bool
+	AcceptInsecureHTTPSTarget bool
 }
 
 type EmailConfig struct {

+ 4 - 1
vite.config.js

@@ -8,7 +8,6 @@ export default defineConfig({
   build: {
     outDir: '../static',
   },
-  // base: '/ui',
   server: {
     proxy: {
       '/cosmos/api': {
@@ -16,6 +15,10 @@ export default defineConfig({
         secure: false,
         ws: true,
       }
+    },
+    
+    watch: {
+      usePolling: true
     }
   }
 })