diff --git a/client/src/api/index.jsx b/client/src/api/index.jsx index 11a0a26..54f47f7 100644 --- a/client/src/api/index.jsx +++ b/client/src/api/index.jsx @@ -42,14 +42,63 @@ let isOnline = () => { }); } -let newInstall = (req) => { - return wrap(fetch('/cosmos/api/newInstall', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(req) - })) +let newInstall = (req, onProgress) => { + if(req.step == '2') { + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(req) + }; + + return fetch('/cosmos/api/newInstall', requestOptions) + .then(response => { + if (!response.ok) { + throw new Error(response.statusText); + } + + // The response body is a ReadableStream. This code reads the stream and passes chunks to the callback. + const reader = response.body.getReader(); + + // Read the stream and pass chunks to the callback as they arrive + return new ReadableStream({ + start(controller) { + function read() { + return reader.read().then(({ done, value }) => { + if (done) { + controller.close(); + return; + } + // Decode the UTF-8 text + let text = new TextDecoder().decode(value); + // Split by lines in case there are multiple lines in one chunk + let lines = text.split('\n'); + for (let line of lines) { + if (line) { + // Call the progress callback + onProgress(line); + } + } + controller.enqueue(value); + return read(); + }); + } + return read(); + } + }); + }).catch((e) => { + alert(e); + }); + } else { + return wrap(fetch('/cosmos/api/newInstall', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(req) + })) + } } const isDemo = import.meta.env.MODE === 'demo'; diff --git a/client/src/components/containers.jsx b/client/src/components/containers.jsx new file mode 100644 index 0000000..0554570 --- /dev/null +++ b/client/src/components/containers.jsx @@ -0,0 +1,9 @@ +import { WarningFilled } from "@ant-design/icons"; +import { Tooltip } from "@mui/material"; + +export const ContainerNetworkWarning = ({container}) => ( + container.HostConfig.NetworkMode != "bridge" && container.HostConfig.NetworkMode != "default" && + + + +); diff --git a/client/src/pages/newInstall/newInstall.jsx b/client/src/pages/newInstall/newInstall.jsx index ee3aa54..25d0a1c 100644 --- a/client/src/pages/newInstall/newInstall.jsx +++ b/client/src/pages/newInstall/newInstall.jsx @@ -14,6 +14,7 @@ import { useEffect, useState } from 'react'; import * as API from '../../api'; import { Formik } from 'formik'; +import LogsInModal from '../../components/logsInModal'; import { CosmosCheckbox, CosmosInputPassword, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts'; import AnimateButton from '../../components/@extended/AnimateButton'; import { Box } from '@mui/system'; @@ -25,6 +26,7 @@ const NewInstall = () => { const [counter, setCounter] = useState(0); let [hostname, setHostname] = useState(''); const [databaseEnable, setDatabaseEnable] = useState(true); + const [pullRequest, setPullRequest] = useState(null); const refreshStatus = async () => { try { @@ -118,27 +120,29 @@ const NewInstall = () => { validate={(values) => { }} onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { - try { - setSubmitting(true); - const res = await API.newInstall({ + setSubmitting(true); + + setPullRequest(() => ((cb) => { + API.newInstall({ step: "2", MongoDBMode: values.DBMode, MongoDB: values.MongoDB, - }); - if(res.status == "OK") { - if(values.DBMode === "DisableUserManagement") { - setDatabaseEnable(false); - } - setStatus({ success: true }); - } - } catch (error) { - setStatus({ success: false }); - setErrors({ submit: error.message }); - setSubmitting(false); - } + }, cb) + })); }}> {(formik) => (
+ { + if(formik.values.DBMode === "DisableUserManagement") { + setDatabaseEnable(false); + } + formik.setStatus({ success: true }); + formik.setSubmitting(false); + }} + /> theme.breakpoints.down('xsm')); const [pullRequest, setPullRequest] = React.useState(null); const [isUpdating, setIsUpdating] = React.useState(false); - console.log(isMiniMobile) - + const doTo = (action) => { setIsUpdating(true); diff --git a/client/src/pages/servapps/containers/docker-compose.jsx b/client/src/pages/servapps/containers/docker-compose.jsx index a1d701a..2b7b7f9 100644 --- a/client/src/pages/servapps/containers/docker-compose.jsx +++ b/client/src/pages/servapps/containers/docker-compose.jsx @@ -116,6 +116,30 @@ const DockerComposeImport = ({refresh}) => { 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)) { + let labels = {}; + doc.services[key].labels.forEach((label) => { + const [key, value] = label.split('='); + labels[''+key] = ''+value; + }); + doc.services[key].labels = labels; + } + } + + + // convert network + if(doc.services[key].networks) { + if(Array.isArray(doc.services[key].networks)) { + let networks = {}; + doc.services[key].networks.forEach((network) => { + networks[''+network] = {}; + }); + doc.services[key].networks = networks; + } + } }); } diff --git a/client/src/pages/servapps/containers/network.jsx b/client/src/pages/servapps/containers/network.jsx index fdb3189..3345d39 100644 --- a/client/src/pages/servapps/containers/network.jsx +++ b/client/src/pages/servapps/containers/network.jsx @@ -67,6 +67,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
{ return { port: port.split('/')[0], @@ -237,6 +238,14 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O + + + {networks && {Object.keys(containerInfo.NetworkSettings.Networks).map((networkName) => { const network = networks.find((n) => n.Name === networkName); diff --git a/client/src/pages/servapps/containers/newServiceForm.jsx b/client/src/pages/servapps/containers/newServiceForm.jsx index 1f15a04..a2fae05 100644 --- a/client/src/pages/servapps/containers/newServiceForm.jsx +++ b/client/src/pages/servapps/containers/newServiceForm.jsx @@ -99,7 +99,7 @@ const NewDockerServiceForm = () => { variant="contained" fullWidth endIcon={} - disabled={currentTab === 4} + disabled={(currentTab === 4) || containerInfo.Name === '' || containerInfo.Config.Image === ''} onClick={() => { setCurrentTab(currentTab + 1); setMaxTab(Math.max(currentTab + 1, maxTab)); diff --git a/client/src/pages/servapps/containers/overview.jsx b/client/src/pages/servapps/containers/overview.jsx index 9822372..f144211 100644 --- a/client/src/pages/servapps/containers/overview.jsx +++ b/client/src/pages/servapps/containers/overview.jsx @@ -108,8 +108,8 @@ const ContainerOverview = ({ containerInfo, config, refresh }) => { )} Image
{Image}
- Name -
{Name}
+ ID +
{containerInfo.Id}
IP Address
{IPAddress}
diff --git a/client/src/pages/servapps/containers/setup.jsx b/client/src/pages/servapps/containers/setup.jsx index 336d2d7..d075803 100644 --- a/client/src/pages/servapps/containers/setup.jsx +++ b/client/src/pages/servapps/containers/setup.jsx @@ -61,6 +61,9 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont if (!values.image) { errors.image = 'Required'; } + if (!values.name && newContainer) { + errors.name = 'Required'; + } // env keys and labels key mustbe unique const envKeys = values.envVars.map((envVar) => envVar.key); const labelKeys = values.labels.map((label) => label.key); diff --git a/client/src/pages/servapps/servapps.jsx b/client/src/pages/servapps/servapps.jsx index 79e33b4..a29ff34 100644 --- a/client/src/pages/servapps/servapps.jsx +++ b/client/src/pages/servapps/servapps.jsx @@ -1,5 +1,5 @@ // material-ui -import { AppstoreAddOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, PlusCircleOutlined, ReloadOutlined, RollbackOutlined, SearchOutlined, SettingOutlined, StopOutlined, UpCircleOutlined, UpSquareFilled } from '@ant-design/icons'; +import { AlertFilled, AppstoreAddOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, PlusCircleOutlined, ReloadOutlined, RollbackOutlined, SearchOutlined, SettingOutlined, StopOutlined, UpCircleOutlined, UpSquareFilled, WarningFilled } from '@ant-design/icons'; import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, IconButton, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material'; import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; import { Stack } from '@mui/system'; @@ -18,6 +18,7 @@ import ExposeModal from './exposeModal'; import GetActions from './actionBar'; import ResponsiveButton from '../../components/responseiveButton'; import DockerComposeImport from './containers/docker-compose'; +import { ContainerNetworkWarning } from '../../components/containers'; const Item = styled(Paper)(({ theme }) => ({ backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', @@ -185,7 +186,7 @@ const ServeApps = () => { - {app.Names[0].replace('/', '')}  + {app.Names[0].replace('/', '')}  {app.Image} @@ -251,7 +252,7 @@ const ServeApps = () => { refreshServeApps(); }) }} - /> Force Secure Network + /> Force Secure Network 0 { - containerConfig.Healthcheck = &conttype.HealthConfig{ - Test: container.HealthCheck.Test, - Interval: time.Duration(container.HealthCheck.Interval) * time.Second, - Timeout: time.Duration(container.HealthCheck.Timeout) * time.Second, - StartPeriod: time.Duration(container.HealthCheck.StartPeriod) * time.Second, - Retries: container.HealthCheck.Retries, - } - } - - // For Devices - devices := []conttype.DeviceMapping{} - - for _, device := range container.Devices { - deviceSplit := strings.Split(device, ":") - devices = append(devices, conttype.DeviceMapping{ - PathOnHost: deviceSplit[0], - PathInContainer: deviceSplit[1], - CgroupPermissions: "rwm", // This can be "r", "w", "m", or any combination - }) - } - - hostConfig.Devices = devices - - - networkingConfig := &network.NetworkingConfig{ - EndpointsConfig: make(map[string]*network.EndpointSettings), - } - - for netName, netConfig := range container.Networks { - networkingConfig.EndpointsConfig[netName] = &network.EndpointSettings{ - Aliases: netConfig.Aliases, - IPAddress: netConfig.IPV4Address, - GlobalIPv6Address: netConfig.IPV6Address, - } - } - - _, err = DockerClient.ContainerCreate(DockerContext, containerConfig, hostConfig, networkingConfig, nil, container.Name) - - if err != nil { - utils.Error("CreateService: Rolling back changes because of -- Container", err) - fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Container creation error: "+err.Error()) - flusher.Flush() - Rollback(rollbackActions, w, flusher) - return - } - - rollbackActions = append(rollbackActions, DockerServiceCreateRollback{ - Action: "remove", - Type: "container", - Name: container.Name, - }) - - // add routes - for _, route := range container.Routes { - // check if route already exists - exists := false - for _, configRoute := range configRoutes { - if configRoute.Name == route.Name { - exists = true - break - } - } - if !exists { - configRoutes = append([]utils.ProxyRouteConfig{(utils.ProxyRouteConfig)(route)}, configRoutes...) - } else { - utils.Error("CreateService: Rolling back changes because of -- Route already exist", nil) - fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Route already exist") - flusher.Flush() - Rollback(rollbackActions, w, flusher) - return - } - } - - // Write a response to the client - utils.Log(fmt.Sprintf("Container %s created", container.Name)) - fmt.Fprintf(w, "Container %s created\n", container.Name) - flusher.Flush() - } - - // re-order containers dpeneding on depends_on - startOrder, err := ReOrderServices(serviceRequest.Services) - if err != nil { - utils.Error("CreateService: Rolling back changes because of -- Container", err) - fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Container creation error: "+err.Error()) - flusher.Flush() - Rollback(rollbackActions, w, flusher) - return - } - - // Start all the newly created containers - for _, container := range startOrder { - err = DockerClient.ContainerStart(DockerContext, container.Name, doctype.ContainerStartOptions{}) - if err != nil { - utils.Error("CreateService: Start Container", err) - fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Container start error: "+err.Error()) - flusher.Flush() - Rollback(rollbackActions, w, flusher) - return - } - - // Write a response to the client - utils.Log(fmt.Sprintf("Container %s started", container.Name)) - fmt.Fprintf(w, "Container %s started\n", container.Name) - flusher.Flush() - } - - // Save the route configs - config.HTTPConfig.ProxyConfig.Routes = configRoutes - utils.SaveConfigTofile(config) - utils.NeedsRestart = true - - // After all operations - utils.Log("CreateService: Operation succeeded. SERVICE STARTED") - fmt.Fprintf(w, "[OPERATION SUCCEEDED]. SERVICE STARTED\n") - flusher.Flush() + CreateService(w, req, serviceRequest) } else { utils.Error("CreateService: Method not allowed" + req.Method, nil) utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") @@ -545,6 +195,375 @@ func CreateServiceRoute(w http.ResponseWriter, req *http.Request) { } } + +func CreateService(w http.ResponseWriter, req *http.Request, serviceRequest DockerServiceCreateRequest) error { + // Enable streaming of response by setting appropriate headers + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Transfer-Encoding", "chunked") + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) + return errors.New("Streaming unsupported!") + } + + utils.ConfigLock.Lock() + defer utils.ConfigLock.Unlock() + + utils.Log("Starting creation of new service...") + fmt.Fprintf(w, "Starting creation of new service...\n") + flusher.Flush() + + config := utils.ReadConfigFromFile() + configRoutes := config.HTTPConfig.ProxyConfig.Routes + + var rollbackActions []DockerServiceCreateRollback + var err error + + // Create networks + for networkToCreateName, networkToCreate := range serviceRequest.Networks { + utils.Log(fmt.Sprintf("Creating network %s...", networkToCreateName)) + fmt.Fprintf(w, "Creating network %s...\n", networkToCreateName) + flusher.Flush() + + // check if network already exists + _, err = DockerClient.NetworkInspect(DockerContext, networkToCreateName, doctype.NetworkInspectOptions{}) + if err == nil { + utils.Error("CreateService: Network", err) + fmt.Fprintf(w, "[ERROR] Network %s already exists\n", networkToCreateName) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + + ipamConfig := make([]network.IPAMConfig, len(networkToCreate.IPAM.Config)) + if networkToCreate.IPAM.Config != nil { + for i, config := range networkToCreate.IPAM.Config { + ipamConfig[i] = network.IPAMConfig{ + Subnet: config.Subnet, + } + } + } + + _, err = DockerClient.NetworkCreate(DockerContext, networkToCreateName, doctype.NetworkCreate{ + Driver: networkToCreate.Driver, + Attachable: networkToCreate.Attachable, + Internal: networkToCreate.Internal, + EnableIPv6: networkToCreate.EnableIPv6, + IPAM: &network.IPAM{ + Driver: networkToCreate.IPAM.Driver, + Config: ipamConfig, + }, + }) + + if err != nil { + utils.Error("CreateService: Rolling back changes because of -- Network", err) + fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Network creation error: "+err.Error()) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + + rollbackActions = append(rollbackActions, DockerServiceCreateRollback{ + Action: "remove", + Type: "network", + Name: networkToCreateName, + }) + + // Write a response to the client + utils.Log(fmt.Sprintf("Network %s created", networkToCreateName)) + fmt.Fprintf(w, "Network %s created\n", networkToCreateName) + flusher.Flush() + } + + // Create volumes + for _, volume := range serviceRequest.Volumes { + utils.Log(fmt.Sprintf("Creating volume %s...", volume.Name)) + fmt.Fprintf(w, "Creating volume %s...\n", volume.Name) + flusher.Flush() + + _, err = DockerClient.VolumeCreate(DockerContext, volumetype.CreateOptions{ + Driver: volume.Driver, + Name: volume.Name, + }) + + if err != nil { + utils.Error("CreateService: Rolling back changes because of -- Volume", err) + fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Volume creation error: "+err.Error()) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + + rollbackActions = append(rollbackActions, DockerServiceCreateRollback{ + Action: "remove", + Type: "volume", + Name: volume.Name, + }) + + // Write a response to the client + utils.Log(fmt.Sprintf("Volume %s created", volume.Name)) + fmt.Fprintf(w, "Volume %s created\n", volume.Name) + flusher.Flush() + } + + // pull images + for _, container := range serviceRequest.Services { + // Write a response to the client + utils.Log(fmt.Sprintf("Pulling image %s", container.Image)) + fmt.Fprintf(w, "Pulling image %s\n", container.Image) + flusher.Flush() + + out, err := DockerClient.ImagePull(DockerContext, container.Image, doctype.ImagePullOptions{}) + if err != nil { + utils.Error("CreateService: Rolling back changes because of -- Image pull", err) + fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Image pull error: "+err.Error()) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + defer out.Close() + + // wait for image pull to finish + scanner := bufio.NewScanner(out) + for scanner.Scan() { + fmt.Fprintf(w, "%s\n", scanner.Text()) + flusher.Flush() + } + + // Write a response to the client + utils.Log(fmt.Sprintf("Image %s pulled", container.Image)) + fmt.Fprintf(w, "Image %s pulled\n", container.Image) + flusher.Flush() + } + + // Create containers + for _, container := range serviceRequest.Services { + utils.Log(fmt.Sprintf("Creating container %s...", container.Name)) + fmt.Fprintf(w, "Creating container %s...\n", container.Name) + flusher.Flush() + + containerConfig := &conttype.Config{ + Image: container.Image, + Env: container.Environment, + Labels: container.Labels, + ExposedPorts: nat.PortSet{}, + WorkingDir: container.WorkingDir, + User: container.User, + Hostname: container.Hostname, + Domainname: container.Domainname, + MacAddress: container.MacAddress, + StopSignal: container.StopSignal, + StopTimeout: &container.StopGracePeriod, + Tty: container.Tty, + OpenStdin: container.StdinOpen, + } + + if container.Command != "" { + containerConfig.Cmd = strings.Fields(container.Command) + } + + if container.Entrypoint != "" { + containerConfig.Entrypoint = strslice.StrSlice(strings.Fields(container.Entrypoint)) + } + + // For Expose / Ports + for _, expose := range container.Expose { + exposePort := nat.Port(expose) + containerConfig.ExposedPorts[exposePort] = struct{}{} + } + + PortBindings := nat.PortMap{} + + for _, port := range container.Ports { + portContainer := strings.Split(port, ":")[0] + portHost := strings.Split(port, ":")[1] + + containerConfig.ExposedPorts[nat.Port(portContainer)] = struct{}{} + PortBindings[nat.Port(portContainer)] = []nat.PortBinding{ + { + HostIP: "", + HostPort: portHost, + }, + } + } + + // 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) + 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()) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + + if container.User != "" { + // Change the ownership of the directory to the container.User + userInfo, err := user.Lookup(container.User) + if err != nil { + utils.Error("CreateService: Unable to lookup user", err) + fmt.Fprintf(w, "[ERROR] Unable to lookup user " + container.User + "." +err.Error()) + flusher.Flush() + } else { + uid, _ := strconv.Atoi(userInfo.Uid) + gid, _ := strconv.Atoi(userInfo.Gid) + err = os.Chown(newmount.Source, 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()) + flusher.Flush() + } + } + } + } + } + } + + hostConfig := &conttype.HostConfig{ + PortBindings: PortBindings, + Mounts: container.Volumes, + RestartPolicy: conttype.RestartPolicy{ + Name: container.RestartPolicy, + }, + Privileged: container.Privileged, + NetworkMode: conttype.NetworkMode(container.NetworkMode), + DNS: container.DNS, + DNSSearch: container.DNSSearch, + ExtraHosts: container.ExtraHosts, + Links: container.Links, + SecurityOpt: container.SecurityOpt, + StorageOpt: container.StorageOpt, + Sysctls: container.Sysctls, + Isolation: conttype.Isolation(container.Isolation), + CapAdd: container.CapAdd, + CapDrop: container.CapDrop, + } + + // For Healthcheck + if len(container.HealthCheck.Test) > 0 { + containerConfig.Healthcheck = &conttype.HealthConfig{ + Test: container.HealthCheck.Test, + Interval: time.Duration(container.HealthCheck.Interval) * time.Second, + Timeout: time.Duration(container.HealthCheck.Timeout) * time.Second, + StartPeriod: time.Duration(container.HealthCheck.StartPeriod) * time.Second, + Retries: container.HealthCheck.Retries, + } + } + + // For Devices + devices := []conttype.DeviceMapping{} + + for _, device := range container.Devices { + deviceSplit := strings.Split(device, ":") + devices = append(devices, conttype.DeviceMapping{ + PathOnHost: deviceSplit[0], + PathInContainer: deviceSplit[1], + CgroupPermissions: "rwm", // This can be "r", "w", "m", or any combination + }) + } + + hostConfig.Devices = devices + + + networkingConfig := &network.NetworkingConfig{ + EndpointsConfig: make(map[string]*network.EndpointSettings), + } + + for netName, netConfig := range container.Networks { + networkingConfig.EndpointsConfig[netName] = &network.EndpointSettings{ + Aliases: netConfig.Aliases, + IPAddress: netConfig.IPV4Address, + GlobalIPv6Address: netConfig.IPV6Address, + } + } + + _, err = DockerClient.ContainerCreate(DockerContext, containerConfig, hostConfig, networkingConfig, nil, container.Name) + + if err != nil { + utils.Error("CreateService: Rolling back changes because of -- Container", err) + fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Container creation error: "+err.Error()) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + + rollbackActions = append(rollbackActions, DockerServiceCreateRollback{ + Action: "remove", + Type: "container", + Name: container.Name, + }) + + // add routes + for _, route := range container.Routes { + // check if route already exists + exists := false + for _, configRoute := range configRoutes { + if configRoute.Name == route.Name { + exists = true + break + } + } + if !exists { + configRoutes = append([]utils.ProxyRouteConfig{(utils.ProxyRouteConfig)(route)}, configRoutes...) + } else { + utils.Error("CreateService: Rolling back changes because of -- Route already exist", nil) + fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Route already exist") + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return errors.New("Route already exist") + } + } + + // Write a response to the client + utils.Log(fmt.Sprintf("Container %s created", container.Name)) + fmt.Fprintf(w, "Container %s created\n", container.Name) + flusher.Flush() + } + + // re-order containers dpeneding on depends_on + startOrder, err := ReOrderServices(serviceRequest.Services) + if err != nil { + utils.Error("CreateService: Rolling back changes because of -- Container", err) + fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Container creation error: "+err.Error()) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + + // Start all the newly created containers + for _, container := range startOrder { + err = DockerClient.ContainerStart(DockerContext, container.Name, doctype.ContainerStartOptions{}) + if err != nil { + utils.Error("CreateService: Start Container", err) + fmt.Fprintf(w, "[ERROR] Rolling back changes because of -- Container start error: "+err.Error()) + flusher.Flush() + Rollback(rollbackActions, w, flusher) + return err + } + + // Write a response to the client + utils.Log(fmt.Sprintf("Container %s started", container.Name)) + fmt.Fprintf(w, "Container %s started\n", container.Name) + flusher.Flush() + } + + // Save the route configs + config.HTTPConfig.ProxyConfig.Routes = configRoutes + utils.SaveConfigTofile(config) + utils.NeedsRestart = true + + // After all operations + utils.Log("CreateService: Operation succeeded. SERVICE STARTED") + fmt.Fprintf(w, "[OPERATION SUCCEEDED]. SERVICE STARTED\n") + flusher.Flush() + + return nil +} + func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]ContainerCreateRequestContainer, error) { startOrder := []ContainerCreateRequestContainer{} diff --git a/src/docker/api_managecont.go b/src/docker/api_managecont.go index 710fd29..40e5000 100644 --- a/src/docker/api_managecont.go +++ b/src/docker/api_managecont.go @@ -68,7 +68,7 @@ func ManageContainerRoute(w http.ResponseWriter, req *http.Request) { case "unpause": err = DockerClient.ContainerUnpause(DockerContext, container.ID) case "recreate": - _, err = EditContainer(container.ID, container) + _, err = EditContainer(container.ID, container, false) case "update": out, errPull := DockerClient.ImagePull(DockerContext, imagename, doctype.ImagePullOptions{}) if errPull != nil { @@ -100,7 +100,7 @@ func ManageContainerRoute(w http.ResponseWriter, req *http.Request) { utils.Log("Container Update - Image pulled " + imagename) - _, err = EditContainer(container.ID, container) + _, err = EditContainer(container.ID, container, false) if err != nil { utils.Error("Container Update - EditContainer", err) diff --git a/src/docker/api_newDB.go b/src/docker/api_newDB.go index 576ff0f..142a7c2 100644 --- a/src/docker/api_newDB.go +++ b/src/docker/api_newDB.go @@ -20,7 +20,7 @@ func NewDBRoute(w http.ResponseWriter, req *http.Request) { } if(req.Method == "GET") { - costr, err := NewDB() + costr, err := NewDB(w, req) if err != nil { utils.Error("NewDB: Error while creating new DB", err) diff --git a/src/docker/api_secureContainer.go b/src/docker/api_secureContainer.go index 70050e7..2d7478e 100644 --- a/src/docker/api_secureContainer.go +++ b/src/docker/api_secureContainer.go @@ -37,9 +37,12 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) { "cosmos-force-network-secured": status, }); + // change network mode to bridge in case it was set to container + container.HostConfig.NetworkMode = "bridge" + utils.Log("API: Set Force network secured "+status+" : " + containerName) - _, errEdit := EditContainer(container.ID, container) + _, errEdit := EditContainer(container.ID, container, false) if errEdit != nil { utils.Error("ContainerSecureEdit", errEdit) utils.HTTPError(w, "Internal server error: " + errEdit.Error(), http.StatusInternalServerError, "DS003") diff --git a/src/docker/api_updateContainer.go b/src/docker/api_updateContainer.go index 929af71..33f9a84 100644 --- a/src/docker/api_updateContainer.go +++ b/src/docker/api_updateContainer.go @@ -23,6 +23,7 @@ type ContainerForm struct { Volumes []mount.Mount `json:"Volumes"` // we make this a int so that we can ignore 0 Interactive int `json:"interactive"` + NetworkMode string `json:"networkMode"` } func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) { @@ -119,8 +120,11 @@ func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) { container.Config.Tty = form.Interactive == 2 container.Config.OpenStdin = form.Interactive == 2 } + if(form.NetworkMode != "") { + container.HostConfig.NetworkMode = containerType.NetworkMode(form.NetworkMode) + } - _, err = EditContainer(container.ID, container) + _, err = EditContainer(container.ID, container, false) if err != nil { utils.Error("UpdateContainer: EditContainer", err) utils.HTTPError(w, "Internal server error: "+err.Error(), http.StatusInternalServerError, "DS004") diff --git a/src/docker/bootstrap.go b/src/docker/bootstrap.go index c4c3475..e5ccae4 100644 --- a/src/docker/bootstrap.go +++ b/src/docker/bootstrap.go @@ -31,6 +31,13 @@ func BootstrapAllContainersFromTags() []error { return errors } +func UnsecureContainer(container types.ContainerJSON) (string, error) { + RemoveLabels(container, []string{ + "cosmos-force-network-secured", + }); + return EditContainer(container.ID, container, false) +} + func BootstrapContainerFromTags(containerID string) error { errD := Connect() if errD != nil { @@ -71,6 +78,12 @@ func BootstrapContainerFromTags(containerID string) error { if !isCosmosCon { needsRestart, errCT = ConnectToSecureNetwork(container) if errCT != nil { + utils.Warn("DockerContainerBootstrapConnectToSecureNetwork -- Cannot connect to network, removing force secure") + _, errUn := UnsecureContainer(container) + if errUn != nil { + utils.Fatal("DockerContainerBootstrapUnsecureContainer -- A broken container state is preventing Cosmos from functionning. Please remove the cosmos-force-secure label from the container "+container.Name+" manually", errUn) + return errCT + } return errCT } if needsRestart { @@ -84,7 +97,12 @@ func BootstrapContainerFromTags(containerID string) error { utils.Log(container.Name+": Disconnecting from bridge network") errDisc := DockerClient.NetworkDisconnect(DockerContext, "bridge", containerID, true) if errDisc != nil { - utils.Error("Docker Network Disconnect", errDisc) + utils.Warn("DockerContainerBootstrapDisconnectFromBridge -- Cannot disconnect from Bridge, removing force secure") + _, errUn := UnsecureContainer(container) + if errUn != nil { + utils.Fatal("DockerContainerBootstrapUnsecureContainer -- A broken container state is preventing Cosmos from functionning. Please remove the cosmos-force-secure label from the container "+container.Name+" manually", errUn) + return errDisc + } return errDisc } } @@ -99,7 +117,7 @@ func BootstrapContainerFromTags(containerID string) error { } if(needsUpdate) { - _, errEdit := EditContainer(containerID, container) + _, errEdit := EditContainer(containerID, container, false) if errEdit != nil { utils.Error("Docker Boostrap, couldn't update container: ", errEdit) return errEdit diff --git a/src/docker/docker.go b/src/docker/docker.go index c1de833..3733428 100644 --- a/src/docker/docker.go +++ b/src/docker/docker.go @@ -4,7 +4,6 @@ import ( "context" "errors" "time" - "fmt" "bufio" "strings" "github.com/azukaar/cosmos-server/src/utils" @@ -82,11 +81,8 @@ func Connect() error { return nil } -func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string, error) { - - utils.Debug("VOLUMES:" + fmt.Sprintf("%v", newConfig.HostConfig.Mounts)) - - if(oldContainerID != "") { +func EditContainer(oldContainerID string, newConfig types.ContainerJSON, noLock bool) (string, error) { + if(oldContainerID != "" && !noLock) { // no need to re-lock if we are reverting DockerNetworkLock <- true defer func() { @@ -99,6 +95,17 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string return "", errD } } + + if(newConfig.HostConfig.NetworkMode != "bridge" && + newConfig.HostConfig.NetworkMode != "default" && + newConfig.HostConfig.NetworkMode != "host" && + newConfig.HostConfig.NetworkMode != "none") { + if(!HasLabel(newConfig, "cosmos-force-network-mode")) { + AddLabels(newConfig, map[string]string{"cosmos-force-network-mode": string(newConfig.HostConfig.NetworkMode)}) + } else { + newConfig.HostConfig.NetworkMode = container.NetworkMode(GetLabel(newConfig, "cosmos-force-network-mode")) + } + } newName := newConfig.Name oldContainer := newConfig @@ -112,8 +119,6 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string // https://godoc.org/github.com/docker/docker/api/types#ContainerJSON oldContainer, err = DockerClient.ContainerInspect(DockerContext, oldContainerID) - utils.Debug("OLD VOLUMES:" + fmt.Sprintf("%v", oldContainer.HostConfig.Mounts)) - if err != nil { return "", err } @@ -138,7 +143,6 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string // if no name, use the same one, that will force Docker to create a hostname if not set newName = oldContainer.Name - newConfig.Config.Hostname = newName // stop and remove container stopError := DockerClient.ContainerStop(DockerContext, oldContainerID, container.StopOptions{}) @@ -168,6 +172,16 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string utils.Log("EditContainer - Revert started") } + // only force hostname if network is bridge or default, otherwise it will fail + if newConfig.HostConfig.NetworkMode == "bridge" || newConfig.HostConfig.NetworkMode == "default" { + newConfig.Config.Hostname = newName + } else { + // if not, remove hostname because otherwise it will try to keep the old one + newConfig.Config.Hostname = "" + // IDK Docker is weird, if you don't erase this it will break + newConfig.Config.ExposedPorts = nil + } + // recreate container with new informations createResponse, createError := DockerClient.ContainerCreate( DockerContext, @@ -177,7 +191,10 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string nil, newName, ) - + if createError != nil { + utils.Error("EditContainer - Failed to create container", createError) + } + utils.Log("EditContainer - Container recreated. Re-connecting networks " + createResponse.ID) // is force secure @@ -202,6 +219,10 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string runError := DockerClient.ContainerStart(DockerContext, createResponse.ID, types.ContainerStartOptions{}) + if runError != nil { + utils.Error("EditContainer - Failed to run container", runError) + } + if createError != nil || runError != nil { if(oldContainerID == "") { if(createError == nil) { @@ -217,12 +238,17 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string if(createError == nil) { utils.Log("EditContainer - Killing new broken container") + // attempt kill + DockerClient.ContainerKill(DockerContext, oldContainerID, "") DockerClient.ContainerKill(DockerContext, createResponse.ID, "") + // attempt remove in case created state + DockerClient.ContainerRemove(DockerContext, oldContainerID, types.ContainerRemoveOptions{}) + DockerClient.ContainerRemove(DockerContext, createResponse.ID, types.ContainerRemoveOptions{}) } utils.Log("EditContainer - Reverting...") // attempt to restore container - restored, restoreError := EditContainer("", oldContainer) + restored, restoreError := EditContainer("", oldContainer, false) if restoreError != nil { utils.Error("EditContainer - Failed to restore container", restoreError) @@ -245,12 +271,48 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string return restored, errors.New("Failed to edit container, but restored to previous state. Error was: " + errorWas) } } + + // Recreating dependant containers + utils.Debug("Unlocking EDIT Container") + + if oldContainerID != "" { + RecreateDepedencies(oldContainerID) + } utils.Log("EditContainer - Container started. All done! " + createResponse.ID) return createResponse.ID, nil } +func RecreateDepedencies(containerID string) { + containers, err := ListContainers() + if err != nil { + utils.Error("RecreateDepedencies", err) + return + } + + for _, container := range containers { + if container.ID == containerID { + continue + } + + fullContainer, err := DockerClient.ContainerInspect(DockerContext, container.ID) + if err != nil { + utils.Error("RecreateDepedencies", err) + continue + } + + // check if network mode contains containerID + if strings.Contains(string(fullContainer.HostConfig.NetworkMode), containerID) { + utils.Log("RecreateDepedencies - Recreating " + container.Names[0]) + _, err := EditContainer(container.ID, fullContainer, true) + if err != nil { + utils.Error("RecreateDepedencies - Failed to update - ", err) + } + } + } +} + func ListContainers() ([]types.Container, error) { errD := Connect() if errD != nil { @@ -397,7 +459,7 @@ func CheckUpdatesAvailable() map[string]bool { if needsUpdate && IsLabel(fullContainer, "cosmos-auto-update") { utils.Log("Downlaoded new update for " + container.Image + " ready to install") - _, err := EditContainer(container.ID, fullContainer) + _, err := EditContainer(container.ID, fullContainer, false) if err != nil { utils.Error("CheckUpdatesAvailable - Failed to update - ", err) } else { diff --git a/src/docker/run.go b/src/docker/run.go index 2d8a6d9..10b79fd 100644 --- a/src/docker/run.go +++ b/src/docker/run.go @@ -4,6 +4,9 @@ import ( "github.com/azukaar/cosmos-server/src/utils" "io" "os" + "net/http" + + // "github.com/docker/docker/client" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types" @@ -18,13 +21,13 @@ type VolumeMount struct { Volume *types.Volume } -func NewDB() (string, error) { +func NewDB(w http.ResponseWriter, req *http.Request) (string, error) { id := utils.GenerateRandomString(3) mongoUser := "cosmos-" + utils.GenerateRandomString(5) mongoPass := utils.GenerateRandomString(24) monHost := "cosmos-mongo-" + id - imageName := "mongo:latest" + imageName := "mongo:5" // if CPU is missing AVX, use 4.4 if runtime.GOARCH == "amd64" && !cpu.X86.HasAVX { @@ -32,28 +35,36 @@ func NewDB() (string, error) { imageName = "mongo:4.4" } - err := RunContainer( - imageName, - monHost, - []string{ + service := DockerServiceCreateRequest{ + Services: map[string]ContainerCreateRequestContainer {}, + } + + service.Services[monHost] = ContainerCreateRequestContainer{ + Name: monHost, + Image: imageName, + RestartPolicy: "always", + Environment: []string{ "MONGO_INITDB_ROOT_USERNAME=" + mongoUser, "MONGO_INITDB_ROOT_PASSWORD=" + mongoPass, }, - []VolumeMount{ + Labels: map[string]string{ + "cosmos-force-network-secured": "true", + }, + Volumes: []mount.Mount{ { - Destination: "/data/db", - Volume: &types.Volume{ - Name: "cosmos-mongo-data-" + id, - }, + Type: mount.TypeVolume, + Source: "cosmos-mongo-data-" + id, + Target: "/data/db", }, { - Destination: "/data/configdb", - Volume: &types.Volume{ - Name: "cosmos-mongo-config-" + id, - }, + Type: mount.TypeVolume, + Source: "cosmos-mongo-config-" + id, + Target: "/data/configdb", }, }, - ) + }; + + err := CreateService(w, req, service) if err != nil { return "", err @@ -87,54 +98,13 @@ func RunContainer(imagename string, containername string, inputEnv []string, vol mounts = append(mounts, mount) } - // Define a PORT opening - // newport, err := natting.NewPort("tcp", port) - // if err != nil { - // fmt.Println("Unable to create docker port") - // return err - // } - - // Configured hostConfig: - // https://godoc.org/github.com/docker/docker/api/types/container#HostConfig hostConfig := &container.HostConfig{ - // PortBindings: natting.PortMap{ - // newport: []natting.PortBinding{ - // { - // HostIP: "0.0.0.0", - // HostPort: port, - // }, - // }, - // }, Mounts : mounts, RestartPolicy: container.RestartPolicy{ Name: "always", }, - // LogConfig: container.LogConfig{ - // Type: "json-file", - // Config: map[string]string{}, - // }, } - - // Define Network config - // https://godoc.org/github.com/docker/docker/api/types/network#NetworkingConfig - - - // networkConfig := &network.NetworkingConfig{ - // EndpointsConfig: map[string]*network.EndpointSettings{}, - // } - // gatewayConfig := &network.EndpointSettings{ - // Gateway: "gatewayname", - // } - // networkConfig.EndpointsConfig["bridge"] = gatewayConfig - - // Define ports to be exposed (has to be same as hostconfig.portbindings.newport) - // exposedPorts := map[natting.Port]struct{}{ - // newport: struct{}{}, - // } - - // Configuration - // https://godoc.org/github.com/docker/docker/api/types/container#Config config := &container.Config{ Image: imagename, Env: inputEnv, @@ -145,9 +115,6 @@ func RunContainer(imagename string, containername string, inputEnv []string, vol // ExposedPorts: exposedPorts, } - //archi := runtime.GOARCH - - // Creating the actual container. This is "nil,nil,nil" in every example. cont, err := DockerClient.ContainerCreate( DockerContext, config, @@ -162,7 +129,6 @@ func RunContainer(imagename string, containername string, inputEnv []string, vol return err } - // Run the actual container DockerClient.ContainerStart(DockerContext, cont.ID, types.ContainerStartOptions{}) utils.Log("Container created " + cont.ID) diff --git a/src/httpServer.go b/src/httpServer.go index 5066465..d62bb36 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -190,7 +190,7 @@ func StartServer() { // need rewrite bc it catches too many things and prevent // client to be notified of the error - // router.Use(middleware.Recoverer) + router.Use(middleware.Logger) router.Use(utils.SetSecurityHeaders) diff --git a/src/icons.go b/src/icons.go index d47e29e..5a48af1 100644 --- a/src/icons.go +++ b/src/icons.go @@ -123,7 +123,7 @@ func GetFavicon(w http.ResponseWriter, req *http.Request) { iconURL := icon.URL u, err := url.Parse(siteurl) if err != nil { - utils.Error("FaviconFetch failed to parse ", err) + utils.Debug("FaviconFetch failed to parse " + err.Error()) continue } @@ -146,21 +146,21 @@ func GetFavicon(w http.ResponseWriter, req *http.Request) { } } - utils.Log("Favicon Trying to fetch " + iconURL) + utils.Debug("Favicon Trying to fetch " + iconURL) // Fetch the favicon resp, err := http.Get(iconURL) if err != nil { - utils.Error("FaviconFetch", err) + utils.Debug("FaviconFetch" + err.Error()) continue } // check if 200 and if image if resp.StatusCode != 200 { - utils.Error("FaviconFetch - " + iconURL + " - not 200 ", nil) + utils.Debug("FaviconFetch - " + iconURL + " - not 200 ") continue } else if !strings.Contains(resp.Header.Get("Content-Type"), "image") && !strings.Contains(resp.Header.Get("Content-Type"), "octet-stream") { - utils.Error("FaviconFetch - " + iconURL + " - not image ", nil) + utils.Debug("FaviconFetch - " + iconURL + " - not image ") continue } else { utils.Log("Favicon found " + iconURL) @@ -168,7 +168,7 @@ func GetFavicon(w http.ResponseWriter, req *http.Request) { // Cache the response body, err := ioutil.ReadAll(resp.Body) if err != nil { - utils.Error("FaviconFetch - cant read ", err) + utils.Debug("FaviconFetch - cant read " + err.Error()) continue } diff --git a/src/newInstall.go b/src/newInstall.go index f53eed5..1eff39b 100644 --- a/src/newInstall.go +++ b/src/newInstall.go @@ -85,13 +85,13 @@ func NewInstallRoute(w http.ResponseWriter, req *http.Request) { } else if (request.MongoDBMode == "Create") { utils.Log("NewInstall: Create DB") newConfig.DisableUserManagement = false - strco, err := docker.NewDB() + + strco, err := docker.NewDB(w, req) if err != nil { utils.Error("NewInstall: Error creating MongoDB", err) - utils.HTTPError(w, "New Install: Error creating MongoDB " + err.Error(), - http.StatusInternalServerError, "NI001") return - } + } + newConfig.MongoDB = strco utils.SaveConfigTofile(newConfig) utils.LoadBaseMainConfig(newConfig)