From 6a8e97b24213a033ec1080c49e8ecbac0979af44 Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Sun, 7 May 2023 17:47:20 +0100 Subject: [PATCH] [release] v0.4.0-unstable5 --- changelog.md | 1 + client/src/api/docker.jsx | 71 ++++- client/src/api/wrap.js | 1 + .../components/tableView/prettyTableView.jsx | 38 +-- .../authentication/auth-forms/AuthLogin.jsx | 38 +-- .../src/pages/servapps/containers/index.jsx | 28 +- .../src/pages/servapps/containers/network.jsx | 264 ++++++++++++++++++ .../src/pages/servapps/containers/setup.jsx | 241 ++++++++++++++++ .../src/pages/servapps/containers/volumes.jsx | 249 +++++++++++++++++ client/src/pages/servapps/createNetwork.jsx | 125 +++++++++ client/src/pages/servapps/createVolumes.jsx | 120 ++++++++ client/src/pages/servapps/networks.jsx | 77 ++--- client/src/pages/servapps/volumes.jsx | 4 + package.json | 2 +- src/docker/api_networks.go | 202 ++++++++++++++ src/docker/api_updateContainer.go | 96 +++++++ src/docker/api_volumes.go | 63 +++++ src/docker/docker.go | 168 +++++++---- src/docker/network.go | 26 +- src/httpServer.go | 7 +- src/index.go | 8 + src/utils/middleware.go | 12 +- 22 files changed, 1669 insertions(+), 172 deletions(-) create mode 100644 client/src/pages/servapps/containers/network.jsx create mode 100644 client/src/pages/servapps/containers/setup.jsx create mode 100644 client/src/pages/servapps/containers/volumes.jsx create mode 100644 client/src/pages/servapps/createNetwork.jsx create mode 100644 client/src/pages/servapps/createVolumes.jsx create mode 100644 src/docker/api_updateContainer.go diff --git a/changelog.md b/changelog.md index a532ef6..7ff529a 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ - Protect server against direct IP access - Improvements to installer to make it more robust - Fix bug where you can't complete the setup if you don't have a database + - When re-creating a container to edit it, restore the previous container if the edit is not succesful - Stop / Start / Restart / Remove / Kill containers ## Version 0.3.0 diff --git a/client/src/api/docker.jsx b/client/src/api/docker.jsx index 77db7d1..4ee3da7 100644 --- a/client/src/api/docker.jsx +++ b/client/src/api/docker.jsx @@ -91,15 +91,72 @@ const newDB = () => { })) } -const manageContainer = (id, action) => { - return wrap(fetch('/cosmos/api/servapps/' + id + '/manage/' + action, { +const manageContainer = (containerId, action) => { + return wrap(fetch('/cosmos/api/servapps/' + containerId + '/manage/' + action, { method: 'GET', headers: { 'Content-Type': 'application/json' } })) } - + +function updateContainer(containerId, values) { + return wrap(fetch('/cosmos/api/servapps/' + containerId + '/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(values), + })) +} + +function listContainerNetworks(containerId) { + return wrap(fetch('/cosmos/api/servapps/' + containerId + '/networks', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + })) +} + +function createNetwork(values) { + return wrap(fetch('/cosmos/api/networks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(values), + })) +} + +function attachNetwork(containerId, networkId) { + return wrap(fetch('/cosmos/api/servapps/' + containerId + '/network/' + networkId, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + })) +} + +function detachNetwork(containerId, networkId) { + return wrap(fetch('/cosmos/api/servapps/' + containerId + '/network/' + networkId, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + })) +} + +function createVolume(values) { + return wrap(fetch('/cosmos/api/volumes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(values), + })) +} + export { list, get, @@ -110,5 +167,11 @@ export { volumeDelete, networkList, networkDelete, - getContainerLogs + getContainerLogs, + updateContainer, + listContainerNetworks, + createNetwork, + attachNetwork, + detachNetwork, + createVolume, }; \ No newline at end of file diff --git a/client/src/api/wrap.js b/client/src/api/wrap.js index e55e5a2..c6b174e 100644 --- a/client/src/api/wrap.js +++ b/client/src/api/wrap.js @@ -15,6 +15,7 @@ export default function wrap(apicall) { snackit(rep.message); const e = new Error(rep.message); e.status = response.status; + e.code = rep.code; throw e; }); } diff --git a/client/src/components/tableView/prettyTableView.jsx b/client/src/components/tableView/prettyTableView.jsx index bb63856..731d9b6 100644 --- a/client/src/components/tableView/prettyTableView.jsx +++ b/client/src/components/tableView/prettyTableView.jsx @@ -11,7 +11,7 @@ import { SearchOutlined } from '@ant-design/icons'; import { useTheme } from '@mui/material/styles'; import { Link } from 'react-router-dom'; -const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => { +const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, fullWidth }) => { const [search, setSearch] = React.useState(''); const theme = useTheme(); const isDark = theme.palette.mode === 'dark'; @@ -25,23 +25,25 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => { } return ( - - - - - } - onChange={(e) => { - setSearch(e.target.value); - }} - /> - - + + + + + + } + onChange={(e) => { + setSearch(e.target.value); + }} + /> + {buttons} + + diff --git a/client/src/pages/authentication/auth-forms/AuthLogin.jsx b/client/src/pages/authentication/auth-forms/AuthLogin.jsx index 9df8cf4..026e0d8 100644 --- a/client/src/pages/authentication/auth-forms/AuthLogin.jsx +++ b/client/src/pages/authentication/auth-forms/AuthLogin.jsx @@ -99,30 +99,22 @@ const AuthLogin = () => { password: Yup.string().max(255).required('Password is required') })} onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { - try { - API.auth.login(values).then((data) => { - if(data.status == 'error') { - setStatus({ success: false }); - if(data.code == 'UL001') { - setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' }); - } else if (data.code == 'UL002') { - setErrors({ submit: 'You have not yet registered your account. You should have an invite link in your emails. If you need a new one, contact your administrator.' }); - } else if(data.status == 'error') { - setErrors({ submit: 'Unexpected error. Try again later.' }); - } - setSubmitting(false); - return; - } else { - setStatus({ success: true }); - setSubmitting(false); - window.location.href = redirectTo; - } - }) - } catch (err) { - setStatus({ success: false }); - setErrors({ submit: err.message }); + setSubmitting(true); + return API.auth.login(values).then((data) => { + setStatus({ success: true }); setSubmitting(false); - } + window.location.href = redirectTo; + }).catch((err) => { + setStatus({ success: false }); + if(err.code == 'UL001') { + setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' }); + } else if (err.code == 'UL002') { + setErrors({ submit: 'You have not yet registered your account. You should have an invite link in your emails. If you need a new one, contact your administrator.' }); + } else { + setErrors({ submit: 'Unexpected error. Try again later.' }); + } + setSubmitting(false); + }); }} > {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( diff --git a/client/src/pages/servapps/containers/index.jsx b/client/src/pages/servapps/containers/index.jsx index ace3f0e..953a83b 100644 --- a/client/src/pages/servapps/containers/index.jsx +++ b/client/src/pages/servapps/containers/index.jsx @@ -1,7 +1,7 @@ import * as React from 'react'; import MainCard from '../../../components/MainCard'; import RestartModal from '../../config/users/restart'; -import { Chip, Divider, Stack, useMediaQuery } from '@mui/material'; +import { Alert, Chip, Divider, Stack, useMediaQuery } from '@mui/material'; import HostChip from '../../../components/hostChip'; import { RouteMode, RouteSecurity } from '../../../components/routeComponents'; import { getFaviconURL } from '../../../utils/routes'; @@ -13,6 +13,9 @@ import Back from '../../../components/back'; import { useParams } from 'react-router'; import ContainerOverview from './overview'; import Logs from './logs'; +import DockerContainerSetup from './setup'; +import NetworkContainerSetup from './network'; +import VolumeContainerSetup from './volumes'; const ContainerIndex = () => { const { containerName } = useParams(); @@ -53,30 +56,27 @@ const ContainerIndex = () => { }, { title: 'Terminal', - children: + children:
+ This feature is not yet implemented. It is planned for next version: 0.5.0 +
}, { title: 'Links', - children:
Links
+ children:
+ This feature is not yet implemented. It is planned for next version: 0.5.0 +
}, - // { - // title: 'Advanced' - // }, { - title: 'Setup', - children:
Image, Restart Policy, Environment Variables, Labels, etc...
+ title: 'Docker', + children: }, { title: 'Network', - children:
Urls, Networks, Ports, etc...
+ children: }, { title: 'Volumes', - children:
Volumes
- }, - { - title: 'Resources', - children:
Runtime Resources, Capabilities...
+ children: }, ]} /> diff --git a/client/src/pages/servapps/containers/network.jsx b/client/src/pages/servapps/containers/network.jsx new file mode 100644 index 0000000..4d78cab --- /dev/null +++ b/client/src/pages/servapps/containers/network.jsx @@ -0,0 +1,264 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { Button, Stack, Grid, MenuItem, TextField, IconButton, FormHelperText, CircularProgress, useTheme } from '@mui/material'; +import MainCard from '../../../components/MainCard'; +import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } + from '../../config/users/formShortcuts'; +import { ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import * as API from '../../../api'; +import { LoadingButton } from '@mui/lab'; +import PrettyTableView from '../../../components/tableView/prettyTableView'; +import { NetworksColumns } from '../networks'; +import NewNetworkButton from '../createNetwork'; + +const NetworkContainerSetup = ({ config, containerInfo, refresh }) => { + const restartPolicies = [ + ['no', 'No Restart'], + ['always', 'Always Restart'], + ['on-failure', 'Restart On Failure'], + ['unless-stopped', 'Restart Unless Stopped'], + ]; + + const [networks, setNetworks] = React.useState([]); + const theme = useTheme(); + const isDark = theme.palette.mode === 'dark'; + + React.useEffect(() => { + API.docker.networkList().then((res) => { + setNetworks(res.data); + }); + }, []); + + const refreshAll = () => { + setNetworks(null); + refresh().then(() => { + API.docker.networkList().then((res) => { + setNetworks(res.data); + }); + }); + }; + + const connect = (network) => { + setNetworks(null); + return API.docker.attachNetwork(containerInfo.Id, network).then(() => { + refreshAll(); + }); + } + + const disconnect = (network) => { + return API.docker.detachNetwork(containerInfo.Id, network).then(() => { + refreshAll(); + }); + } + + return ( +
+ { + return { + port: port.split('/')[0], + protocol: port.split('/')[1], + hostPort: containerInfo.NetworkSettings.Ports[port] ? + containerInfo.NetworkSettings.Ports[port][0].HostPort : '', + }; + }) + }} + validate={(values) => { + const errors = {}; + // check unique + const ports = values.ports.map((port) => { + return `${port.port}/${port.protocol}`; + }); + const unique = [...new Set(ports)]; + if (unique.length !== ports.length) { + errors.submit = 'Ports must be unique'; + } + return errors; + }} + onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { + setSubmitting(true); + const realvalues = { + portBindings: {}, + }; + values.ports.forEach((port) => { + if (port.hostPort) { + realvalues.portBindings[`${port.port}/${port.protocol}`] = [ + { + HostPort: port.hostPort, + } + ]; + } + }); + return API.docker.updateContainer(containerInfo.Name.replace('/', ''), realvalues) + .then((res) => { + setStatus({ success: true }); + setSubmitting(false); + } + ).catch((err) => { + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + }); + }} + > + {(formik) => ( +
+ + + + + {formik.values.ports.map((port, idx) => ( + + + { + const newports = [...formik.values.ports]; + newports[idx].port = e.target.value; + formik.setFieldValue('ports', newports); + }} + /> + + + { + const newports = [...formik.values.ports]; + newports[idx].hostPort = e.target.value; + formik.setFieldValue('ports', newports); + }} + /> + + + { + const newports = [...formik.values.ports]; + newports[idx].protocol = e.target.value; + formik.setFieldValue('ports', newports); + }} + > + TCP + UDP + + + + { + const newports = [...formik.values.ports]; + newports.splice(idx, 1); + formik.setFieldValue('ports', newports); + }} + > + + + + + ))} + { + const newports = [...formik.values.ports]; + newports.push({ + port: '', + protocol: 'tcp', + hostPort: '', + }); + formik.setFieldValue('ports', newports); + }} + > + + + + + + {formik.errors.submit && ( + + {formik.errors.submit} + + )} + + Update Ports + + + + + + + {networks && , + ]} + onRowClick={() => { }} + getKey={(r) => r.Id} + columns={[ + { + title: '', + field: (r) => { + const isConnected = containerInfo.NetworkSettings.Networks[r.Name]; + // icon + return isConnected ? + : + + } + }, + ...NetworksColumns(theme, isDark), + { + title: '', + field: (r) => { + const isConnected = containerInfo.NetworkSettings.Networks[r.Name]; + return () + } + } + ]} + />} + {!networks && (
+
+
+ +
+
+ )} +
+
+ + )} +
+
+
); +}; + +export default NetworkContainerSetup; \ No newline at end of file diff --git a/client/src/pages/servapps/containers/setup.jsx b/client/src/pages/servapps/containers/setup.jsx new file mode 100644 index 0000000..876fe1e --- /dev/null +++ b/client/src/pages/servapps/containers/setup.jsx @@ -0,0 +1,241 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { Button, Stack, Grid, MenuItem, TextField, IconButton, FormHelperText } from '@mui/material'; +import MainCard from '../../../components/MainCard'; +import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } + from '../../config/users/formShortcuts'; +import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import * as API from '../../../api'; +import { LoadingButton } from '@mui/lab'; + +const DockerContainerSetup = ({config, containerInfo}) => { + const restartPolicies = [ + ['no', 'No Restart'], + ['always', 'Always Restart'], + ['on-failure', 'Restart On Failure'], + ['unless-stopped', 'Restart Unless Stopped'], + ]; + + return ( +
+ { + const [key, value] = envVar.split('='); + return { key, value }; + }), + labels: Object.keys(containerInfo.Config.Labels).map((key) => { + return { key, value: containerInfo.Config.Labels[key] }; + }), + }} + validate={(values) => { + const errors = {}; + if (!values.image) { + errors.image = 'Required'; + } + // env keys and labels key mustbe unique + const envKeys = values.envVars.map((envVar) => envVar.key); + const labelKeys = values.labels.map((label) => label.key); + const uniqueEnvKeysKeys = [...new Set(envKeys)]; + const uniqueLabelKeys = [...new Set(labelKeys)]; + if (uniqueEnvKeysKeys.length !== envKeys.length) { + errors.submit = 'Environment Variables must be unique'; + } + if (uniqueLabelKeys.length !== labelKeys.length) { + errors.submit = 'Labels must be unique'; + } + return errors; + }} + onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { + setSubmitting(true); + const labels = {}; + values.labels.forEach((label) => { + labels[label.key] = label.value; + }); + const envVars = values.envVars.map((envVar) => { + return `${envVar.key}=${envVar.value}`; + }); + const realvalues = { + ...values, + envVars: envVars, + labels: labels, + }; + return API.docker.updateContainer(containerInfo.Name.replace('/', ''), realvalues) + .then((res) => { + setStatus({ success: true }); + setSubmitting(false); + } + ).catch((err) => { + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + }); + }} + > + {(formik) => ( +
+ + + + + + + + {formik.values.envVars.map((envVar, idx) => ( + + + { + const newEnvVars = [...formik.values.envVars]; + newEnvVars[idx].key = e.target.value; + formik.setFieldValue('envVars', newEnvVars); + }} + /> + + + { + const newEnvVars = [...formik.values.envVars]; + newEnvVars[idx].value = e.target.value; + formik.setFieldValue('envVars', newEnvVars); + }} + /> + + + { + const newEnvVars = [...formik.values.envVars]; + newEnvVars.splice(idx, 1); + formik.setFieldValue('envVars', newEnvVars); + }} + > + + + + + ))} + { + const newEnvVars = [...formik.values.envVars]; + newEnvVars.push({ key: '', value: '' }); + formik.setFieldValue('envVars', newEnvVars); + }} + > + + + + + + {formik.values.labels.map((label, idx) => ( + + + { + const newLabels = [...formik.values.labels]; + newLabels[idx].key = e.target.value; + formik.setFieldValue('labels', newLabels); + }} + /> + + + { + const newLabels = [...formik.values.labels]; + newLabels[idx].value = e.target.value; + formik.setFieldValue('labels', newLabels); + }} + /> + + + { + const newLabels = [...formik.values.labels]; + newLabels.splice(idx, 1); + formik.setFieldValue('labels', newLabels); + }} + > + + + + + ))} + { + const newLabels = [...formik.values.labels]; + newLabels.push({ key: '', value: '' }); + formik.setFieldValue('labels', newLabels); + }} + > + + + + + + + + {formik.errors.submit && ( + + {formik.errors.submit} + + )} + + Update + + + + + + )} +
+
); +}; + +export default DockerContainerSetup; \ No newline at end of file diff --git a/client/src/pages/servapps/containers/volumes.jsx b/client/src/pages/servapps/containers/volumes.jsx new file mode 100644 index 0000000..d5c40eb --- /dev/null +++ b/client/src/pages/servapps/containers/volumes.jsx @@ -0,0 +1,249 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { Button, Stack, Grid, MenuItem, TextField, IconButton, FormHelperText, CircularProgress, useTheme, Checkbox } from '@mui/material'; +import MainCard from '../../../components/MainCard'; +import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } + from '../../config/users/formShortcuts'; +import { ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons'; +import * as API from '../../../api'; +import { LoadingButton } from '@mui/lab'; +import PrettyTableView from '../../../components/tableView/prettyTableView'; +import { NetworksColumns } from '../networks'; +import NewNetworkButton from '../createNetwork'; + +const VolumeContainerSetup = ({ config, containerInfo, refresh }) => { + const restartPolicies = [ + ['no', 'No Restart'], + ['always', 'Always Restart'], + ['on-failure', 'Restart On Failure'], + ['unless-stopped', 'Restart Unless Stopped'], + ]; + + const [volumes, setVolumes] = React.useState([]); + const theme = useTheme(); + const isDark = theme.palette.mode === 'dark'; + + React.useEffect(() => { + API.docker.networkList().then((res) => { + setVolumes(res.data); + }); + }, []); + + const refreshAll = () => { + setVolumes(null); + refresh().then(() => { + API.docker.networkList().then((res) => { + setVolumes(res.data); + }); + }); + }; + + return ( +
+ { + const [source, destination, mode] = bind.split(':'); + return { + Type: 'bind', + Source: source, + Target: destination, + } + }) + ]) + }} + validate={(values) => { + const errors = {}; + // check unique + const volumes = values.volumes.map((volume) => { + return `${volume.Destination}`; + }); + const unique = [...new Set(volumes)]; + if (unique.length !== volumes.length) { + errors.submit = 'Mounts must have unique destinations'; + } + return errors; + }} + onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { + setSubmitting(true); + const realvalues = { + Volumes: values.volumes + }; + return API.docker.updateContainer(containerInfo.Name.replace('/', ''), realvalues) + .then((res) => { + setStatus({ success: true }); + setSubmitting(false); + } + ).catch((err) => { + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + }); + }} + > + {(formik) => ( +
+ + + + {volumes && { }} + getKey={(r) => r.Id} + fullWidth + buttons={[ + + ]} + columns={[ + { + title: 'Type', + field: (r, k) => ( +
+ { + const newVolumes = [...formik.values.volumes]; + newVolumes[k].Type = e.target.value; + formik.setFieldValue('volumes', newVolumes); + }} + > + Bind + Volume + +
), + }, + { + title: 'Source', + field: (r, k) => ( +
+ {(r.Type == "bind") ? + { + const newVolumes = [...formik.values.volumes]; + newVolumes[k].Source = e.target.value; + formik.setFieldValue('volumes', newVolumes); + }} + /> : + { + const newVolumes = [...formik.values.volumes]; + newVolumes[k].Source = e.target.value; + formik.setFieldValue('volumes', newVolumes); + }} + > + {volumes.map((volume) => ( + + {volume.Name} + + ))} + + } +
), + }, + { + title: 'Target', + field: (r, k) => ( +
+ { + const newVolumes = [...formik.values.volumes]; + newVolumes[k].Target = e.target.value; + formik.setFieldValue('volumes', newVolumes); + }} + /> +
), + }, + { + title: '', + field: (r) => { + console.log(r); + return () + } + } + ]} + />} + {!volumes && (
+
+
+ +
+
+ )} +
+ + + {formik.errors.submit && ( + + {formik.errors.submit} + + )} + + Update Volumes + + + +
+
+ + )} +
+
+
); +}; + +export default VolumeContainerSetup; \ No newline at end of file diff --git a/client/src/pages/servapps/createNetwork.jsx b/client/src/pages/servapps/createNetwork.jsx new file mode 100644 index 0000000..91beab9 --- /dev/null +++ b/client/src/pages/servapps/createNetwork.jsx @@ -0,0 +1,125 @@ +// material-ui +import * as React from 'react'; +import { Alert, Button, Checkbox, FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, Stack, TextField } from '@mui/material'; +import { PlusCircleFilled, PlusCircleOutlined, WarningOutlined } from '@ant-design/icons'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import CircularProgress from '@mui/material/CircularProgress'; +import { useEffect, useState } from 'react'; +import { LoadingButton } from '@mui/lab'; +import { FormikProvider, useFormik } from 'formik'; +import * as Yup from 'yup'; + +import * as API from '../../api'; +import { CosmosCheckbox } from '../config/users/formShortcuts'; + +const NewNetworkButton = ({ fullWidth, refresh }) => { + const [isOpened, setIsOpened] = useState(false); + const formik = useFormik({ + initialValues: { + name: '', + driver: 'bridge', + attachCosmos: false, + }, + validationSchema: Yup.object({ + name: Yup.string().required('Required'), + driver: Yup.string().required('Required'), + }), + onSubmit: (values, { setErrors, setStatus, setSubmitting }) => { + setSubmitting(true); + return API.docker.createNetwork(values) + .then((res) => { + setStatus({ success: true }); + setSubmitting(false); + setIsOpened(false); + refresh && refresh(); + }).catch((err) => { + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + }); + }, + }); + + return <> + setIsOpened(false)}> + + New Network + + +
+ + + + + Driver + + + + + + {formik.errors.submit && ( + + {formik.errors.submit} + + )} +
+
+ { + + + Create + + } +
+
+ + ; +}; + +export default NewNetworkButton; diff --git a/client/src/pages/servapps/createVolumes.jsx b/client/src/pages/servapps/createVolumes.jsx new file mode 100644 index 0000000..d403aa7 --- /dev/null +++ b/client/src/pages/servapps/createVolumes.jsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { FormikProvider, useFormik } from 'formik'; +import * as Yup from 'yup'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField, + FormControl, + InputLabel, + Select, + MenuItem, + Grid, + FormHelperText, +} from '@mui/material'; +import { PlusCircleOutlined } from '@ant-design/icons'; +import { LoadingButton } from '@mui/lab'; +import * as API from '../../api'; + +const NewVolumeButton = ({ fullWidth, refresh }) => { + const [isOpened, setIsOpened] = useState(false); + + const formik = useFormik({ + initialValues: { + name: '', + driver: '', + }, + validationSchema: Yup.object({ + name: Yup.string().required('Required'), + driver: Yup.string().required('Required'), + }), + onSubmit: (values, { setErrors, setStatus, setSubmitting }) => { + setSubmitting(true); + return API.docker.createVolume(values) + .then((res) => { + setStatus({ success: true }); + setSubmitting(false); + setIsOpened(false); + refresh && refresh(); + }).catch((err) => { + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + }); + }, + }); + + return ( + <> + setIsOpened(false)}> + + New Volume + + +
+ + + Driver + + + + {formik.errors.submit && ( + + {formik.errors.submit} + + )} +
+ + + + Create + + +
+
+ + + ); +}; + +export default NewVolumeButton; \ No newline at end of file diff --git a/client/src/pages/servapps/networks.jsx b/client/src/pages/servapps/networks.jsx index 2a59257..8c01191 100644 --- a/client/src/pages/servapps/networks.jsx +++ b/client/src/pages/servapps/networks.jsx @@ -5,6 +5,45 @@ import { useEffect, useState } from 'react'; import * as API from '../../api'; import PrettyTableView from '../../components/tableView/prettyTableView'; +import NewNetworkButton from './createNetwork'; + +export const NetworksColumns = (theme, isDark) => [ + { + title: 'Network Name', + field: (r) => +
{r.Name}

+
{r.Driver} driver
+
, + search: (r) => r.Name, + }, + { + title: 'Properties', + screenMin: 'md', + field: (r) => ( + + + {r.Internal && } + {r.Attachable && } + {r.Ingress && } + + ), + }, + { + title: 'IPAM gateway / mask', + screenMin: 'lg', + field: (r) => r.IPAM.Config.map((config, index) => ( + +
{config.Gateway}
+
{config.Subnet}
+
+ )), + }, + { + title: 'Created At', + screenMin: 'lg', + field: (r) => new Date(r.Created).toLocaleString(), + }, +]; const NetworkManagementList = () => { const [isLoading, setIsLoading] = useState(false); @@ -45,43 +84,13 @@ const NetworkManagementList = () => { {!isLoading && rows && ( , + ]} onRowClick={() => { }} getKey={(r) => r.Id} columns={[ - { - title: 'Network Name', - field: (r) => -
{r.Name}

-
{r.Driver} driver
-
, - search: (r) => r.Name, - }, - { - title: 'Properties', - screenMin: 'md', - field: (r) => ( - - - {r.Internal && } - {r.Attachable && } - {r.Ingress && } - - ), - }, - { - title: 'IPAM gateway / mask', - screenMin: 'lg', - field: (r) => r.IPAM.Config.map((config, index) => ( -
- {config.Gateway} / {config.Subnet} -
- )), - }, - { - title: 'Created At', - screenMin: 'lg', - field: (r) => new Date(r.Created).toLocaleString(), - }, + ...NetworksColumns(theme, isDark), { title: '', clickable: true, diff --git a/client/src/pages/servapps/volumes.jsx b/client/src/pages/servapps/volumes.jsx index 8b87633..8aef397 100644 --- a/client/src/pages/servapps/volumes.jsx +++ b/client/src/pages/servapps/volumes.jsx @@ -14,6 +14,7 @@ import RouteManagement from '../config/routes/routeman'; import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes'; import HostChip from '../../components/hostChip'; import PrettyTableView from '../../components/tableView/prettyTableView'; +import NewVolumeButton from './createVolumes'; const VolumeManagementList = () => { const [isLoading, setIsLoading] = useState(false); @@ -56,6 +57,9 @@ const VolumeManagementList = () => { data={rows} onRowClick={() => {}} getKey={(r) => r.Name} + buttons={[ + , + ]} columns={[ { title: 'Volume Name', diff --git a/package.json b/package.json index b905825..e5118de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.4.0-unstable4", + "version": "0.4.0-unstable5", "description": "", "main": "test-server.js", "bugs": { diff --git a/src/docker/api_networks.go b/src/docker/api_networks.go index 9bad5ac..d2cb556 100644 --- a/src/docker/api_networks.go +++ b/src/docker/api_networks.go @@ -77,4 +77,206 @@ func DeleteNetworkRoute(w http.ResponseWriter, req *http.Request) { utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") return } +} + +func NetworkContainerRoutes(w http.ResponseWriter, req *http.Request) { + if req.Method == "GET" { + ListContainerNetworks(w, req) + } else if req.Method == "DELETE" { + DetachNetwork(w, req) + } else if req.Method == "POST" { + AttachNetwork(w, req) + } else { + utils.Error("NetworkContainerRoutes: Method not allowed " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func NetworkRoutes(w http.ResponseWriter, req *http.Request) { + if req.Method == "GET" { + ListNetworksRoute(w, req) + } else if req.Method == "POST" { + CreateNetworkRoute(w, req) + } else { + utils.Error("NetworkContainerRoutes: Method not allowed " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func AttachNetwork(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + if req.Method == "POST" { + vars := mux.Vars(req) + containerID := vars["containerId"] + networkID := vars["networkId"] + + errD := Connect() + if errD != nil { + utils.Error("AttachNetwork", errD) + utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "AN001") + return + } + + err := DockerClient.NetworkConnect(context.Background(), networkID, containerID, nil) + if err != nil { + utils.Error("AttachNetwork: Error while attaching network", err) + utils.HTTPError(w, "Network Attach Error: "+err.Error(), http.StatusInternalServerError, "AN002") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "message": "Network attached successfully", + }) + } else { + utils.Error("AttachNetwork: Method not allowed "+req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func DetachNetwork(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + if req.Method == "DELETE" { + vars := mux.Vars(req) + containerID := vars["containerId"] + networkID := vars["networkId"] + + errD := Connect() + if errD != nil { + utils.Error("DetachNetwork", errD) + utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "DN001") + return + } + + err := DockerClient.NetworkDisconnect(context.Background(), networkID, containerID, true) + if err != nil { + utils.Error("DetachNetwork: Error while detaching network", err) + utils.HTTPError(w, "Network Detach Error: "+err.Error(), http.StatusInternalServerError, "DN002") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "message": "Network detached successfully", + }) + } else { + utils.Error("DetachNetwork: Method not allowed "+req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func ListContainerNetworks(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + if req.Method == "GET" { + vars := mux.Vars(req) + containerID := vars["containerId"] + + errD := Connect() + if errD != nil { + utils.Error("ListContainerNetworks", errD) + utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001") + return + } + + // List Docker networks + networks, err := DockerClient.NetworkList(context.Background(), types.NetworkListOptions{}) + if err != nil { + utils.Error("ListNetworksRoute: Error while getting networks", err) + utils.HTTPError(w, "Networks Get Error: " + err.Error(), http.StatusInternalServerError, "LN002") + return + } + + container, err := DockerClient.ContainerInspect(context.Background(), containerID) + if err != nil { + utils.Error("ListContainerNetworks: Error while getting container", err) + utils.HTTPError(w, "Container Get Error: "+err.Error(), http.StatusInternalServerError, "LN002") + return + } + containerNetworks := container.NetworkSettings.Networks + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": map[string]interface{}{ + "networks": networks, + "containerNetworks": containerNetworks, + }, + }) + } else { + utils.Error("ListContainerNetworks: Method not allowed " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +type createNetworkPayload struct { + Name string `json:"name"` + Driver string `json:"driver"` + AttachCosmos bool `json:"attachCosmos"` +} + +func CreateNetworkRoute(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + if req.Method == "POST" { + errD := Connect() + if errD != nil { + utils.Error("CreateNetworkRoute", errD) + utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "CN001") + return + } + + var payload createNetworkPayload + err := json.NewDecoder(req.Body).Decode(&payload) + if err != nil { + utils.Error("CreateNetworkRoute: Error reading request body", err) + utils.HTTPError(w, "Error reading request body: "+err.Error(), http.StatusBadRequest, "CN002") + return + } + + networkCreate := types.NetworkCreate{ + CheckDuplicate: true, + Driver: payload.Driver, + } + + resp, err := DockerClient.NetworkCreate(context.Background(), payload.Name, networkCreate) + if err != nil { + utils.Error("CreateNetworkRoute: Error while creating network", err) + utils.HTTPError(w, "Network Create Error: " + err.Error(), http.StatusInternalServerError, "CN004") + return + } + + if payload.AttachCosmos { + // Attach network to cosmos + err = AttachNetworkToCosmos(resp.ID) + if err != nil { + utils.Error("CreateNetworkRoute: Error while attaching network to cosmos", err) + utils.HTTPError(w, "Network Attach Error: " + err.Error(), http.StatusInternalServerError, "CN005") + return + } + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": resp, + }) + } else { + utils.Error("CreateNetworkRoute: Method not allowed " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } } \ No newline at end of file diff --git a/src/docker/api_updateContainer.go b/src/docker/api_updateContainer.go new file mode 100644 index 0000000..bb13796 --- /dev/null +++ b/src/docker/api_updateContainer.go @@ -0,0 +1,96 @@ +package docker + +import ( + "encoding/json" + "net/http" + + "github.com/azukaar/cosmos-server/src/utils" + containerType "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/docker/docker/api/types/mount" + "github.com/gorilla/mux" +) + +type ContainerForm struct { + Image string `json:"image"` + RestartPolicy string `json:"restartPolicy"` + Env []string `json:"envVars"` + Labels map[string]string `json:"labels"` + PortBindings nat.PortMap `json:"portBindings"` + Volumes []mount.Mount `json:"Volumes"` +} + +func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + utils.Log("UpdateContainer" + "Updating container") + + if req.Method == "POST" { + errD := Connect() + if errD != nil { + utils.Error("UpdateContainer", errD) + utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "DS002") + return + } + + vars := mux.Vars(req) + containerName := utils.Sanitize(vars["containerId"]) + + container, err := DockerClient.ContainerInspect(DockerContext, containerName) + if err != nil { + utils.Error("UpdateContainer", err) + utils.HTTPError(w, "Internal server error: "+err.Error(), http.StatusInternalServerError, "DS002") + return + } + + var form ContainerForm + err = json.NewDecoder(req.Body).Decode(&form) + if err != nil { + utils.Error("UpdateContainer", err) + utils.HTTPError(w, "Invalid JSON", http.StatusBadRequest, "DS003") + return + } + + // Update container settings + if(form.Image != "") { + container.Config.Image = form.Image + } + if(form.RestartPolicy != "") { + container.HostConfig.RestartPolicy = containerType.RestartPolicy{Name: form.RestartPolicy} + } + if(form.Env != nil) { + container.Config.Env = form.Env + } + if(form.Labels != nil) { + container.Config.Labels = form.Labels + } + if(form.PortBindings != nil) { + container.HostConfig.PortBindings = form.PortBindings + container.Config.ExposedPorts = make(map[nat.Port]struct{}) + for port := range form.PortBindings { + container.Config.ExposedPorts[port] = struct{}{} + } + } + if(form.Volumes != nil) { + container.HostConfig.Mounts = form.Volumes + container.HostConfig.Binds = []string{} + } + + _, err = EditContainer(container.ID, container) + if err != nil { + utils.Error("UpdateContainer: EditContainer", err) + utils.HTTPError(w, "Internal server error: "+err.Error(), http.StatusInternalServerError, "DS004") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + } else { + utils.Error("UpdateContainer: Method not allowed " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/docker/api_volumes.go b/src/docker/api_volumes.go index 5a1d6ad..6c8a6ca 100644 --- a/src/docker/api_volumes.go +++ b/src/docker/api_volumes.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/mux" "github.com/azukaar/cosmos-server/src/utils" filters "github.com/docker/docker/api/types/filters" + volumeTypes"github.com/docker/docker/api/types/volume" ) func ListVolumeRoute(w http.ResponseWriter, req *http.Request) { @@ -76,4 +77,66 @@ func DeleteVolumeRoute(w http.ResponseWriter, req *http.Request) { utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") return } +} + +type VolumeCreateRequest struct { + Name string `json:"name"` + Driver string `json:"driver"` +} + +func CreateVolumeRoute(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + if req.Method == "POST" { + errD := Connect() + if errD != nil { + utils.Error("CreateVolumeRoute", errD) + utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "CV001") + return + } + + var payload VolumeCreateRequest + err := json.NewDecoder(req.Body).Decode(&payload) + if err != nil { + utils.Error("CreateNetworkRoute: Error reading request body", err) + utils.HTTPError(w, "Error reading request body: "+err.Error(), http.StatusBadRequest, "CN002") + return + } + + // Create Docker volume with the provided options + volumeOptions := volumeTypes.VolumeCreateBody{ + Name: payload.Name, + Driver: payload.Driver, + } + + volume, err := DockerClient.VolumeCreate(context.Background(), volumeOptions) + if err != nil { + utils.Error("CreateVolumeRoute: Error while creating volume", err) + utils.HTTPError(w, "Volume creation error: "+err.Error(), http.StatusInternalServerError, "CV004") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": volume, + }) + } else { + utils.Error("CreateVolumeRoute: Method not allowed " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func VolumesRoute(w http.ResponseWriter, req *http.Request) { + if req.Method == "GET" { + ListVolumeRoute(w, req) + } else if req.Method == "POST" { + CreateVolumeRoute(w, req) + } else { + utils.Error("VolumesRoute: Method not allowed " + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } } \ No newline at end of file diff --git a/src/docker/docker.go b/src/docker/docker.go index 3be69dd..76d9bb0 100644 --- a/src/docker/docker.go +++ b/src/docker/docker.go @@ -4,6 +4,7 @@ import ( "context" "errors" "time" + "fmt" "github.com/azukaar/cosmos-server/src/utils" "github.com/docker/docker/client" @@ -79,55 +80,73 @@ func Connect() error { return nil } -func EditContainer(containerID string, newConfig types.ContainerJSON) (string, error) { - DockerNetworkLock <- true - defer func() { - <-DockerNetworkLock - utils.Debug("Unlocking EDIT Container") - }() +func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string, error) { + + utils.Debug("VOLUMES:" + fmt.Sprintf("%v", newConfig.HostConfig.Mounts)) + + if(oldContainerID != "") { + // no need to re-lock if we are reverting + DockerNetworkLock <- true + defer func() { + <-DockerNetworkLock + utils.Debug("Unlocking EDIT Container") + }() - errD := Connect() - if errD != nil { - return "", errD - } - utils.Log("EditContainer - Container updating " + containerID) - - // get container informations - // https://godoc.org/github.com/docker/docker/api/types#ContainerJSON - oldContainer, err := DockerClient.ContainerInspect(DockerContext, containerID) - - if err != nil { - return "", err - } - - // 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, containerID, container.StopOptions{}) - if stopError != nil { - return "", stopError - } - - removeError := DockerClient.ContainerRemove(DockerContext, containerID, types.ContainerRemoveOptions{}) - if removeError != nil { - return "", removeError - } - - // wait for container to be destroyed - // - for { - _, err := DockerClient.ContainerInspect(DockerContext, containerID) - if err != nil { - break - } else { - utils.Log("EditContainer - Waiting for container to be destroyed") - time.Sleep(1 * time.Second) + errD := Connect() + if errD != nil { + return "", errD } } + + newName := newConfig.Name + oldContainer := newConfig - utils.Log("EditContainer - Container stopped " + containerID) + if(oldContainerID != "") { + utils.Log("EditContainer - Container updating. Retriveing currently running " + oldContainerID) + + var err error + + // get container informations + // 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 + } + + // 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{}) + if stopError != nil { + return "", stopError + } + + removeError := DockerClient.ContainerRemove(DockerContext, oldContainerID, types.ContainerRemoveOptions{}) + if removeError != nil { + return "", removeError + } + + // wait for container to be destroyed + // + for { + _, err := DockerClient.ContainerInspect(DockerContext, oldContainerID) + if err != nil { + break + } else { + utils.Log("EditContainer - Waiting for container to be destroyed") + time.Sleep(1 * time.Second) + } + } + + utils.Log("EditContainer - Container stopped " + oldContainerID) + } else { + utils.Log("EditContainer - Revert started") + } // recreate container with new informations createResponse, createError := DockerClient.ContainerCreate( @@ -139,6 +158,8 @@ func EditContainer(containerID string, newConfig types.ContainerJSON) (string, e newName, ) + utils.Log("EditContainer - Container recreated. Re-connecting networks " + createResponse.ID) + // is force secure isForceSecure := newConfig.Config.Labels["cosmos-force-network-secured"] == "true" @@ -148,6 +169,7 @@ func EditContainer(containerID string, newConfig types.ContainerJSON) (string, e utils.Log("EditContainer - Skipping network " + networkName + " (cosmos-force-network-secured is true)") continue } + utils.Log("EditContainer - Connecting to network " + networkName) errNet := ConnectToNetworkSync(networkName, createResponse.ID) if errNet != nil { utils.Error("EditContainer - Failed to connect to network " + networkName, errNet) @@ -155,25 +177,57 @@ func EditContainer(containerID string, newConfig types.ContainerJSON) (string, e utils.Debug("EditContainer - New Container connected to network " + networkName) } } + + utils.Log("EditContainer - Networks Connected. Starting new container " + createResponse.ID) runError := DockerClient.ContainerStart(DockerContext, createResponse.ID, types.ContainerStartOptions{}) - if runError != nil { - return "", runError - } - - utils.Log("EditContainer - Container recreated " + createResponse.ID) - - if createError != nil { - // attempt to restore container - _, restoreError := DockerClient.ContainerCreate(DockerContext, oldContainer.Config, nil, nil, nil, oldContainer.Name) - if restoreError != nil { - utils.Error("EditContainer - Failed to restore Docker Container after update failure", restoreError) + if createError != nil || runError != nil { + if(oldContainerID == "") { + if(createError == nil) { + utils.Error("EditContainer - Failed to revert. Container is re-created but in broken state.", runError) + return "", runError + } else { + utils.Error("EditContainer - Failed to revert. Giving up.", createError) + return "", createError + } } - return "", createError + utils.Log("EditContainer - Failed to edit, attempting to revert changes") + + if(createError == nil) { + utils.Log("EditContainer - Killing new broken container") + DockerClient.ContainerKill(DockerContext, createResponse.ID, "") + } + + utils.Log("EditContainer - Reverting...") + // attempt to restore container + restored, restoreError := EditContainer("", oldContainer) + + if restoreError != nil { + utils.Error("EditContainer - Failed to restore container", restoreError) + + if createError != nil { + utils.Error("EditContainer - re-create container ", createError) + return "", createError + } else { + utils.Error("EditContainer - re-start container ", runError) + return "", runError + } + } else { + utils.Log("EditContainer - Container restored " + oldContainerID) + errorWas := "" + if createError != nil { + errorWas = createError.Error() + } else { + errorWas = runError.Error() + } + return restored, errors.New("Failed to edit container, but restored to previous state. Error was: " + errorWas) + } } + utils.Log("EditContainer - Container started. All done! " + createResponse.ID) + return createResponse.ID, nil } diff --git a/src/docker/network.go b/src/docker/network.go index 4a5f5f7..1e675e2 100644 --- a/src/docker/network.go +++ b/src/docker/network.go @@ -52,20 +52,24 @@ func CreateCosmosNetwork() (string, error) { return "", err } - //if running in Docker, connect to main network - // utils.Debug("HOSTNAME: " + os.Getenv("HOSTNAME")) - // if os.Getenv("HOSTNAME") != "" { - // err := DockerClient.NetworkConnect(DockerContext, newNeworkName, os.Getenv("HOSTNAME"), &network.EndpointSettings{}) - - // if err != nil { - // utils.Error("Docker Network Connect", err) - // return "", err - // } - // } - return newNeworkName, nil } +func AttachNetworkToCosmos(newNeworkName string ) error { + utils.Log("Connecting Cosmos to network " + newNeworkName) + utils.Debug("HOSTNAME: " + os.Getenv("HOSTNAME")) + if os.Getenv("HOSTNAME") != "" { + err := DockerClient.NetworkConnect(DockerContext, newNeworkName, os.Getenv("HOSTNAME"), &network.EndpointSettings{}) + + if err != nil { + utils.Error("Docker Network Connect", err) + return err + } + return nil + } + return nil +} + func ConnectToSecureNetwork(containerConfig types.ContainerJSON) (bool, error) { errD := Connect() if errD != nil { diff --git a/src/httpServer.go b/src/httpServer.go index 8315872..351d445 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -219,15 +219,18 @@ func StartServer() { srapi.HandleFunc("/api/users", user.UsersRoute) srapi.HandleFunc("/api/volume/{volumeName}", docker.DeleteVolumeRoute) - srapi.HandleFunc("/api/volumes", docker.ListVolumeRoute) + srapi.HandleFunc("/api/volumes", docker.VolumesRoute) srapi.HandleFunc("/api/network/{networkID}", docker.DeleteNetworkRoute) - srapi.HandleFunc("/api/networks", docker.ListNetworksRoute) + srapi.HandleFunc("/api/networks", docker.NetworkRoutes) srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/logs", docker.GetContainerLogsRoute) + srapi.HandleFunc("/api/servapps/{containerId}/update", docker.UpdateContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/", docker.GetContainerRoute) + srapi.HandleFunc("/api/servapps/{containerId}/network/{networkId}", docker.NetworkContainerRoutes) + srapi.HandleFunc("/api/servapps/{containerId}/networks", docker.NetworkContainerRoutes) srapi.HandleFunc("/api/servapps", docker.ContainersRoute) if(!config.HTTPConfig.AcceptAllInsecureHostname) { diff --git a/src/index.go b/src/index.go index 4accfb7..8fdd6f0 100644 --- a/src/index.go +++ b/src/index.go @@ -3,6 +3,7 @@ package main import ( "math/rand" "time" + "context" "github.com/azukaar/cosmos-server/src/docker" "github.com/azukaar/cosmos-server/src/utils" @@ -23,5 +24,12 @@ func main() { docker.BootstrapAllContainersFromTags() + version, err := docker.DockerClient.ServerVersion(context.Background()) + if err != nil { + panic(err) + } + + utils.Log("Docker API version: " + version.APIVersion) + StartServer() } diff --git a/src/utils/middleware.go b/src/utils/middleware.go index eab5626..c38fdf1 100644 --- a/src/utils/middleware.go +++ b/src/utils/middleware.go @@ -5,6 +5,7 @@ import ( "net/http" "time" "net" + "strings" "fmt" "github.com/mxk/go-flowrate/flowrate" @@ -180,18 +181,13 @@ func EnsureHostname(next http.Handler) http.Handler { return } - port := "" - if (IsHTTPS && MainConfig.HTTPConfig.HTTPSPort != "443") { - port = ":" + MainConfig.HTTPConfig.HTTPSPort - } else if (!IsHTTPS && MainConfig.HTTPConfig.HTTPPort != "80") { - port = ":" + MainConfig.HTTPConfig.HTTPPort - } - hostnames := GetAllHostnames() + reqHostNoPort := strings.Split(r.Host, ":")[0] + isOk := false for _, hostname := range hostnames { - if r.Host == hostname + port { + if reqHostNoPort == hostname { isOk = true } }