[release] version 0.5.0-unstable2
This commit is contained in:
parent
8b4d738c2e
commit
c670456d47
|
@ -173,6 +173,53 @@ function createTerminal(containerId) {
|
|||
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/new');
|
||||
}
|
||||
|
||||
function createService(serviceData, onProgress) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serviceData)
|
||||
};
|
||||
|
||||
return fetch('/cosmos/api/docker-service', 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
get,
|
||||
|
@ -192,4 +239,5 @@ export {
|
|||
createVolume,
|
||||
attachTerminal,
|
||||
createTerminal,
|
||||
createService,
|
||||
};
|
BIN
client/src/assets/images/wallpaper.jpg
Normal file
BIN
client/src/assets/images/wallpaper.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
Before Width: | Height: | Size: 684 KiB |
23
client/src/components/fileUpload.jsx
Normal file
23
client/src/components/fileUpload.jsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
|
||||
export default function UploadButtons({OnChange, accept}) {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
accept={accept}
|
||||
style={{ display: 'none' }}
|
||||
id="contained-button-file"
|
||||
multiple
|
||||
type="file"
|
||||
onChange={OnChange}
|
||||
/>
|
||||
<label htmlFor="contained-button-file">
|
||||
<Button variant="contained" component="span" startIcon={<UploadOutlined />}>
|
||||
Upload
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -37,21 +37,26 @@ const a11yProps = (index) => {
|
|||
};
|
||||
};
|
||||
|
||||
const PrettyTabbedView = ({ tabs, isLoading }) => {
|
||||
const PrettyTabbedView = ({ tabs, isLoading, currentTab, setCurrentTab }) => {
|
||||
const [value, setValue] = useState(0);
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'));
|
||||
|
||||
if((currentTab != null && typeof currentTab === 'number') && value !== currentTab)
|
||||
setValue(currentTab);
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
setCurrentTab && setCurrentTab(newValue);
|
||||
};
|
||||
|
||||
const handleSelectChange = (event) => {
|
||||
setValue(event.target.value);
|
||||
setCurrentTab && setCurrentTab(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" height="100%" flexDirection={isMobile ? 'column' : 'row'}>
|
||||
{isMobile ? (
|
||||
{(isMobile && !currentTab) ? (
|
||||
<Select value={value} onChange={handleSelectChange} sx={{ minWidth: 120, marginBottom: '15px' }}>
|
||||
{tabs.map((tab, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
|
@ -70,7 +75,7 @@ const PrettyTabbedView = ({ tabs, isLoading }) => {
|
|||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
style={{fontWeight: !tab.children ? '1000' : '', }}
|
||||
disabled={!tab.children} key={index}
|
||||
disabled={tab.disabled || !tab.children} key={index}
|
||||
label={tab.title} {...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -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, buttons, fullWidth }) => {
|
||||
const PrettyTableView = ({ getKey, data, columns, sort, onRowClick, linkTo, buttons, fullWidth }) => {
|
||||
const [search, setSearch] = React.useState('');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
|
@ -64,6 +64,10 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, f
|
|||
})
|
||||
return found;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (!sort) return 0;
|
||||
return sort(a, b);
|
||||
})
|
||||
.map((row, key) => (
|
||||
<TableRow
|
||||
key={getKey(row)}
|
||||
|
|
|
@ -33,7 +33,7 @@ import defaultport from '../../servapps/defaultport.json';
|
|||
|
||||
import * as API from '../../../api';
|
||||
|
||||
export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTargetChange}) {
|
||||
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [containers, setContainers] = React.useState([]);
|
||||
const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
|
||||
|
@ -173,7 +173,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
|
|||
const newTarget = formik.values[name];
|
||||
React.useEffect(() => {
|
||||
if(onTargetChange) {
|
||||
onTargetChange(newTarget)
|
||||
onTargetChange(newTarget, targetResult.container.replace("/", ""), targetResult)
|
||||
}
|
||||
}, [newTarget])
|
||||
|
||||
|
@ -219,62 +219,63 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
|
|||
/>
|
||||
)}
|
||||
/>}
|
||||
{!nameOnly && <>
|
||||
{(portsOptions) ? (<>
|
||||
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
|
||||
<Autocomplete
|
||||
className="px-2 my-2"
|
||||
variant="outlined"
|
||||
name={name + "-port"}
|
||||
id={name + "-port"}
|
||||
value={targetResult.port}
|
||||
options={portsOptions.map((option) => (option))}
|
||||
placeholder='Select a port'
|
||||
freeSolo
|
||||
filterOptions={(x) => x} // disable filtering
|
||||
getOptionLabel={(option) => '' + option}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
return ('' + option) === value
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
targetResult.port = newValue
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
{targetResult.port == '' && targetResult.port == 0 && <FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
Please select a port
|
||||
</FormHelperText>}
|
||||
</>) : ''}
|
||||
|
||||
|
||||
{(portsOptions) ? (<>
|
||||
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
|
||||
<TextField
|
||||
type="text"
|
||||
name={name + "-protocol"}
|
||||
defaultValue={targetResult.protocol}
|
||||
onChange={(event) => {
|
||||
targetResult.protocol = event.target.value && event.target.value.toLowerCase()
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
/>
|
||||
</>) : ''}
|
||||
|
||||
{(portsOptions) ? (<>
|
||||
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
|
||||
<Autocomplete
|
||||
className="px-2 my-2"
|
||||
variant="outlined"
|
||||
name={name + "-port"}
|
||||
id={name + "-port"}
|
||||
value={targetResult.port}
|
||||
options={portsOptions.map((option) => (option))}
|
||||
placeholder='Select a port'
|
||||
freeSolo
|
||||
filterOptions={(x) => x} // disable filtering
|
||||
getOptionLabel={(option) => '' + option}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
return ('' + option) === value
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
targetResult.port = newValue
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
{targetResult.port == '' && targetResult.port == 0 && <FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
Please select a port
|
||||
</FormHelperText>}
|
||||
</>) : ''}
|
||||
|
||||
|
||||
{(portsOptions) ? (<>
|
||||
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
|
||||
<TextField
|
||||
type="text"
|
||||
name={name + "-protocol"}
|
||||
defaultValue={targetResult.protocol}
|
||||
onChange={(event) => {
|
||||
targetResult.protocol = event.target.value && event.target.value.toLowerCase()
|
||||
formik.setFieldValue(name, getTarget())
|
||||
}}
|
||||
/>
|
||||
</>) : ''}
|
||||
|
||||
<InputLabel htmlFor={name}>Result Target Preview</InputLabel>
|
||||
<TextField
|
||||
name={name}
|
||||
placeholder={"This will be generated automatically"}
|
||||
id={name}
|
||||
value={formik.values[name]}
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
{formik.errors[name] && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
<InputLabel htmlFor={name}>Result Target Preview</InputLabel>
|
||||
<TextField
|
||||
name={name}
|
||||
placeholder={"This will be generated automatically"}
|
||||
id={name}
|
||||
value={formik.values[name]}
|
||||
disabled={true}
|
||||
/>
|
||||
</>}
|
||||
|
||||
{formik.errors[name] && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import Back from "../../components/back";
|
|||
import { Alert, Box, CircularProgress, Grid, Stack, useTheme } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import * as API from "../../api";
|
||||
import wallpaper from '../../assets/images/wallpaper.png';
|
||||
import wallpaper from '../../assets/images/wallpaper.jpg';
|
||||
import Grid2 from "@mui/material/Unstable_Grid2/Grid2";
|
||||
import { getFaviconURL } from "../../utils/routes";
|
||||
import { Link } from "react-router-dom";
|
||||
|
|
182
client/src/pages/servapps/containers/docker-compose.jsx
Normal file
182
client/src/pages/servapps/containers/docker-compose.jsx
Normal file
|
@ -0,0 +1,182 @@
|
|||
// material-ui
|
||||
import * as React from 'react';
|
||||
import { Alert, Button, Stack, Typography } from '@mui/material';
|
||||
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined } from '@ant-design/icons';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Paper from '@mui/material/Paper';
|
||||
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 TextField from '@mui/material/TextField';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import * as API from '../../../api';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ResponsiveButton from '../../../components/responseiveButton';
|
||||
import UploadButtons from '../../../components/fileUpload';
|
||||
import NewDockerService from './newService';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
function checkIsOnline() {
|
||||
API.isOnline().then((res) => {
|
||||
window.location.reload();
|
||||
}).catch((err) => {
|
||||
setTimeout(() => {
|
||||
checkIsOnline();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const preStyle = {
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '500px',
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
position: 'relative',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.5',
|
||||
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '10px',
|
||||
marginTop: '10px',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
display: 'block',
|
||||
textAlign: 'left',
|
||||
verticalAlign: 'baseline',
|
||||
opacity: '1',
|
||||
}
|
||||
|
||||
const DockerComposeImport = () => {
|
||||
const [step, setStep] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [dockerCompose, setDockerCompose] = useState('');
|
||||
const [service, setService] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if(dockerCompose === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
let newService = {};
|
||||
let doc = yaml.load(dockerCompose);
|
||||
|
||||
// convert to the proper format
|
||||
if(doc.services) {
|
||||
Object.keys(doc.services).forEach((key) => {
|
||||
// convert volumes
|
||||
if(doc.services[key].volumes) {
|
||||
let volumes = [];
|
||||
doc.services[key].volumes.forEach((volume) => {
|
||||
let volumeSplit = volume.split(':');
|
||||
let volumeObj = {
|
||||
Source: volumeSplit[0],
|
||||
Target: volumeSplit[1],
|
||||
Type: volume[0] === '/' ? 'bind' : 'volume',
|
||||
};
|
||||
volumes.push(volumeObj);
|
||||
});
|
||||
doc.services[key].volumes = volumes;
|
||||
}
|
||||
|
||||
// convert expose
|
||||
if(doc.services[key].expose) {
|
||||
doc.services[key].expose = doc.services[key].expose.map((port) => {
|
||||
return ''+port;
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setService(doc);
|
||||
console.log(doc);
|
||||
}, [dockerCompose]);
|
||||
|
||||
return <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>Import Docker Compose</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{step === 0 && <Stack spacing={2}>
|
||||
<Alert severity="warning" icon={<WarningOutlined />}>
|
||||
This is a highly experimental feature. It is recommended to use with caution.
|
||||
</Alert>
|
||||
|
||||
<UploadButtons
|
||||
accept='.yml,.yaml'
|
||||
OnChange={(e) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
setDockerCompose(e.target.result);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
multiline
|
||||
placeholder='Paste your docker-compose.yml here or use the file upload button.'
|
||||
fullWidth
|
||||
value={dockerCompose}
|
||||
onChange={(e) => setDockerCompose(e.target.value)}
|
||||
style={preStyle}
|
||||
rows={20}></TextField>
|
||||
</Stack>}
|
||||
{step === 1 && <Stack spacing={2}>
|
||||
<NewDockerService service={service} />
|
||||
</Stack>}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
{!isLoading && <DialogActions>
|
||||
<Button onClick={() => {
|
||||
setOpenModal(false);
|
||||
setStep(0);
|
||||
setDockerCompose('');
|
||||
}}>Close</Button>
|
||||
<Button onClick={() => {
|
||||
if(step === 0) {
|
||||
setStep(1);
|
||||
} else {
|
||||
setStep(0);
|
||||
}
|
||||
}}>
|
||||
{step === 0 && 'Next'}
|
||||
{step === 1 && 'Back'}
|
||||
</Button>
|
||||
</DialogActions>}
|
||||
</Dialog>
|
||||
|
||||
<ResponsiveButton
|
||||
color="primary"
|
||||
onClick={() => setOpenModal(true)}
|
||||
variant="outlined"
|
||||
startIcon={<ArrowUpOutlined />}
|
||||
>
|
||||
Import Docker Compose
|
||||
</ResponsiveButton>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default DockerComposeImport;
|
|
@ -59,12 +59,6 @@ const ContainerIndex = () => {
|
|||
title: 'Terminal',
|
||||
children: <DockerTerminal refresh={refreshContainer} containerInfo={container} config={config}/>
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
children: <div>
|
||||
<Alert severity="info">This feature is not yet implemented. It is planned for next version: 0.5.0</Alert>
|
||||
</div>
|
||||
},
|
||||
{
|
||||
title: 'Docker',
|
||||
children: <DockerContainerSetup refresh={refreshContainer} containerInfo={container} config={config}/>
|
||||
|
@ -74,7 +68,7 @@ const ContainerIndex = () => {
|
|||
children: <NetworkContainerSetup refresh={refreshContainer} containerInfo={container} config={config}/>
|
||||
},
|
||||
{
|
||||
title: 'Volumes',
|
||||
title: 'Storage',
|
||||
children: <VolumeContainerSetup refresh={refreshContainer} containerInfo={container} config={config}/>
|
||||
},
|
||||
]} />
|
||||
|
|
|
@ -10,14 +10,18 @@ import { LoadingButton } from '@mui/lab';
|
|||
import PrettyTableView from '../../../components/tableView/prettyTableView';
|
||||
import { NetworksColumns } from '../networks';
|
||||
import NewNetworkButton from '../createNetwork';
|
||||
import LinkContainersButton from '../linkContainersButton';
|
||||
|
||||
const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
||||
const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange, OnConnect, OnDisconnect }) => {
|
||||
const [networks, setNetworks] = React.useState([]);
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const padding = isMobile ? '6px 4px' : '12px 10px';
|
||||
|
||||
const isForceSecure = containerInfo.Config.Labels.hasOwnProperty('cosmos-force-network-secured') &&
|
||||
containerInfo.Config.Labels['cosmos-force-network-secured'] === 'true';
|
||||
|
||||
React.useEffect(() => {
|
||||
API.docker.networkList().then((res) => {
|
||||
setNetworks(res.data);
|
||||
|
@ -25,25 +29,38 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
}, []);
|
||||
|
||||
const refreshAll = () => {
|
||||
setNetworks(null);
|
||||
refresh().then(() => {
|
||||
if(refresh)
|
||||
refresh().then(() => {
|
||||
API.docker.networkList().then((res) => {
|
||||
setNetworks(res.data);
|
||||
});
|
||||
});
|
||||
else
|
||||
API.docker.networkList().then((res) => {
|
||||
setNetworks(res.data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const connect = (network) => {
|
||||
setNetworks(null);
|
||||
return API.docker.attachNetwork(containerInfo.Name.replace('/', ''), network).then(() => {
|
||||
if(!OnConnect) {
|
||||
return API.docker.attachNetwork(containerInfo.Name.replace('/', ''), network).then(() => {
|
||||
refreshAll();
|
||||
});
|
||||
} else {
|
||||
OnConnect(network);
|
||||
refreshAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = (network) => {
|
||||
return API.docker.detachNetwork(containerInfo.Name.replace('/', ''), network).then(() => {
|
||||
if(!OnDisconnect) {
|
||||
return API.docker.detachNetwork(containerInfo.Name.replace('/', ''), network).then(() => {
|
||||
refreshAll();
|
||||
});
|
||||
} else {
|
||||
OnDisconnect(network);
|
||||
refreshAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (<Stack spacing={2}>
|
||||
|
@ -54,7 +71,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
return {
|
||||
port: port.split('/')[0],
|
||||
protocol: port.split('/')[1],
|
||||
hostPort: containerInfo.NetworkSettings.Ports[port] ?
|
||||
hostPort: containerInfo.NetworkSettings.Ports[port] && containerInfo.NetworkSettings.Ports[port][0] ?
|
||||
containerInfo.NetworkSettings.Ports[port][0].HostPort : '',
|
||||
};
|
||||
})
|
||||
|
@ -69,9 +86,11 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
if (unique.length !== ports.length) {
|
||||
errors.submit = 'Ports must be unique';
|
||||
}
|
||||
OnChange && OnChange(values);
|
||||
return errors;
|
||||
}}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
if(newContainer) return false;
|
||||
setSubmitting(true);
|
||||
const realvalues = {
|
||||
portBindings: {},
|
||||
|
@ -104,11 +123,16 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
<Stack spacing={2}>
|
||||
<MainCard title={'Ports'}>
|
||||
<Stack spacing={4}>
|
||||
{containerInfo.State.Status !== 'running' && (
|
||||
{containerInfo.State && containerInfo.State.Status !== 'running' && (
|
||||
<Alert severity="warning" style={{ marginBottom: '0px' }}>
|
||||
This container is not running. Editing any settings will cause the container to start again.
|
||||
</Alert>
|
||||
)}
|
||||
{isForceSecure && (
|
||||
<Alert severity="warning" style={{ marginBottom: '0px' }}>
|
||||
This container is forced to be secured. You cannot expose any ports to the internet directly, please create a URL in Cosmos instead. You also cannot connect it to the Bridge network.
|
||||
</Alert>
|
||||
)}
|
||||
<div>
|
||||
{formik.values.ports.map((port, idx) => (
|
||||
<Grid container key={idx}>
|
||||
|
@ -195,7 +219,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
<LoadingButton
|
||||
{!newContainer && <LoadingButton
|
||||
fullWidth
|
||||
disableElevation
|
||||
disabled={formik.errors.submit}
|
||||
|
@ -206,7 +230,7 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
color="primary"
|
||||
>
|
||||
Update Ports
|
||||
</LoadingButton>
|
||||
</LoadingButton>}
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
|
@ -237,8 +261,15 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
|
||||
{networks && <PrettyTableView
|
||||
data={networks}
|
||||
sort={(a, b) => a.Name > b.Name}
|
||||
buttons={[
|
||||
<NewNetworkButton refresh={refreshAll} />,
|
||||
<LinkContainersButton
|
||||
refresh={refreshAll}
|
||||
originContainer={containerInfo.Name.replace('/', '')}
|
||||
newContainer={newContainer}
|
||||
OnConnect={OnConnect}
|
||||
/>,
|
||||
]}
|
||||
onRowClick={() => { }}
|
||||
getKey={(r) => r.Id}
|
||||
|
|
123
client/src/pages/servapps/containers/newService.jsx
Normal file
123
client/src/pages/servapps/containers/newService.jsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
import * as React from 'react';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
import RestartModal from '../../config/users/restart';
|
||||
import { Alert, Button, Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||
import HostChip from '../../../components/hostChip';
|
||||
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||
import { getFaviconURL } from '../../../utils/routes';
|
||||
import * as API from '../../../api';
|
||||
import { AppstoreOutlined, ArrowUpOutlined, BulbTwoTone, CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, PlusCircleOutlined, SyncOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
import PrettyTabbedView from '../../../components/tabbedView/tabbedView';
|
||||
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';
|
||||
import DockerTerminal from './terminal';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const preStyle = {
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '500px',
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
position: 'relative',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.5',
|
||||
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '10px',
|
||||
marginTop: '10px',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
display: 'block',
|
||||
textAlign: 'left',
|
||||
verticalAlign: 'baseline',
|
||||
opacity: '1',
|
||||
}
|
||||
|
||||
const NewDockerService = ({service}) => {
|
||||
const { containerName } = useParams();
|
||||
const [container, setContainer] = React.useState(null);
|
||||
const [config, setConfig] = React.useState(null);
|
||||
const [log, setLog] = React.useState([]);
|
||||
const [isDone, setIsDone] = React.useState(false);
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
// refreshContainer();
|
||||
}, []);
|
||||
|
||||
const create = () => {
|
||||
setLog([])
|
||||
API.docker.createService(service, (newlog) => {
|
||||
setLog((old) => [...old, newlog]);
|
||||
if (newlog.includes('[OPERATION SUCCEEDED]')) {
|
||||
setIsDone(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const needsRestart = service && service.service && service.service.some((c) => {
|
||||
return c.routes && c.routes.length > 0;
|
||||
});
|
||||
|
||||
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||
<MainCard title="Create Service">
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
<Stack spacing={1}>
|
||||
{!isDone && <Button
|
||||
onClick={create}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<PlusCircleOutlined />}
|
||||
>Create</Button>}
|
||||
{isDone && <Stack spacing={1}>
|
||||
<Alert severity="success">Service Created!</Alert>
|
||||
{needsRestart && <Alert severity="warning">Cosmos needs to be restarted to apply changes to the URLs</Alert>}
|
||||
{needsRestart &&
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<SyncOutlined />}
|
||||
onClick={() => {
|
||||
setOpenModal(true);
|
||||
}}
|
||||
>Restart</Button>
|
||||
}
|
||||
<Link to={`/ui/servapps`}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<AppstoreOutlined />}
|
||||
>Back to Servapps</Button>
|
||||
</Link>
|
||||
</Stack>}
|
||||
<pre style={preStyle}>
|
||||
{!log.length && JSON.stringify(service, false ,2)}
|
||||
{log.map((l) => {
|
||||
return <div>{l}</div>
|
||||
})}
|
||||
</pre>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default NewDockerService;
|
237
client/src/pages/servapps/containers/newServiceForm.jsx
Normal file
237
client/src/pages/servapps/containers/newServiceForm.jsx
Normal file
|
@ -0,0 +1,237 @@
|
|||
import * as React from 'react';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
import RestartModal from '../../config/users/restart';
|
||||
import { Alert, Button, Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||
import HostChip from '../../../components/hostChip';
|
||||
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||
import { getFaviconURL } from '../../../utils/routes';
|
||||
import * as API from '../../../api';
|
||||
import { ArrowLeftOutlined, ArrowRightOutlined, CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
import PrettyTabbedView from '../../../components/tabbedView/tabbedView';
|
||||
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';
|
||||
import DockerTerminal from './terminal';
|
||||
import NewDockerService from './newService';
|
||||
|
||||
const NewDockerServiceForm = () => {
|
||||
const [currentTab, setCurrentTab] = React.useState(0);
|
||||
const [maxTab, setMaxTab] = React.useState(0);
|
||||
const [containerInfo, setContainerInfo] = React.useState({
|
||||
Name: '',
|
||||
Config: {
|
||||
Env: [],
|
||||
Labels: {},
|
||||
ExposedPorts: [],
|
||||
Volumes: {},
|
||||
Name: '',
|
||||
Image: '',
|
||||
Tty: true,
|
||||
OpenStdin: true,
|
||||
},
|
||||
HostConfig: {
|
||||
PortBindings: [],
|
||||
RestartPolicy: {
|
||||
Name: 'always',
|
||||
},
|
||||
Mounts: [],
|
||||
},
|
||||
NetworkSettings: {
|
||||
Networks: {},
|
||||
Ports: [],
|
||||
},
|
||||
});
|
||||
|
||||
let service = {
|
||||
Services: {
|
||||
container_name : {
|
||||
container_name: containerInfo.Name,
|
||||
image: containerInfo.Config.Image,
|
||||
environment: containerInfo.Config.Env,
|
||||
labels: containerInfo.Config.Labels,
|
||||
expose: containerInfo.Config.ExposedPorts,
|
||||
tty: containerInfo.Config.Tty,
|
||||
stdin_open: containerInfo.Config.OpenStdin,
|
||||
ports: containerInfo.HostConfig.PortBindings,
|
||||
restart: containerInfo.HostConfig.RestartPolicy.Name,
|
||||
volumes: containerInfo.HostConfig.Mounts,
|
||||
networks: Object.keys(containerInfo.NetworkSettings.Networks).reduce((acc, cur) => {
|
||||
acc[cur] = {};
|
||||
return acc;
|
||||
}, {}),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const nav = () => <Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
style={{
|
||||
maxWidth: '1000px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<ArrowLeftOutlined />}
|
||||
disabled={currentTab === 0}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
setCurrentTab(currentTab - 1);
|
||||
}}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
endIcon={<ArrowRightOutlined />}
|
||||
disabled={currentTab === 3}
|
||||
onClick={() => {
|
||||
setCurrentTab(currentTab + 1);
|
||||
setMaxTab(Math.max(currentTab + 1, maxTab));
|
||||
}}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
return <div>
|
||||
<Stack spacing={1}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Back />
|
||||
<div>Start New Servapp</div>
|
||||
</Stack>
|
||||
<IsLoggedIn />
|
||||
|
||||
{<PrettyTabbedView
|
||||
currentTab={currentTab}
|
||||
setCurrentTab={setCurrentTab}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Docker',
|
||||
disabled: false,
|
||||
children: <Stack spacing={2}><DockerContainerSetup newContainer containerInfo={containerInfo} OnChange={(values) => {
|
||||
const newValues = {
|
||||
...containerInfo,
|
||||
Name: values.name,
|
||||
Config: {
|
||||
...containerInfo.Config,
|
||||
Image: values.image,
|
||||
Name: values.name,
|
||||
Env: values.envVars,
|
||||
Labels: values.labels,
|
||||
},
|
||||
HostConfig: {
|
||||
...containerInfo.HostConfig,
|
||||
RestartPolicy: {
|
||||
Name: values.restartPolicy,
|
||||
},
|
||||
},
|
||||
}
|
||||
setContainerInfo(newValues);
|
||||
}}
|
||||
OnForceSecure={(value) => {
|
||||
const newValues = {
|
||||
...containerInfo,
|
||||
Config: {
|
||||
...containerInfo.Config,
|
||||
Labels: {
|
||||
...containerInfo.Config.Labels,
|
||||
'cosmos-force-network-secured': ''+value,
|
||||
},
|
||||
},
|
||||
}
|
||||
setContainerInfo(newValues);
|
||||
}}/>{nav()}</Stack>
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
disabled: maxTab < 1,
|
||||
children: <Stack spacing={2}><NetworkContainerSetup newContainer containerInfo={containerInfo} OnChange={(values) => {
|
||||
const newValues = {
|
||||
...containerInfo,
|
||||
Config: {
|
||||
...containerInfo.Config,
|
||||
ExposedPorts:
|
||||
values.ports.map((port) => {
|
||||
return `${port.port}/${port.protocol}`;
|
||||
}),
|
||||
},
|
||||
HostConfig: {
|
||||
...containerInfo.HostConfig,
|
||||
PortBindings: values.ports.map((port) => {
|
||||
return `${port.port}:${port.hostPort}/${port.protocol}`;
|
||||
}),
|
||||
},
|
||||
NetworkSettings: {
|
||||
...containerInfo.NetworkSettings,
|
||||
Ports: values.ports && values.ports.reduce((acc, port) => {
|
||||
acc[`${port.port}/${port.protocol}`] = [{
|
||||
HostIp: '',
|
||||
HostPort: `${port.hostPort}`,
|
||||
}];
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
}
|
||||
setContainerInfo(newValues);
|
||||
}} OnConnect={(net) => {
|
||||
const newValues = {
|
||||
...containerInfo,
|
||||
NetworkSettings: {
|
||||
...containerInfo.NetworkSettings,
|
||||
Networks: {
|
||||
...containerInfo.NetworkSettings.Networks,
|
||||
[net]: {
|
||||
Name: net,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
setContainerInfo(newValues);
|
||||
}} OnDisconnect={(net) => {
|
||||
let newContainerInfo = {
|
||||
...containerInfo,
|
||||
}
|
||||
delete newContainerInfo.NetworkSettings.Networks[net];
|
||||
setContainerInfo(newContainerInfo);
|
||||
}}/>{nav()}</Stack>
|
||||
},
|
||||
{
|
||||
title: 'Storage',
|
||||
disabled: maxTab < 2,
|
||||
children: <Stack spacing={2}><VolumeContainerSetup ontainer containerInfo={containerInfo} OnChange={(values) => {
|
||||
console.log(values)
|
||||
const newValues = {
|
||||
...containerInfo,
|
||||
HostConfig: {
|
||||
...containerInfo.HostConfig,
|
||||
Mounts: values.volumes.map((volume) => {
|
||||
return {
|
||||
Type: volume.Type,
|
||||
Source: volume.Source,
|
||||
Target: volume.Target,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}
|
||||
setContainerInfo(newValues);
|
||||
}} />{nav()}</Stack>
|
||||
},
|
||||
{
|
||||
title: 'Review & Start',
|
||||
disabled: maxTab < 3,
|
||||
children: <Stack spacing={2}><NewDockerService service={service} />{nav()}</Stack>
|
||||
}
|
||||
]} />}
|
||||
|
||||
</Stack>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default NewDockerServiceForm;
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Formik } from 'formik';
|
||||
import { Button, Stack, Grid, MenuItem, TextField, IconButton, FormHelperText, useMediaQuery, useTheme, Alert } from '@mui/material';
|
||||
import { Field, Formik } from 'formik';
|
||||
import { Button, Stack, Grid, MenuItem, TextField, IconButton, FormHelperText, useMediaQuery, useTheme, Alert, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect }
|
||||
from '../../config/users/formShortcuts';
|
||||
|
@ -8,7 +8,25 @@ import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
|||
import * as API from '../../../api';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
|
||||
const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
||||
const containerInfoFrom = (values) => {
|
||||
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,
|
||||
};
|
||||
realvalues.interactive = realvalues.interactive ? 2 : 1;
|
||||
|
||||
return realvalues;
|
||||
}
|
||||
|
||||
const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newContainer, OnForceSecure}) => {
|
||||
const restartPolicies = [
|
||||
['no', 'No Restart'],
|
||||
['always', 'Always Restart'],
|
||||
|
@ -23,6 +41,7 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
<div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||
<Formik
|
||||
initialValues={{
|
||||
name: containerInfo.Name.replace('/', ''),
|
||||
image: containerInfo.Config.Image,
|
||||
restartPolicy: containerInfo.HostConfig.RestartPolicy.Name,
|
||||
envVars: containerInfo.Config.Env.map((envVar) => {
|
||||
|
@ -50,23 +69,16 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
if (uniqueLabelKeys.length !== labelKeys.length) {
|
||||
errors.submit = 'Labels must be unique';
|
||||
}
|
||||
OnChange && OnChange(containerInfoFrom(values));
|
||||
return errors;
|
||||
}}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
if(newContainer) return false;
|
||||
delete values.name;
|
||||
|
||||
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,
|
||||
};
|
||||
realvalues.interactive = realvalues.interactive ? 2 : 1;
|
||||
|
||||
let realvalues = containerInfoFrom(values);
|
||||
|
||||
return API.docker.updateContainer(containerInfo.Name.replace('/', ''), realvalues)
|
||||
.then((res) => {
|
||||
|
@ -85,12 +97,18 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2}>
|
||||
<MainCard title={'Docker Container Setup'}>
|
||||
{containerInfo.State.Status !== 'running' && (
|
||||
{containerInfo.State && containerInfo.State.Status !== 'running' && (
|
||||
<Alert severity="warning" style={{ marginBottom: '15px' }}>
|
||||
This container is not running. Editing any settings will cause the container to start again.
|
||||
</Alert>
|
||||
)}
|
||||
<Grid container spacing={4}>
|
||||
{newContainer && <CosmosInputText
|
||||
name="name"
|
||||
label="Name"
|
||||
placeholder="Name"
|
||||
formik={formik}
|
||||
/>}
|
||||
<CosmosInputText
|
||||
name="image"
|
||||
label="Image"
|
||||
|
@ -109,6 +127,21 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
label="Interactive Mode"
|
||||
formik={formik}
|
||||
/>
|
||||
{OnForceSecure && <Grid item xs={12}>
|
||||
<Checkbox
|
||||
type="checkbox"
|
||||
as={FormControlLabel}
|
||||
control={<Checkbox size="large" />}
|
||||
label={'Force secure container'}
|
||||
checked={
|
||||
containerInfo.Config.Labels.hasOwnProperty('cosmos-force-network-secured') &&
|
||||
containerInfo.Config.Labels['cosmos-force-network-secured'] === 'true'
|
||||
}
|
||||
onChange={(e) => {
|
||||
OnForceSecure(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
</Grid>}
|
||||
|
||||
<CosmosFormDivider title={'Environment Variables'} />
|
||||
<Grid item xs={12}>
|
||||
|
@ -228,7 +261,7 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
</Grid>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
<MainCard>
|
||||
{!newContainer && <MainCard>
|
||||
<Stack direction="column" spacing={2}>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
|
@ -248,7 +281,7 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
Update
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</MainCard>}
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { NetworksColumns } from '../networks';
|
|||
import NewNetworkButton from '../createNetwork';
|
||||
import ResponsiveButton from '../../../components/responseiveButton';
|
||||
|
||||
const VolumeContainerSetup = ({ config, containerInfo, refresh }) => {
|
||||
const VolumeContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange }) => {
|
||||
const restartPolicies = [
|
||||
['no', 'No Restart'],
|
||||
['always', 'Always Restart'],
|
||||
|
@ -32,11 +32,16 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
|
||||
const refreshAll = () => {
|
||||
setVolumes(null);
|
||||
refresh().then(() => {
|
||||
if(refresh)
|
||||
refresh().then(() => {
|
||||
API.docker.volumeList().then((res) => {
|
||||
setVolumes(res.data.Volumes);
|
||||
});
|
||||
});
|
||||
else
|
||||
API.docker.volumeList().then((res) => {
|
||||
setVolumes(res.data.Volumes);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (<Stack spacing={2}>
|
||||
|
@ -65,9 +70,11 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
if (unique.length !== volumes.length) {
|
||||
errors.submit = 'Mounts must have unique targets';
|
||||
}
|
||||
OnChange && OnChange(values);
|
||||
return errors;
|
||||
}}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
if(newContainer) return;
|
||||
setSubmitting(true);
|
||||
const realvalues = {
|
||||
Volumes: values.volumes
|
||||
|
@ -89,7 +96,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<MainCard title={'Volume Mounts'}>
|
||||
{containerInfo.State.Status !== 'running' && (
|
||||
{containerInfo.State && containerInfo.State.Status !== 'running' && (
|
||||
<Alert severity="warning" style={{ marginBottom: '15px' }}>
|
||||
This container is not running. Editing any settings will cause the container to start again.
|
||||
</Alert>
|
||||
|
@ -230,7 +237,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
<LoadingButton
|
||||
{!newContainer && <LoadingButton
|
||||
fullWidth
|
||||
disableElevation
|
||||
disabled={formik.errors.submit}
|
||||
|
@ -241,7 +248,7 @@ const VolumeContainerSetup = ({ config, containerInfo, refresh }) => {
|
|||
color="primary"
|
||||
>
|
||||
Update Volumes
|
||||
</LoadingButton>
|
||||
</LoadingButton>}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
113
client/src/pages/servapps/linkContainersButton.jsx
Normal file
113
client/src/pages/servapps/linkContainersButton.jsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
// 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';
|
||||
import ResponsiveButton from '../../components/responseiveButton';
|
||||
import { CosmosContainerPicker } from '../config/users/containerPicker';
|
||||
import { randomString } from '../../utils/indexs';
|
||||
|
||||
const LinkContainersButton = ({ fullWidth, refresh, originContainer, newContainer, OnConnect }) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
container: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
container: Yup.string().required('Required'),
|
||||
}),
|
||||
validate: (values) => {
|
||||
const errors = {};
|
||||
if (values.container === originContainer) {
|
||||
errors.container = 'Cannot link to itself';
|
||||
}
|
||||
return errors;
|
||||
},
|
||||
onSubmit: (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
let name = 'link-' + originContainer + '-' + values.container + '-' + randomString(3);
|
||||
return API.docker.createNetwork({
|
||||
name,
|
||||
driver : 'bridge',
|
||||
attachCosmos: false,
|
||||
})
|
||||
.then((res) => {
|
||||
return Promise.all([
|
||||
newContainer ? OnConnect(name) : API.docker.attachNetwork(originContainer, name),
|
||||
API.docker.attachNetwork(values.container, name),
|
||||
])
|
||||
.then(() => {
|
||||
setStatus({ success: true });
|
||||
setSubmitting(false);
|
||||
setIsOpened(false);
|
||||
refresh && refresh();
|
||||
})
|
||||
.catch((err) => {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: err.message });
|
||||
setSubmitting(false);
|
||||
});
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpened} onClose={() => setIsOpened(false)}>
|
||||
<FormikProvider value={formik}>
|
||||
<DialogTitle>Link with container</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={2} width={'300px'} style={{ marginTop: '10px' }}>
|
||||
<CosmosContainerPicker
|
||||
formik={formik}
|
||||
onTargetChange={(_, name) => {
|
||||
formik.setFieldValue('container', name);
|
||||
}}
|
||||
nameOnly
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
{formik.errors.submit && (
|
||||
<Grid item xs={12}>
|
||||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setIsOpened(false)}>Cancel</Button>
|
||||
<LoadingButton
|
||||
disabled={formik.errors.submit}
|
||||
onClick={formik.handleSubmit}
|
||||
loading={formik.isSubmitting}>
|
||||
Link Containers
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</FormikProvider>
|
||||
</Dialog>
|
||||
<ResponsiveButton
|
||||
fullWidth={fullWidth}
|
||||
onClick={() => setIsOpened(true)}
|
||||
startIcon={<PlusCircleOutlined />}
|
||||
>
|
||||
Link Containers
|
||||
</ResponsiveButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkContainersButton;
|
|
@ -17,6 +17,7 @@ import { Link } from 'react-router-dom';
|
|||
import ExposeModal from './exposeModal';
|
||||
import GetActions from './actionBar';
|
||||
import ResponsiveButton from '../../components/responseiveButton';
|
||||
import DockerComposeImport from './containers/docker-compose';
|
||||
|
||||
const Item = styled(Paper)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
|
||||
|
@ -144,14 +145,13 @@ const ServeApps = () => {
|
|||
<ResponsiveButton variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
|
||||
refreshServeApps();
|
||||
}}>Refresh</ResponsiveButton>
|
||||
<Tooltip title="This is not implemented yet.">
|
||||
<span style={{ cursor: 'not-allowed' }}>
|
||||
<ResponsiveButton
|
||||
variant="contained"
|
||||
startIcon={<AppstoreAddOutlined />}
|
||||
disabled >Start ServApp</ResponsiveButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Link to="/ui/servapps/new-service">
|
||||
<ResponsiveButton
|
||||
variant="contained"
|
||||
startIcon={<AppstoreAddOutlined />}
|
||||
>Start ServApp</ResponsiveButton>
|
||||
</Link>
|
||||
<DockerComposeImport />
|
||||
</Stack>
|
||||
|
||||
<Grid2 container spacing={{xs: 1, sm: 1, md: 2 }}>
|
||||
|
|
|
@ -12,6 +12,8 @@ import RouteConfigPage from '../pages/config/routeConfigPage';
|
|||
import logo from '../assets/images/icons/cosmos.png';
|
||||
import HomePage from '../pages/home';
|
||||
import ContainerIndex from '../pages/servapps/containers';
|
||||
import NewDockerService from '../pages/servapps/containers/newService';
|
||||
import NewDockerServiceForm from '../pages/servapps/containers/newServiceForm';
|
||||
|
||||
|
||||
// render - dashboard
|
||||
|
@ -63,6 +65,10 @@ const MainRoutes = {
|
|||
path: '/ui/config-general',
|
||||
element: <ConfigManagement />
|
||||
},
|
||||
{
|
||||
path: '/ui/servapps/new-service',
|
||||
element: <NewDockerServiceForm />
|
||||
},
|
||||
{
|
||||
path: '/ui/config-url',
|
||||
element: <ProxyManagement />
|
||||
|
|
8
client/src/utils/indexs.js
Normal file
8
client/src/utils/indexs.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
export const randomString = (length) => {
|
||||
let text = "";
|
||||
const possible =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
for (let i = 0; i < length; i++)
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
return text;
|
||||
}
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.4.0-unstable2",
|
||||
"version": "0.5.0-unstable",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cosmos-server",
|
||||
"version": "0.4.0-unstable2",
|
||||
"version": "0.5.0-unstable",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
|
@ -25,6 +25,7 @@
|
|||
"formik": "^2.2.9",
|
||||
"framer-motion": "^7.3.6",
|
||||
"history": "^5.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
|
@ -4089,8 +4090,7 @@
|
|||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.1.3",
|
||||
|
@ -7087,7 +7087,6 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
@ -12203,8 +12202,7 @@
|
|||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "5.1.3",
|
||||
|
@ -14392,7 +14390,6 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.5.0-unstable",
|
||||
"version": "0.5.0-unstable2",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
@ -25,6 +25,7 @@
|
|||
"formik": "^2.2.9",
|
||||
"framer-motion": "^7.3.6",
|
||||
"history": "^5.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.3",
|
||||
|
|
|
@ -3,7 +3,6 @@ package configapi
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
@ -14,15 +13,13 @@ type UpdateRouteRequest struct {
|
|||
NewRoute *utils.ProxyRouteConfig `json:"newRoute,omitempty"`
|
||||
}
|
||||
|
||||
var configLock sync.Mutex
|
||||
|
||||
func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
utils.ConfigLock.Lock()
|
||||
defer utils.ConfigLock.Unlock()
|
||||
|
||||
var updateReq UpdateRouteRequest
|
||||
err := json.NewDecoder(req.Body).Decode(&updateReq)
|
||||
|
|
564
src/docker/api_blueprint.go
Normal file
564
src/docker/api_blueprint.go
Normal file
|
@ -0,0 +1,564 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"bufio"
|
||||
"errors"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
conttype "github.com/docker/docker/api/types/container"
|
||||
doctype "github.com/docker/docker/api/types"
|
||||
strslice "github.com/docker/docker/api/types/strslice"
|
||||
volumetype "github.com/docker/docker/api/types/volume"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
type ContainerCreateRequestContainer struct {
|
||||
Name string `json:"container_name"`
|
||||
Image string `json:"image"`
|
||||
Environment []string `json:"environment"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Ports []string `json:"ports"`
|
||||
Volumes []mount.Mount `json:"volumes"`
|
||||
Networks map[string]struct {
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
IPV4Address string `json:"ipv4_address,omitempty"`
|
||||
IPV6Address string `json:"ipv6_address,omitempty"`
|
||||
} `json:"networks"`
|
||||
Routes []utils.ProxyRouteConfig `json:"routes"`
|
||||
|
||||
RestartPolicy string `json:"restart,omitempty"`
|
||||
Devices []string `json:"devices"`
|
||||
Expose []string `json:"expose"`
|
||||
DependsOn []string `json:"depends_on"`
|
||||
Tty bool `json:"tty,omitempty"`
|
||||
StdinOpen bool `json:"stdin_open,omitempty"`
|
||||
|
||||
Command string `json:"command,omitempty"`
|
||||
Entrypoint string `json:"entrypoint,omitempty"`
|
||||
WorkingDir string `json:"working_dir,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
Domainname string `json:"domainname,omitempty"`
|
||||
MacAddress string `json:"mac_address,omitempty"`
|
||||
Privileged bool `json:"privileged,omitempty"`
|
||||
NetworkMode string `json:"network_mode,omitempty"`
|
||||
StopSignal string `json:"stop_signal,omitempty"`
|
||||
StopGracePeriod int `json:"stop_grace_period,omitempty"`
|
||||
HealthCheck struct {
|
||||
Test []string `json:"test"`
|
||||
Interval int `json:"interval"`
|
||||
Timeout int `json:"timeout"`
|
||||
Retries int `json:"retries"`
|
||||
StartPeriod int `json:"start_period"`
|
||||
} `json:"healthcheck,omitempty"`
|
||||
DNS []string `json:"dns,omitempty"`
|
||||
DNSSearch []string `json:"dns_search,omitempty"`
|
||||
ExtraHosts []string `json:"extra_hosts,omitempty"`
|
||||
Links []string `json:"links,omitempty"`
|
||||
SecurityOpt []string `json:"security_opt,omitempty"`
|
||||
StorageOpt map[string]string `json:"storage_opt,omitempty"`
|
||||
Sysctls map[string]string `json:"sysctls,omitempty"`
|
||||
Isolation string `json:"isolation,omitempty"`
|
||||
|
||||
CapAdd []string `json:"cap_add,omitempty"`
|
||||
CapDrop []string `json:"cap_drop,omitempty"`
|
||||
SysctlsMap map[string]string `json:"sysctls,omitempty"`
|
||||
}
|
||||
|
||||
type ContainerCreateRequestVolume struct {
|
||||
// name must be unique
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
type ContainerCreateRequestNetwork struct {
|
||||
// name must be unique
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver"`
|
||||
Attachable bool `json:"attachable"`
|
||||
Internal bool `json:"internal"`
|
||||
EnableIPv6 bool `json:"enable_ipv6"`
|
||||
IPAM struct {
|
||||
Driver string `json:"driver"`
|
||||
Config []struct {
|
||||
Subnet string `json:"subnet"`
|
||||
} `json:"config"`
|
||||
} `json:"ipam"`
|
||||
}
|
||||
|
||||
type DockerServiceCreateRequest struct {
|
||||
Services map[string]ContainerCreateRequestContainer `json:"services"`
|
||||
Volumes []ContainerCreateRequestVolume `json:"volumes"`
|
||||
Networks map[string]ContainerCreateRequestNetwork `json:"networks"`
|
||||
}
|
||||
|
||||
type DockerServiceCreateRollback struct {
|
||||
// action: disconnect, remove, etc...
|
||||
Action string `json:"action"`
|
||||
// type: container, volume, network
|
||||
Type string `json:"type"`
|
||||
// name: container name, volume name, network name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func Rollback(actions []DockerServiceCreateRollback , w http.ResponseWriter, flusher http.Flusher) {
|
||||
for i := len(actions) - 1; i >= 0; i-- {
|
||||
action := actions[i]
|
||||
switch action.Type {
|
||||
case "container":
|
||||
DockerClient.ContainerKill(DockerContext, action.Name, "SIGKILL")
|
||||
err := DockerClient.ContainerRemove(DockerContext, action.Name, doctype.ContainerRemoveOptions{})
|
||||
if err != nil {
|
||||
utils.Error("Rollback: Container", err)
|
||||
} else {
|
||||
utils.Log(fmt.Sprintf("Rolled back container %s", action.Name))
|
||||
fmt.Fprintf(w, "Rolled back container %s\n", action.Name)
|
||||
flusher.Flush()
|
||||
}
|
||||
case "volume":
|
||||
err := DockerClient.VolumeRemove(DockerContext, action.Name, true)
|
||||
if err != nil {
|
||||
utils.Error("Rollback: Volume", err)
|
||||
} else {
|
||||
utils.Log(fmt.Sprintf("Rolled back volume %s", action.Name))
|
||||
fmt.Fprintf(w, "Rolled back volume %s\n", action.Name)
|
||||
flusher.Flush()
|
||||
}
|
||||
case "network":
|
||||
err := DockerClient.NetworkRemove(DockerContext, action.Name)
|
||||
if err != nil {
|
||||
utils.Error("Rollback: Network", err)
|
||||
} else {
|
||||
utils.Log(fmt.Sprintf("Rolled back network %s", action.Name))
|
||||
fmt.Fprintf(w, "Rolled back network %s\n", action.Name)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After all operations
|
||||
utils.Error("CreateService", fmt.Errorf("Operation failed. Changes have been rolled back."))
|
||||
fmt.Fprintf(w, "[OPERATION FAILED]. CHANGES HAVE BEEN ROLLEDBACK.\n")
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
func CreateServiceRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("CreateService", errD)
|
||||
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "POST" {
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
var serviceRequest DockerServiceCreateRequest
|
||||
err := decoder.Decode(&serviceRequest)
|
||||
if err != nil {
|
||||
utils.Error("CreateService", err)
|
||||
utils.HTTPError(w, "Bad request: "+err.Error(), http.StatusBadRequest, "DS003")
|
||||
return
|
||||
}
|
||||
|
||||
var rollbackActions []DockerServiceCreateRollback
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
utils.Error("CreateService: Method not allowed" + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]ContainerCreateRequestContainer, error) {
|
||||
startOrder := []ContainerCreateRequestContainer{}
|
||||
|
||||
for len(serviceMap) > 0 {
|
||||
// Keep track of whether we've added any services in this iteration
|
||||
changed := false
|
||||
|
||||
for name, service := range serviceMap {
|
||||
// Check if all dependencies are already in startOrder
|
||||
allDependenciesStarted := true
|
||||
for _, dependency := range service.DependsOn {
|
||||
dependencyStarted := false
|
||||
for _, startedService := range startOrder {
|
||||
if startedService.Name == dependency {
|
||||
dependencyStarted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !dependencyStarted {
|
||||
allDependenciesStarted = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If all dependencies are started, we can add this service to startOrder
|
||||
if allDependenciesStarted {
|
||||
startOrder = append(startOrder, service)
|
||||
delete(serviceMap, name)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't added any services in this iteration, then there must be a circular dependency
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If there are any services left in serviceMap, they couldn't be started due to unsatisfied dependencies or circular dependencies
|
||||
if len(serviceMap) > 0 {
|
||||
errorMessage := "Could not start all services due to unsatisfied dependencies or circular dependencies:\n"
|
||||
for name, _ := range serviceMap {
|
||||
errorMessage += "Could not start service: " + name + "\n"
|
||||
errorMessage += "Unsatisfied dependencies:\n"
|
||||
for _, dependency := range serviceMap[name].DependsOn {
|
||||
_, ok := serviceMap[dependency]
|
||||
if ok {
|
||||
errorMessage += dependency + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
return startOrder, nil
|
||||
}
|
43
src/docker/api_images.go
Normal file
43
src/docker/api_images.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func InspectImageRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("InspectImage", errD)
|
||||
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002")
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
imageName := utils.SanitizeSafe(vars["imageName"])
|
||||
|
||||
utils.Log("InspectImage " + imageName)
|
||||
|
||||
if req.Method == "GET" {
|
||||
image, _, err := DockerClient.ImageInspectWithRaw(DockerContext, imageName)
|
||||
if err != nil {
|
||||
utils.Error("InspectImage", err)
|
||||
utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS002")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(image)
|
||||
} else {
|
||||
utils.Error("InspectImage: Method not allowed" + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
|
@ -218,6 +218,8 @@ func StartServer() {
|
|||
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
||||
srapi.HandleFunc("/api/users", user.UsersRoute)
|
||||
|
||||
srapi.HandleFunc("/api/images/{imageName}", docker.InspectImageRoute)
|
||||
|
||||
srapi.HandleFunc("/api/volume/{volumeName}", docker.DeleteVolumeRoute)
|
||||
srapi.HandleFunc("/api/volumes", docker.VolumesRoute)
|
||||
|
||||
|
@ -234,6 +236,7 @@ func StartServer() {
|
|||
srapi.HandleFunc("/api/servapps/{containerId}/networks", docker.NetworkContainerRoutes)
|
||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||
|
||||
srapi.HandleFunc("/api/docker-service", docker.CreateServiceRoute)
|
||||
|
||||
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
srapi.Use(utils.EnsureHostname)
|
||||
|
|
|
@ -25,6 +25,7 @@ type userBan struct {
|
|||
ClientID string
|
||||
banType int
|
||||
time time.Time
|
||||
reason string
|
||||
}
|
||||
|
||||
type smartShieldState struct {
|
||||
|
@ -94,6 +95,23 @@ func (shield *smartShieldState) GetUserUsedBudgets(ClientID string) userUsedBudg
|
|||
return userConsumed
|
||||
}
|
||||
|
||||
func (shield *smartShieldState) GetLastBan(policy utils.SmartShieldPolicy, userConsumed userUsedBudget) *userBan {
|
||||
shield.Lock()
|
||||
defer shield.Unlock()
|
||||
|
||||
ClientID := userConsumed.ClientID
|
||||
|
||||
// Check for bans
|
||||
for i := len(shield.bans) - 1; i >= 0; i-- {
|
||||
ban := shield.bans[i]
|
||||
if ban.banType == STRIKE && ban.ClientID == ClientID {
|
||||
return ban
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (shield *smartShieldState) isAllowedToReqest(policy utils.SmartShieldPolicy, userConsumed userUsedBudget) bool {
|
||||
shield.Lock()
|
||||
defer shield.Unlock()
|
||||
|
@ -106,16 +124,15 @@ func (shield *smartShieldState) isAllowedToReqest(policy utils.SmartShieldPolicy
|
|||
// Check for bans
|
||||
for i := len(shield.bans) - 1; i >= 0; i-- {
|
||||
ban := shield.bans[i]
|
||||
if ban.banType == PERM {
|
||||
if ban.banType == PERM && ban.ClientID == ClientID {
|
||||
return false
|
||||
} else if ban.banType == TEMP {
|
||||
} else if ban.banType == TEMP && ban.ClientID == ClientID {
|
||||
if(ban.time.Add(4 * 3600 * time.Second).Before(time.Now())) {
|
||||
return false
|
||||
} else if (ban.time.Add(72 * 3600 * time.Second).Before(time.Now())) {
|
||||
nbTempBans++
|
||||
}
|
||||
} else if ban.banType == STRIKE {
|
||||
return false
|
||||
} else if ban.banType == STRIKE && ban.ClientID == ClientID {
|
||||
if(ban.time.Add(3600 * time.Second).Before(time.Now())) {
|
||||
return false
|
||||
} else if (ban.time.Add(24 * 3600 * time.Second).Before(time.Now())) {
|
||||
|
@ -154,6 +171,7 @@ func (shield *smartShieldState) isAllowedToReqest(policy utils.SmartShieldPolicy
|
|||
ClientID: ClientID,
|
||||
banType: STRIKE,
|
||||
time: time.Now(),
|
||||
reason: fmt.Sprintf("%+v out of %+v", userConsumed, policy),
|
||||
})
|
||||
utils.Warn("User " + ClientID + " has received a strike: "+ fmt.Sprintf("%+v", userConsumed))
|
||||
return false
|
||||
|
@ -285,8 +303,8 @@ func SmartShieldMiddleware(policy utils.SmartShieldPolicy) func(http.Handler) ht
|
|||
userConsumed := shield.GetUserUsedBudgets(clientID)
|
||||
|
||||
if !isPrivileged(r, policy) && !shield.isAllowedToReqest(policy, userConsumed) {
|
||||
utils.Log("SmartShield: User is blocked due to abuse: " + fmt.Sprintf("%+v", userConsumed))
|
||||
|
||||
lastBan := shield.GetLastBan(policy, userConsumed)
|
||||
utils.Log("SmartShield: User is blocked due to abuse: " + fmt.Sprintf("%+v", lastBan))
|
||||
http.Error(w, "Too many requests", http.StatusTooManyRequests)
|
||||
return
|
||||
} else {
|
||||
|
|
|
@ -11,11 +11,14 @@ import (
|
|||
"strings"
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"sync"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
var ConfigLock sync.Mutex
|
||||
|
||||
var BaseMainConfig Config
|
||||
var MainConfig Config
|
||||
var IsHTTPS = false
|
||||
|
|
Loading…
Reference in a new issue