[release] v0.4.0-unstable3
This commit is contained in:
parent
8ce9d52fbd
commit
ac6fbe64e7
|
@ -1,7 +1,8 @@
|
|||
## Version 0.4.0
|
||||
- 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
|
||||
- Stop / Start / Restart / Remove / Kill containers
|
||||
-
|
||||
|
||||
## Version 0.3.0
|
||||
- Implement 2 FA
|
||||
|
|
|
@ -9,6 +9,70 @@ function list() {
|
|||
}))
|
||||
}
|
||||
|
||||
function get(containerName) {
|
||||
return wrap(fetch('/cosmos/api/servapps/' + containerName, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function getContainerLogs(containerId, searchQuery, limit, lastReceivedLogs, errorOnly) {
|
||||
|
||||
if(limit < 50) limit = 50;
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
search: searchQuery || "",
|
||||
limit: limit || "",
|
||||
lastReceivedLogs: lastReceivedLogs || "",
|
||||
errorOnly: errorOnly || "",
|
||||
});
|
||||
|
||||
return wrap(fetch(`/cosmos/api/servapps/${containerId}/logs?${queryParams}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function volumeList() {
|
||||
return wrap(fetch('/cosmos/api/volumes', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function volumeDelete(name) {
|
||||
return wrap(fetch(`/cosmos/api/volume/${name}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function networkList() {
|
||||
return wrap(fetch('/cosmos/api/networks', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function networkDelete(name) {
|
||||
return wrap(fetch(`/cosmos/api/network/${name}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function secure(id, res) {
|
||||
return wrap(fetch('/cosmos/api/servapps/' + id + '/secure/'+res, {
|
||||
method: 'GET',
|
||||
|
@ -38,7 +102,13 @@ const manageContainer = (id, action) => {
|
|||
|
||||
export {
|
||||
list,
|
||||
get,
|
||||
newDB,
|
||||
secure,
|
||||
manageContainer
|
||||
manageContainer,
|
||||
volumeList,
|
||||
volumeDelete,
|
||||
networkList,
|
||||
networkDelete,
|
||||
getContainerLogs
|
||||
};
|
|
@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
|||
import { getOrigin, getFullOrigin } from "../utils/routes";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
const HostChip = ({route, settings}) => {
|
||||
const HostChip = ({route, settings, style}) => {
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
const [isOnline, setIsOnline] = useState(null);
|
||||
|
@ -27,6 +27,7 @@ const HostChip = ({route, settings}) => {
|
|||
style={{
|
||||
paddingRight: '4px',
|
||||
textDecoration: isOnline ? 'none' : 'underline wavy red',
|
||||
...style
|
||||
}}
|
||||
onClick={() => {
|
||||
if(route.UseHost)
|
||||
|
|
103
client/src/components/logLine.jsx
Normal file
103
client/src/components/logLine.jsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { Stack } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
function decodeUnicode(str) {
|
||||
return str.replace(/\\u([0-9a-zA-Z]{3-5})/g, (match, p1) => {
|
||||
return String.fromCharCode(parseInt(p1, 16));
|
||||
});
|
||||
}
|
||||
|
||||
const LogLine = ({ message, docker, isMobile }) => {
|
||||
let html = decodeUnicode(message)
|
||||
.replace('\u0001\u0000\u0000\u0000\u0000\u0000\u0000', '')
|
||||
.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/<2F>/g, '')
|
||||
.replace(/\x1b\[([0-9]{1,2}(?:;[0-9]{1,2})*)?m/g, (match, p1) => {
|
||||
if (!p1) {
|
||||
return '</span>';
|
||||
}
|
||||
const codes = p1.split(';');
|
||||
const styles = [];
|
||||
for (const code of codes) {
|
||||
switch (code) {
|
||||
case '1':
|
||||
styles.push('font-weight:bold');
|
||||
break;
|
||||
case '3':
|
||||
styles.push('font-style:italic');
|
||||
break;
|
||||
case '4':
|
||||
styles.push('text-decoration:underline');
|
||||
break;
|
||||
case '30':
|
||||
case '31':
|
||||
case '32':
|
||||
case '33':
|
||||
case '34':
|
||||
case '35':
|
||||
case '36':
|
||||
case '37':
|
||||
case '90':
|
||||
case '91':
|
||||
case '92':
|
||||
case '93':
|
||||
case '94':
|
||||
case '95':
|
||||
case '96':
|
||||
case '97':
|
||||
styles.push(`color:${getColor(code)}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `<span style="${styles.join(';')}">`;
|
||||
});
|
||||
|
||||
if(docker) {
|
||||
let parts = html.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)
|
||||
let restString = html.replace(parts[0], '')
|
||||
|
||||
return <Stack direction={isMobile ? 'column' : 'row'} spacing={1}>
|
||||
<div style={{color:'grey', fontStyle:'italic', whiteSpace: 'pre'}}>
|
||||
{parts[0].replace('T', ' ').split('.')[0]}
|
||||
</div>
|
||||
<div dangerouslySetInnerHTML={{ __html: restString }} />
|
||||
</Stack>;
|
||||
}
|
||||
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
};
|
||||
|
||||
const getColor = (code) => {
|
||||
switch (code) {
|
||||
case '30':
|
||||
case '90':
|
||||
return 'black';
|
||||
case '31':
|
||||
case '91':
|
||||
return 'red';
|
||||
case '32':
|
||||
case '92':
|
||||
return 'green';
|
||||
case '33':
|
||||
case '93':
|
||||
return 'yellow';
|
||||
case '34':
|
||||
case '94':
|
||||
return 'blue';
|
||||
case '35':
|
||||
case '95':
|
||||
return 'magenta';
|
||||
case '36':
|
||||
case '96':
|
||||
return 'cyan';
|
||||
case '37':
|
||||
case '97':
|
||||
return 'white';
|
||||
default:
|
||||
return 'inherit';
|
||||
}
|
||||
};
|
||||
|
||||
export default LogLine;
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, Tab, Tabs, Typography, MenuItem, Select, useMediaQuery } from '@mui/material';
|
||||
import { Box, Tab, Tabs, Typography, MenuItem, Select, useMediaQuery, CircularProgress } from '@mui/material';
|
||||
import { styled } from '@mui/system';
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
|
@ -36,7 +36,7 @@ const a11yProps = (index) => {
|
|||
};
|
||||
};
|
||||
|
||||
const PrettyTabbedView = ({ tabs }) => {
|
||||
const PrettyTabbedView = ({ tabs, isLoading }) => {
|
||||
const [value, setValue] = useState(0);
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||
|
||||
|
@ -67,15 +67,32 @@ const PrettyTabbedView = ({ tabs }) => {
|
|||
aria-label="Vertical tabs"
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab key={index} label={tab.title} {...a11yProps(index)} />
|
||||
<Tab
|
||||
style={{fontWeight: !tab.children ? '1000' : '', }}
|
||||
disabled={!tab.children} key={index}
|
||||
label={tab.title} {...a11yProps(index)}
|
||||
/>
|
||||
))}
|
||||
</StyledTabs>
|
||||
)}
|
||||
{tabs.map((tab, index) => (
|
||||
{!isLoading && tabs.map((tab, index) => (
|
||||
<TabPanel key={index} value={value} index={index}>
|
||||
{tab.children}
|
||||
</TabPanel>
|
||||
))}
|
||||
{isLoading && (
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="100%"
|
||||
height="100%"
|
||||
color="text.primary"
|
||||
p={2}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,6 +20,8 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
|||
sm: useMediaQuery((theme) => theme.breakpoints.up('sm')),
|
||||
md: useMediaQuery((theme) => theme.breakpoints.up('md')),
|
||||
lg: useMediaQuery((theme) => theme.breakpoints.up('lg')),
|
||||
xl: useMediaQuery((theme) => theme.breakpoints.up('xl')),
|
||||
xxl: useMediaQuery((theme) => theme.breakpoints.up('xxl')),
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -61,4 +61,8 @@
|
|||
color:white;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.darken {
|
||||
filter: brightness(0.5);
|
||||
}
|
|
@ -7,6 +7,7 @@ import { useEffect, useState } from "react";
|
|||
import * as API from "../../api";
|
||||
import RouteSecurity from "./routes/routeSecurity";
|
||||
import RouteOverview from "./routes/routeoverview";
|
||||
import IsLoggedIn from "../../isLoggedIn";
|
||||
|
||||
const RouteConfigPage = () => {
|
||||
const { routeName } = useParams();
|
||||
|
@ -28,6 +29,7 @@ const RouteConfigPage = () => {
|
|||
}, []);
|
||||
|
||||
return <div>
|
||||
<IsLoggedIn />
|
||||
<h2>
|
||||
<Stack spacing={1}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
|
|
|
@ -7,6 +7,13 @@ import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
|||
import { getFaviconURL } from '../../../utils/routes';
|
||||
import * as API from '../../../api';
|
||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
|
||||
const info = {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
}
|
||||
|
||||
const RouteOverview = ({ routeConfig }) => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
|
@ -35,7 +42,7 @@ const RouteOverview = ({ routeConfig }) => {
|
|||
</div>
|
||||
<Stack spacing={2} >
|
||||
<strong>Description</strong>
|
||||
<div>{routeConfig.Description}</div>
|
||||
<div style={info}>{routeConfig.Description}</div>
|
||||
<strong>URL</strong>
|
||||
<div><HostChip route={routeConfig} /></div>
|
||||
<strong>Target</strong>
|
||||
|
|
|
@ -23,6 +23,9 @@ const NewInstall = () => {
|
|||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [status, setStatus] = useState(null);
|
||||
const [counter, setCounter] = useState(0);
|
||||
let [hostname, setHostname] = useState('');
|
||||
const [databaseEnable, setDatabaseEnable] = useState(true);
|
||||
|
||||
const refreshStatus = async () => {
|
||||
try {
|
||||
const res = await API.getStatus()
|
||||
|
@ -34,7 +37,7 @@ const NewInstall = () => {
|
|||
if (typeof status !== 'undefined') {
|
||||
setTimeout(() => {
|
||||
setCounter(counter + 1);
|
||||
}, 2000);
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +46,7 @@ const NewInstall = () => {
|
|||
}, [counter]);
|
||||
|
||||
useEffect(() => {
|
||||
if(activeStep == 4 && status && !status.database) {
|
||||
if(activeStep == 4 && status && !databaseEnable) {
|
||||
setActiveStep(5);
|
||||
}
|
||||
}, [activeStep, status]);
|
||||
|
@ -122,8 +125,12 @@ const NewInstall = () => {
|
|||
MongoDBMode: values.DBMode,
|
||||
MongoDB: values.MongoDB,
|
||||
});
|
||||
if(res.status == "OK")
|
||||
if(res.status == "OK") {
|
||||
if(values.DBMode === "DisableUserManagement") {
|
||||
setDatabaseEnable(false);
|
||||
}
|
||||
setStatus({ success: true });
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: error.message });
|
||||
|
@ -205,9 +212,14 @@ const NewInstall = () => {
|
|||
If you enable HTTPS, it will be effective after the next restart.
|
||||
</div>
|
||||
<div>
|
||||
{status && <div>
|
||||
HTTPS Certificate Mode is currently: <b>{status.HTTPSCertificateMode}</b>
|
||||
</div>}
|
||||
{status && <>
|
||||
<div>
|
||||
HTTPS Certificate Mode is currently: <b>{status.HTTPSCertificateMode}</b>
|
||||
</div>
|
||||
<div>
|
||||
Hostname is currently: <b>{status.hostname}</b>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
<div>
|
||||
<Formik
|
||||
|
@ -245,8 +257,10 @@ const NewInstall = () => {
|
|||
TLSCert: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSCert : '',
|
||||
Hostname: values.Hostname,
|
||||
});
|
||||
if(res.status == "OK")
|
||||
if(res.status == "OK") {
|
||||
setStatus({ success: true });
|
||||
setHostname((values.HTTPSCertificateMode == "DISABLED" ? "http://" : "https://") + values.Hostname);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus({ success: false });
|
||||
setErrors({ submit: "Please check you have filled all the inputs properly" });
|
||||
|
@ -264,11 +278,15 @@ const NewInstall = () => {
|
|||
["LETSENCRYPT", "Use Let's Encrypt automatic HTTPS (recommended)"],
|
||||
["PROVIDED", "Supply my own HTTPS certificate"],
|
||||
["SELFSIGNED", "Generate a self-signed certificate"],
|
||||
["DISABLE", "Use HTTP only (not recommended)"],
|
||||
["DISABLED", "Use HTTP only (not recommended)"],
|
||||
]}
|
||||
/>
|
||||
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
||||
<>
|
||||
<Alert severity="warning">
|
||||
If you are using Cloudflare, make sure the DNS record is <strong>NOT</strong> set to <b>Proxied</b> (you should not see the orange cloud but a grey one).
|
||||
Otherwise Cloudflare will not allow Let's Encrypt to verify your domain.
|
||||
</Alert>
|
||||
<CosmosInputText
|
||||
name="SSLEmail"
|
||||
label="Let's Encrypt Email"
|
||||
|
@ -457,7 +475,12 @@ const NewInstall = () => {
|
|||
<Button
|
||||
variant="contained"
|
||||
startIcon={<LeftOutlined />}
|
||||
onClick={() => setActiveStep(activeStep - 1)}
|
||||
onClick={() => {
|
||||
if(activeStep == 5 && !databaseEnable) {
|
||||
setActiveStep(activeStep - 2)
|
||||
}
|
||||
setActiveStep(activeStep - 1)
|
||||
}}
|
||||
disabled={activeStep <= 0}
|
||||
>Back</Button>
|
||||
|
||||
|
@ -471,7 +494,7 @@ const NewInstall = () => {
|
|||
step: "5",
|
||||
})
|
||||
setTimeout(() => {
|
||||
window.location.href = "/ui/login";
|
||||
window.location.href = hostname + "/ui/login";
|
||||
}, 500);
|
||||
} else
|
||||
setActiveStep(activeStep + 1)
|
||||
|
|
93
client/src/pages/servapps/actionBar.jsx
Normal file
93
client/src/pages/servapps/actionBar.jsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
import React from 'react';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
import { CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, ReloadOutlined, RollbackOutlined, StopOutlined, UpCircleOutlined } from '@ant-design/icons';
|
||||
import * as API from '../../api';
|
||||
|
||||
const GetActions = ({
|
||||
Id,
|
||||
state,
|
||||
refreshServeApps,
|
||||
setIsUpdatingId
|
||||
}) => {
|
||||
const doTo = (action) => {
|
||||
setIsUpdatingId(Id, true);
|
||||
API.docker.manageContainer(Id, action).then((res) => {
|
||||
refreshServeApps();
|
||||
});
|
||||
};
|
||||
|
||||
let actions = [
|
||||
{
|
||||
t: 'Update Available',
|
||||
if: ['update_available'],
|
||||
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size='large'>
|
||||
<UpCircleOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Start',
|
||||
if: ['exited', 'created'],
|
||||
e: <IconButton onClick={() => {doTo('start')}} size='large'>
|
||||
<PlaySquareOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Unpause',
|
||||
if: ['paused'],
|
||||
e: <IconButton onClick={() => {doTo('unpause')}} size='large'>
|
||||
<PlaySquareOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Pause',
|
||||
if: ['running'],
|
||||
e: <IconButton onClick={() => {doTo('pause')}} size='large'>
|
||||
<PauseCircleOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Stop',
|
||||
if: ['paused', 'restarting', 'running'],
|
||||
e: <IconButton onClick={() => {doTo('stop')}} size='large' variant="outlined">
|
||||
<StopOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Restart',
|
||||
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
||||
e: <IconButton onClick={() => doTo('restart')} size='large'>
|
||||
<ReloadOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Re-create',
|
||||
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
||||
e: <IconButton onClick={() => doTo('recreate')} color="error" size='large'>
|
||||
<RollbackOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Kill',
|
||||
if: ['running', 'paused', 'created', 'restarting'],
|
||||
e: <IconButton onClick={() => doTo('kill')} color="error" size='large'>
|
||||
<CloseSquareOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Delete',
|
||||
if: ['exited', 'created'],
|
||||
e: <IconButton onClick={() => {doTo('remove')}} color="error" size='large'>
|
||||
<DeleteOutlined />
|
||||
</IconButton>
|
||||
}
|
||||
];
|
||||
|
||||
return actions.filter((action) => {
|
||||
let updateAvailable = false;
|
||||
return action.if.includes(state) ?? (updateAvailable && action.if.includes('update_available'));
|
||||
}).map((action) => {
|
||||
return <Tooltip title={action.t}>{action.e}</Tooltip>
|
||||
});
|
||||
}
|
||||
|
||||
export default GetActions;
|
86
client/src/pages/servapps/containers/index.jsx
Normal file
86
client/src/pages/servapps/containers/index.jsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
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 HostChip from '../../../components/hostChip';
|
||||
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||
import { getFaviconURL } from '../../../utils/routes';
|
||||
import * as API from '../../../api';
|
||||
import { 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';
|
||||
|
||||
const ContainerIndex = () => {
|
||||
const { containerName } = useParams();
|
||||
const [container, setContainer] = React.useState(null);
|
||||
const [config, setConfig] = React.useState(null);
|
||||
|
||||
const refreshContainer = () => {
|
||||
return Promise.all([API.docker.get(containerName).then((res) => {
|
||||
setContainer(res.data);
|
||||
}),
|
||||
API.config.get().then((res) => {
|
||||
setConfig(res.data);
|
||||
})]);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refreshContainer();
|
||||
}, []);
|
||||
|
||||
return <div>
|
||||
<Stack spacing={1}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Back />
|
||||
<div>{containerName}</div>
|
||||
</Stack>
|
||||
<IsLoggedIn />
|
||||
|
||||
<PrettyTabbedView
|
||||
isLoading={!container || !config}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Overview',
|
||||
children: <ContainerOverview refresh={refreshContainer} containerInfo={container} config={config}/>
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
children: <Logs containerInfo={container} config={config}/>
|
||||
},
|
||||
{
|
||||
title: 'Terminal',
|
||||
children: <Logs containerInfo={container} config={config}/>
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
children: <div>Links</div>
|
||||
},
|
||||
// {
|
||||
// title: 'Advanced'
|
||||
// },
|
||||
{
|
||||
title: 'Setup',
|
||||
children: <div>Image, Restart Policy, Environment Variables, Labels, etc...</div>
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
children: <div>Urls, Networks, Ports, etc...</div>
|
||||
},
|
||||
{
|
||||
title: 'Volumes',
|
||||
children: <div>Volumes</div>
|
||||
},
|
||||
{
|
||||
title: 'Resources',
|
||||
children: <div>Runtime Resources, Capabilities...</div>
|
||||
},
|
||||
]} />
|
||||
</Stack>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default ContainerIndex;
|
193
client/src/pages/servapps/containers/logs.jsx
Normal file
193
client/src/pages/servapps/containers/logs.jsx
Normal file
|
@ -0,0 +1,193 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Button, Checkbox, CircularProgress, Input, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
|
||||
import * as API from '../../../api';
|
||||
import { ReactTerminal } from "react-terminal";
|
||||
import LogLine from '../../../components/logLine';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
const Logs = ({ containerInfo }) => {
|
||||
const { Name, Config, NetworkSettings, State } = containerInfo;
|
||||
const containerName = Name;
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorOnly, setErrorOnly] = useState(false);
|
||||
const [limit, setLimit] = useState(100);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [page, setPage] = useState('');
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [hasScrolled, setHasScrolled] = useState(false);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [forceUpdate, setForceUpdate] = useState(false);
|
||||
const [lastReceivedLogs, setLastReceivedLogs] = useState('');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
const [scrollToMe, setScrollToMe] = useState(null);
|
||||
const screenMin = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||
|
||||
const bottomRef = useRef(null);
|
||||
const topRef = useRef(null);
|
||||
const terminalRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const fetchLogs = async (reset, ignoreState) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await API.docker.getContainerLogs(
|
||||
containerName,
|
||||
searchTerm,
|
||||
limit,
|
||||
ignoreState ? '' : lastReceivedLogs,
|
||||
errorOnly
|
||||
);
|
||||
const { data } = response;
|
||||
if (data.length > 0) {
|
||||
const date = data[0].output.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)[0];
|
||||
if (date) {
|
||||
date.replace(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.)(\d+)Z/, (match, p1, p2) => {
|
||||
const newNumber = parseInt(p2) - 1;
|
||||
const newDate = `${p1}${newNumber}Z`;
|
||||
setLastReceivedLogs(newDate);
|
||||
});
|
||||
} else {
|
||||
console.error('Could not parse date from log: ', data[0]);
|
||||
setLastReceivedLogs('');
|
||||
}
|
||||
}
|
||||
if(reset) {
|
||||
setLogs(data);
|
||||
} else {
|
||||
// const current = topRef.current;
|
||||
// setScrollToMe(() => current);
|
||||
setLogs((logs) => [...data, ...logs]);
|
||||
// calculate the height of the new logs and scroll to that position
|
||||
// OK I will fix this later
|
||||
// const newHeight = 999999999;
|
||||
// terminalRef.current.scrollTop = newHeight;
|
||||
}
|
||||
setHasMore(true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setFetching(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs(true);
|
||||
}, [searchTerm, errorOnly, limit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetching) return;
|
||||
fetchLogs();
|
||||
}, [fetching]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasScrolled) {
|
||||
scrollToBottom();
|
||||
} else {
|
||||
// scrollToMe && scrollToMe.scrollIntoView({ });
|
||||
// setScrollToMe(null);
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
const handleScroll = (event) => {
|
||||
const { scrollTop } = event.target;
|
||||
setHasScrolled(true);
|
||||
if (scrollTop === 0) {
|
||||
if(!hasMore) return;
|
||||
setFetching(true);
|
||||
setHasMore(false);
|
||||
} else {
|
||||
setHasMore(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={2} sx={{ width: '100%'}}>
|
||||
<Stack
|
||||
spacing={2}
|
||||
direction="column"
|
||||
>
|
||||
<Stack direction={screenMin ? 'row' : 'column'} spacing={3}>
|
||||
<Stack direction="row" spacing={3}>
|
||||
<Input
|
||||
label="Search"
|
||||
value={searchTerm}
|
||||
placeholder="Search..."
|
||||
onChange={(e) => {
|
||||
setHasScrolled(false);
|
||||
setSearchTerm(e.target.value);
|
||||
setLastReceivedLogs('');
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Checkbox
|
||||
checked={errorOnly}
|
||||
onChange={(e) => {
|
||||
setHasScrolled(false);
|
||||
setErrorOnly(e.target.checked);
|
||||
setLastReceivedLogs('');
|
||||
}}
|
||||
/>
|
||||
Error Only
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={3}>
|
||||
<Box>
|
||||
<TextField
|
||||
label="Limit"
|
||||
type="number"
|
||||
value={limit}
|
||||
onChange={(e) => {
|
||||
setHasScrolled(false);
|
||||
setLimit(e.target.value);
|
||||
setLastReceivedLogs('');
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setHasScrolled(false);
|
||||
setLastReceivedLogs('');
|
||||
fetchLogs(true, true);
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{loading && <CircularProgress />}
|
||||
</Stack>
|
||||
<Box
|
||||
ref={terminalRef}
|
||||
sx={{
|
||||
maxHeight: 'calc(1vh * 80 - 200px)',
|
||||
overflow: 'auto',
|
||||
paddingRight: '10px',
|
||||
paddingLeft: '10px',
|
||||
paddingBottom: '10px',
|
||||
wordBreak: 'break-all',
|
||||
background: '#272d36',
|
||||
color: '#fff',
|
||||
borderTop: '3px solid ' + theme.palette.primary.main
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{logs.map((log, index) => (
|
||||
<div key={log.index} style={{paddingTop: (!screenMin) ? '10px' : '2px'}}>
|
||||
<LogLine message={log.output} docker isMobile={!screenMin} />
|
||||
</div>
|
||||
))}
|
||||
{fetching && <CircularProgress sx={{ mt: 1, mb: 2 }} />}
|
||||
<div ref={bottomRef} />
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logs;
|
153
client/src/pages/servapps/containers/overview.jsx
Normal file
153
client/src/pages/servapps/containers/overview.jsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
import React from 'react';
|
||||
import { Checkbox, Chip, CircularProgress, Stack, Typography, useMediaQuery } from '@mui/material';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
import { ContainerOutlined, DesktopOutlined, InfoCircleOutlined, NodeExpandOutlined, PlayCircleOutlined, PlusCircleOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { getFaviconURL, getContainersRoutes } from '../../../utils/routes';
|
||||
import HostChip from '../../../components/hostChip';
|
||||
import ExposeModal from '../exposeModal';
|
||||
import * as API from '../../../api';
|
||||
import RestartModal from '../../config/users/restart';
|
||||
import GetActions from '../actionBar';
|
||||
|
||||
const info = {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
}
|
||||
|
||||
const ContainerOverview = ({ containerInfo, config, refresh }) => {
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const [openRestartModal, setOpenRestartModal] = React.useState(false);
|
||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||
|
||||
const { Name, Config, NetworkSettings, State } = containerInfo;
|
||||
const Image = Config.Image;
|
||||
const IPAddress = NetworkSettings.Networks?.[Object.keys(NetworkSettings.Networks)[0]]?.IPAddress;
|
||||
const Health = State.Health;
|
||||
const healthStatus = Health ? Health.Status : 'Healthy';
|
||||
const healthIconColor = healthStatus === 'Healthy' ? 'green' : 'red';
|
||||
const routes = getContainersRoutes(config, Name.replace('/', ''));
|
||||
|
||||
let refreshAll = refresh && (() => refresh().then(() => {
|
||||
setIsUpdating(false);
|
||||
}));
|
||||
|
||||
const updateRoutes = (newRoute) => {
|
||||
API.config.addRoute(newRoute).then(() => {
|
||||
refreshAll();
|
||||
});
|
||||
}
|
||||
|
||||
const addNewRoute = async () => {
|
||||
const apps = (await API.docker.list()).data;
|
||||
const app = apps.find((a) => a.Names[0] === Name);
|
||||
setOpenModal(app);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1000px', width: '100%' }}>
|
||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||
<ExposeModal
|
||||
openModal={openModal}
|
||||
setOpenModal={setOpenModal}
|
||||
container={containerInfo}
|
||||
config={config}
|
||||
updateRoutes={
|
||||
(_newRoute) => {
|
||||
updateRoutes(_newRoute);
|
||||
setOpenModal(false);
|
||||
setOpenRestartModal(true);
|
||||
}
|
||||
}
|
||||
/>
|
||||
<MainCard name={Name} title={<div>{Name}</div>}>
|
||||
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
||||
<Stack spacing={2} direction={'column'} justifyContent={'center'} alignItems={'center'}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<img className={isUpdating ? 'darken' : ''} src={getFaviconURL(routes && routes[0])} width="128px" />
|
||||
{isUpdating ? (
|
||||
<CircularProgress
|
||||
style={{ position: 'absolute', top: 'calc(50% - 22.5px)', left: 'calc(50% - 22.5px)' }}
|
||||
width="128px"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
{({
|
||||
"created": <Chip label="Created" color="warning" />,
|
||||
"restarting": <Chip label="Restarting" color="warning" />,
|
||||
"running": <Chip label="Running" color="success" />,
|
||||
"removing": <Chip label="Removing" color="error" />,
|
||||
"paused": <Chip label="Paused" color="info" />,
|
||||
"exited": <Chip label="Exited" color="error" />,
|
||||
"dead": <Chip label="Dead" color="error" />,
|
||||
})[State.Status]}
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={2} style={{ width: '100%' }} >
|
||||
<Stack spacing={2} direction={'row'} >
|
||||
<GetActions
|
||||
Id={containerInfo.Id}
|
||||
state={State.Status}
|
||||
refreshServeApps={() => {
|
||||
refreshAll()
|
||||
}}
|
||||
setIsUpdatingId={() => {
|
||||
setIsUpdating(true);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<strong><ContainerOutlined /> Image</strong>
|
||||
<div style={info}>{Image}</div>
|
||||
<strong><DesktopOutlined /> Name</strong>
|
||||
<div style={info}>{Name}</div>
|
||||
<strong><InfoCircleOutlined /> IP Address</strong>
|
||||
<div style={info}>{IPAddress}</div>
|
||||
<strong>
|
||||
<SafetyCertificateOutlined/> Health
|
||||
</strong>
|
||||
<div style={info}>{healthStatus}</div>
|
||||
<strong><SettingOutlined /> Settings {State.Status !== 'running' ? '(Start container to edit)' : ''}</strong>
|
||||
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
||||
<Checkbox
|
||||
checked={Config.Labels['cosmos-force-network-secured'] === 'true'}
|
||||
disabled={State.Status !== 'running' || isUpdating}
|
||||
onChange={(e) => {
|
||||
setIsUpdating(true);
|
||||
API.docker.secure(Name, e.target.checked).then(() => {
|
||||
setTimeout(() => {
|
||||
refreshAll();
|
||||
}, 3000);
|
||||
})
|
||||
}}
|
||||
/> Force Secure Network
|
||||
</Stack>
|
||||
<strong><NodeExpandOutlined /> URLs</strong>
|
||||
<div>
|
||||
{routes.map((route) => {
|
||||
return <HostChip route={route} settings style={{margin: '5px'}}/>
|
||||
})}
|
||||
<br />
|
||||
<Chip
|
||||
label="New"
|
||||
color="primary"
|
||||
style={{paddingRight: '4px', margin: '5px'}}
|
||||
deleteIcon={<PlusCircleOutlined />}
|
||||
onClick={() => {
|
||||
addNewRoute();
|
||||
}}
|
||||
onDelete={() => {
|
||||
addNewRoute();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContainerOverview;
|
98
client/src/pages/servapps/exposeModal.jsx
Normal file
98
client/src/pages/servapps/exposeModal.jsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack } from '@mui/material';
|
||||
import { Alert } from '@mui/material';
|
||||
import RouteManagement from '../config/routes/routeman';
|
||||
import { ValidateRoute, getFaviconURL, sanitizeRoute, getContainersRoutes } from '../../utils/routes';
|
||||
import * as API from '../../api';
|
||||
|
||||
const getHostnameFromName = (name) => {
|
||||
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
|
||||
}
|
||||
|
||||
const ExposeModal = ({ openModal, setOpenModal, config, updateRoutes, container }) => {
|
||||
const [submitErrors, setSubmitErrors] = useState([]);
|
||||
const [newRoute, setNewRoute] = useState(null);
|
||||
|
||||
let containerName = openModal && (openModal.Names[0]);
|
||||
|
||||
const hasCosmosNetwork = () => {
|
||||
return container && container.NetworkSettings.Networks && Object.keys(container.NetworkSettings.Networks).some((network) => {
|
||||
if(network.startsWith('cosmos-network'))
|
||||
return true;
|
||||
})
|
||||
}
|
||||
|
||||
return <Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>Expose ServApp</DialogTitle>
|
||||
{openModal && <>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
Welcome to the URL Wizard. This interface will help you expose your ServApp securely to the internet by creating a new URL.
|
||||
</div>
|
||||
<div>
|
||||
{openModal && !hasCosmosNetwork(containerName) && <Alert severity="warning">This ServApp does not appear to be connected to a Cosmos Network, so the hostname might not be accessible. The easiest way to fix this is to check the box "Force Secure Network" or manually create a sub-network in Docker.</Alert>}
|
||||
</div>
|
||||
<div>
|
||||
<RouteManagement TargetContainer={openModal}
|
||||
routeConfig={{
|
||||
Target: "http://"+containerName.replace('/', '') + ":",
|
||||
Mode: "SERVAPP",
|
||||
Name: containerName.replace('/', ''),
|
||||
Description: "Expose " + containerName.replace('/', '') + " to the internet",
|
||||
UseHost: true,
|
||||
Host: getHostnameFromName(containerName),
|
||||
UsePathPrefix: false,
|
||||
PathPrefix: '',
|
||||
CORSOrigin: '',
|
||||
StripPathPrefix: false,
|
||||
AuthEnabled: false,
|
||||
Timeout: 14400000,
|
||||
ThrottlePerMinute: 10000,
|
||||
BlockCommonBots: true,
|
||||
SmartShield: {
|
||||
Enabled: true,
|
||||
}
|
||||
}}
|
||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||
setRouteConfig={(_newRoute) => {
|
||||
setNewRoute(sanitizeRoute(_newRoute));
|
||||
}}
|
||||
up={() => {}}
|
||||
down={() => {}}
|
||||
deleteRoute={() => {}}
|
||||
noControls
|
||||
lockTarget
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{submitErrors && submitErrors.length > 0 && <Stack spacing={2} direction={"column"}>
|
||||
<Alert severity="error">{submitErrors.map((err) => {
|
||||
return <div>{err}</div>
|
||||
})}</Alert>
|
||||
</Stack>}
|
||||
<Button onClick={() => setOpenModal(false)}>Cancel</Button>
|
||||
<Button onClick={() => {
|
||||
let errors = ValidateRoute(newRoute, config);
|
||||
if (errors && errors.length > 0) {
|
||||
errors = errors.map((err) => {
|
||||
return `${err}`;
|
||||
});
|
||||
setSubmitErrors(errors);
|
||||
return true;
|
||||
} else {
|
||||
setSubmitErrors([]);
|
||||
updateRoutes(newRoute);
|
||||
}
|
||||
|
||||
}}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</>}
|
||||
</Dialog>
|
||||
};
|
||||
|
||||
export default ExposeModal;
|
41
client/src/pages/servapps/index.jsx
Normal file
41
client/src/pages/servapps/index.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
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 HostChip from '../../components/hostChip';
|
||||
import { RouteMode, RouteSecurity } from '../../components/routeComponents';
|
||||
import { getFaviconURL } from '../../utils/routes';
|
||||
import * as API from '../../api';
|
||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import IsLoggedIn from '../../isLoggedIn';
|
||||
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
|
||||
import ServeApps from './servapps';
|
||||
import VolumeManagementList from './volumes';
|
||||
import NetworkManagementList from './networks';
|
||||
|
||||
const ServappsIndex = () => {
|
||||
return <div>
|
||||
<IsLoggedIn />
|
||||
|
||||
<PrettyTabbedView path="/ui/servapps/:tab" tabs={[
|
||||
{
|
||||
title: 'Containers',
|
||||
children: <ServeApps />,
|
||||
path: 'containers'
|
||||
},
|
||||
{
|
||||
title: 'Volumes',
|
||||
children: <VolumeManagementList />,
|
||||
path: 'volumes'
|
||||
},
|
||||
{
|
||||
title: 'Networks',
|
||||
children: <NetworkManagementList />,
|
||||
path: 'networks'
|
||||
},
|
||||
]}/>
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default ServappsIndex;
|
118
client/src/pages/servapps/networks.jsx
Normal file
118
client/src/pages/servapps/networks.jsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
// material-ui
|
||||
import { CloseSquareOutlined, DeleteOutlined, PlusCircleOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { Button, Chip, CircularProgress, Stack, useTheme } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import * as API from '../../api';
|
||||
import PrettyTableView from '../../components/tableView/prettyTableView';
|
||||
|
||||
const NetworkManagementList = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [rows, setRows] = useState(null);
|
||||
const [tryDelete, setTryDelete] = useState(null);
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
|
||||
function refresh() {
|
||||
setIsLoading(true);
|
||||
API.docker.networkList()
|
||||
.then(data => {
|
||||
setRows(data.data);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction='row' spacing={1} style={{ marginBottom: '20px' }}>
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={refresh}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{isLoading && (<div style={{ height: '550px' }}>
|
||||
<center>
|
||||
<br />
|
||||
<CircularProgress />
|
||||
</center>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && rows && (
|
||||
<PrettyTableView
|
||||
data={rows}
|
||||
onRowClick={() => { }}
|
||||
getKey={(r) => r.Id}
|
||||
columns={[
|
||||
{
|
||||
title: 'Network Name',
|
||||
field: (r) => <Stack direction='column'>
|
||||
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
|
||||
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Driver} driver</div>
|
||||
</Stack>,
|
||||
search: (r) => r.Name,
|
||||
},
|
||||
{
|
||||
title: 'Properties',
|
||||
screenMin: 'md',
|
||||
field: (r) => (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Chip label={r.Scope} color="primary" />
|
||||
{r.Internal && <Chip label="Internal" color="secondary" />}
|
||||
{r.Attachable && <Chip label="Attachable" color="success" />}
|
||||
{r.Ingress && <Chip label="Ingress" color="warning" />}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'IPAM gateway / mask',
|
||||
screenMin: 'lg',
|
||||
field: (r) => r.IPAM.Config.map((config, index) => (
|
||||
<div key={index}>
|
||||
{config.Gateway} / {config.Subnet}
|
||||
</div>
|
||||
)),
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
screenMin: 'lg',
|
||||
field: (r) => new Date(r.Created).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
clickable: true,
|
||||
field: (r) => (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
if (tryDelete === r.Id) {
|
||||
setIsLoading(true);
|
||||
API.docker.networkDelete(r.Id).then(() => {
|
||||
refresh();
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
setTryDelete(r.Id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tryDelete === r.Id ? "Really?" : "Delete"}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NetworkManagementList;
|
|
@ -11,8 +11,11 @@ import * as API from '../../api';
|
|||
import IsLoggedIn from '../../isLoggedIn';
|
||||
import RestartModal from '../config/users/restart';
|
||||
import RouteManagement from '../config/routes/routeman';
|
||||
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes';
|
||||
import { ValidateRoute, getFaviconURL, sanitizeRoute, getContainersRoutes } from '../../utils/routes';
|
||||
import HostChip from '../../components/hostChip';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ExposeModal from './exposeModal';
|
||||
import GetActions from './actionBar';
|
||||
|
||||
const Item = styled(Paper)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
|
||||
|
@ -39,16 +42,6 @@ const ServeApps = () => {
|
|||
const [submitErrors, setSubmitErrors] = useState([]);
|
||||
const [openRestartModal, setOpenRestartModal] = useState(false);
|
||||
|
||||
const hasCosmosNetwork = (containerName) => {
|
||||
const container = serveApps.find((app) => {
|
||||
return app.Names[0].replace('/', '') === containerName.replace('/', '');
|
||||
});
|
||||
return container && container.NetworkSettings.Networks && Object.keys(container.NetworkSettings.Networks).some((network) => {
|
||||
if(network.startsWith('cosmos-network'))
|
||||
return true;
|
||||
})
|
||||
}
|
||||
|
||||
const refreshServeApps = () => {
|
||||
API.docker.list().then((res) => {
|
||||
setServeApps(res.data);
|
||||
|
@ -66,22 +59,11 @@ const ServeApps = () => {
|
|||
});
|
||||
}
|
||||
|
||||
const getContainersRoutes = (containerName) => {
|
||||
return (config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
|
||||
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
|
||||
return route.Mode == "SERVAPP" && reg.test(route.Target)
|
||||
// (
|
||||
// route.Target.startsWith(containerName) ||
|
||||
// route.Target.split('://')[1].startsWith(containerName)
|
||||
// )
|
||||
})) || [];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshServeApps();
|
||||
}, []);
|
||||
|
||||
function updateRoutes() {
|
||||
function updateRoutes(newRoute) {
|
||||
let con = {
|
||||
...config,
|
||||
HTTPConfig: {
|
||||
|
@ -112,12 +94,15 @@ const ServeApps = () => {
|
|||
},
|
||||
};
|
||||
|
||||
const getHostnameFromName = (name) => {
|
||||
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
|
||||
const selectable = {
|
||||
cursor: 'pointer',
|
||||
"&:hover": {
|
||||
textDecoration: 'underline',
|
||||
}
|
||||
}
|
||||
|
||||
const getFirstRouteFavIcon = (app) => {
|
||||
let routes = getContainersRoutes(app.Names[0].replace('/', ''));
|
||||
let routes = getContainersRoutes(config, app.Names[0].replace('/', ''));
|
||||
if(routes.length > 0) {
|
||||
let url = getFaviconURL(routes[0]);
|
||||
return url;
|
||||
|
@ -126,162 +111,21 @@ const ServeApps = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const getActions = (app) => {
|
||||
const doTo = (action) => {
|
||||
setIsUpdatingId(app.Id, true);
|
||||
API.docker.manageContainer(app.Id, action).then((res) => {
|
||||
refreshServeApps();
|
||||
});
|
||||
};
|
||||
|
||||
let actions = [
|
||||
{
|
||||
t: 'Update Available',
|
||||
if: ['update_available'],
|
||||
e: <IconButton className="shinyButton" color='primary' onClick={() => {doTo('update')}} size='large'>
|
||||
<UpCircleOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Start',
|
||||
if: ['exited'],
|
||||
e: <IconButton onClick={() => {doTo('start')}} size='large'>
|
||||
<PlaySquareOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Unpause',
|
||||
if: ['paused'],
|
||||
e: <IconButton onClick={() => {doTo('unpause')}} size='large'>
|
||||
<PlaySquareOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Pause',
|
||||
if: ['running'],
|
||||
e: <IconButton onClick={() => {doTo('pause')}} size='large'>
|
||||
<PauseCircleOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Stop',
|
||||
if: ['created', 'paused', 'restarting', 'running'],
|
||||
e: <IconButton onClick={() => {doTo('stop')}} size='large' variant="outlined">
|
||||
<StopOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Restart',
|
||||
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
||||
e: <IconButton onClick={() => doTo('restart')} size='large'>
|
||||
<ReloadOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Re-create',
|
||||
if: ['exited', 'running', 'paused', 'created', 'restarting'],
|
||||
e: <IconButton onClick={() => doTo('recreate')} color="error" size='large'>
|
||||
<RollbackOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Delete',
|
||||
if: ['exited'],
|
||||
e: <IconButton onClick={() => {doTo('remove')}} color="error" size='large'>
|
||||
<DeleteOutlined />
|
||||
</IconButton>
|
||||
},
|
||||
{
|
||||
t: 'Kill',
|
||||
if: ['running', 'paused', 'created', 'restarting'],
|
||||
e: <IconButton onClick={() => doTo('kill')} color="error" size='large'>
|
||||
<CloseSquareOutlined />
|
||||
</IconButton>
|
||||
}
|
||||
];
|
||||
|
||||
return actions.filter((action) => {
|
||||
let updateAvailable = false;
|
||||
return action.if.includes(app.State) ?? (updateAvailable && action.if.includes('update_available'));
|
||||
}).map((action) => {
|
||||
return <Tooltip title={action.t}>{action.e}</Tooltip>
|
||||
});
|
||||
}
|
||||
|
||||
return <div>
|
||||
<IsLoggedIn />
|
||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>Expose ServApp</DialogTitle>
|
||||
{openModal && <>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
Welcome to the URL Wizard. This interface will help you expose your ServApp securely to the internet by creating a new URL.
|
||||
</div>
|
||||
<div>
|
||||
{openModal && !hasCosmosNetwork(openModal.Names[0]) && <Alert severity="warning">This ServApp does not appear to be connected to a Cosmos Network, so the hostname might not be accessible. The easiest way to fix this is to check the box "Force Secure Network" or manually create a sub-network in Docker.</Alert>}
|
||||
</div>
|
||||
<div>
|
||||
<RouteManagement TargetContainer={openModal}
|
||||
routeConfig={{
|
||||
Target: "http://"+openModal.Names[0].replace('/', '') + ":",
|
||||
Mode: "SERVAPP",
|
||||
Name: openModal.Names[0].replace('/', ''),
|
||||
Description: "Expose " + openModal.Names[0].replace('/', '') + " to the internet",
|
||||
UseHost: true,
|
||||
Host: getHostnameFromName(openModal.Names[0]),
|
||||
UsePathPrefix: false,
|
||||
PathPrefix: '',
|
||||
CORSOrigin: '',
|
||||
StripPathPrefix: false,
|
||||
AuthEnabled: false,
|
||||
Timeout: 14400000,
|
||||
ThrottlePerMinute: 10000,
|
||||
BlockCommonBots: true,
|
||||
SmartShield: {
|
||||
Enabled: true,
|
||||
}
|
||||
}}
|
||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||
setRouteConfig={(_newRoute) => {
|
||||
setNewRoute(sanitizeRoute(_newRoute));
|
||||
}}
|
||||
up={() => {}}
|
||||
down={() => {}}
|
||||
deleteRoute={() => {}}
|
||||
noControls
|
||||
lockTarget
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{submitErrors && submitErrors.length > 0 && <Stack spacing={2} direction={"column"}>
|
||||
<Alert severity="error">{submitErrors.map((err) => {
|
||||
return <div>{err}</div>
|
||||
})}</Alert>
|
||||
</Stack>}
|
||||
<Button onClick={() => setOpenModal(false)}>Cancel</Button>
|
||||
<Button onClick={() => {
|
||||
let errors = ValidateRoute(newRoute, config);
|
||||
if (errors && errors.length > 0) {
|
||||
errors = errors.map((err) => {
|
||||
return `${err}`;
|
||||
});
|
||||
setSubmitErrors(errors);
|
||||
return true;
|
||||
} else {
|
||||
setSubmitErrors([]);
|
||||
updateRoutes();
|
||||
}
|
||||
|
||||
}}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</>}
|
||||
</Dialog>
|
||||
<ExposeModal
|
||||
openModal={openModal}
|
||||
setOpenModal={setOpenModal}
|
||||
container={serveApps.find((app) => {
|
||||
return app.Names[0].replace('/', '') === openModal && openModal.Names[0].replace('/', '');
|
||||
})}
|
||||
config={config}
|
||||
updateRoutes={
|
||||
(_newRoute) => {
|
||||
updateRoutes(_newRoute);
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
|
@ -306,7 +150,7 @@ const ServeApps = () => {
|
|||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 container spacing={2}>
|
||||
{serveApps && serveApps.filter(app => search.length < 2 || app.Names[0].toLowerCase().includes(search.toLowerCase())).map((app) => {
|
||||
return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4}>
|
||||
<Item>
|
||||
|
@ -342,8 +186,7 @@ const ServeApps = () => {
|
|||
{/* <Button variant="contained" size="small" onClick={() => {}}>
|
||||
Update
|
||||
</Button> */}
|
||||
|
||||
{getActions(app)}
|
||||
<GetActions Id={app.Id} state={app.State} setIsUpdatingId={setIsUpdatingId} refreshServeApps={refreshServeApps} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
|
@ -369,35 +212,39 @@ const ServeApps = () => {
|
|||
</Stack>
|
||||
</Stack>
|
||||
{isUpdating[app.Id] ? <div>
|
||||
<CircularProgress color="inherit" />
|
||||
</div> :
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Settings
|
||||
</Typography>
|
||||
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
||||
<Checkbox
|
||||
checked={app.Labels['cosmos-force-network-secured'] === 'true'}
|
||||
onChange={(e) => {
|
||||
setIsUpdatingId(app.Id, true);
|
||||
API.docker.secure(app.Id, e.target.checked).then(() => {
|
||||
setTimeout(() => {
|
||||
setIsUpdatingId(app.Id, false);
|
||||
refreshServeApps();
|
||||
}, 3000);
|
||||
})
|
||||
}}
|
||||
/> Force Secure Network
|
||||
</Stack></Stack>}
|
||||
<CircularProgress color="inherit" />
|
||||
</div>
|
||||
:
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Settings {app.State !== 'running' ? '(Start container to edit)' : ''}
|
||||
</Typography>
|
||||
<Stack style={{ fontSize: '80%' }} direction={"row"} alignItems="center">
|
||||
<Checkbox
|
||||
checked={app.Labels['cosmos-force-network-secured'] === 'true'}
|
||||
disabled={app.State !== 'running'}
|
||||
onChange={(e) => {
|
||||
setIsUpdatingId(app.Id, true);
|
||||
API.docker.secure(app.Id, e.target.checked).then(() => {
|
||||
setTimeout(() => {
|
||||
setIsUpdatingId(app.Id, false);
|
||||
refreshServeApps();
|
||||
}, 3000);
|
||||
})
|
||||
}}
|
||||
/> Force Secure Network
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
URLs
|
||||
</Typography>
|
||||
<Stack style={noOver} spacing={2} direction="row">
|
||||
{getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
|
||||
{getContainersRoutes(config, app.Names[0].replace('/', '')).map((route) => {
|
||||
return <HostChip route={route} settings/>
|
||||
})}
|
||||
{/* {getContainersRoutes(app.Names[0].replace('/', '')).length == 0 && */}
|
||||
{/* {getContainersRoutes(config, app.Names[0].replace('/', '')).length == 0 && */}
|
||||
<Chip
|
||||
label="New"
|
||||
color="primary"
|
||||
|
@ -413,6 +260,11 @@ const ServeApps = () => {
|
|||
{/* } */}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<div>
|
||||
<Link to={`/ui/servapps/containers/${app.Names[0].replace('/', '')}`}>
|
||||
<Button variant="outlined" color="primary" fullWidth>Details</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{/* <Stack>
|
||||
<Button variant="contained" color="primary" onClick={() => {
|
||||
setOpenModal(app);
|
||||
|
|
116
client/src/pages/servapps/volumes.jsx
Normal file
116
client/src/pages/servapps/volumes.jsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
// material-ui
|
||||
import { AppstoreAddOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, PlusCircleOutlined, ReloadOutlined, RollbackOutlined, SearchOutlined, SettingOutlined, StopOutlined, SyncOutlined, UpCircleOutlined, UpSquareFilled } from '@ant-design/icons';
|
||||
import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, IconButton, Input, InputAdornment, TextField, Tooltip, Typography, useTheme } from '@mui/material';
|
||||
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
|
||||
import { Stack } from '@mui/system';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import * as API from '../../api';
|
||||
import IsLoggedIn from '../../isLoggedIn';
|
||||
import RestartModal from '../config/users/restart';
|
||||
import RouteManagement from '../config/routes/routeman';
|
||||
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes';
|
||||
import HostChip from '../../components/hostChip';
|
||||
import PrettyTableView from '../../components/tableView/prettyTableView';
|
||||
|
||||
const VolumeManagementList = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [rows, setRows] = useState(null);
|
||||
const [tryDelete, setTryDelete] = useState(null);
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
|
||||
function refresh() {
|
||||
setIsLoading(true);
|
||||
API.docker.volumeList()
|
||||
.then(data => {
|
||||
setRows(data.data.Volumes);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction='row' spacing={1} style={{ marginBottom: '20px' }}>
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={refresh}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{isLoading && (<div style={{height: '550px'}}>
|
||||
<center>
|
||||
<br />
|
||||
<CircularProgress />
|
||||
</center>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && rows && (
|
||||
<PrettyTableView
|
||||
data={rows}
|
||||
onRowClick={() => {}}
|
||||
getKey={(r) => r.Name}
|
||||
columns={[
|
||||
{
|
||||
title: 'Volume Name',
|
||||
field: (r) => <Stack direction='column'>
|
||||
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
|
||||
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Mountpoint}</div>
|
||||
</Stack>,
|
||||
search: (r) => r.Name,
|
||||
},
|
||||
{
|
||||
title: 'Driver',
|
||||
screenMin: 'lg',
|
||||
field: (r) => r.Driver,
|
||||
},
|
||||
{
|
||||
title: 'Scope',
|
||||
screenMin: 'lg',
|
||||
field: (r) => r.Scope,
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
screenMin: 'lg',
|
||||
field: (r) => new Date(r.CreatedAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
clickable: true,
|
||||
field: (r) => (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
if(tryDelete === r.Name) {
|
||||
setIsLoading(true);
|
||||
API.docker.volumeDelete(r.Name).then(() => {
|
||||
refresh();
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
setTryDelete(r.Name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tryDelete === r.Name ? "Really?" : "Delete"}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VolumeManagementList;
|
|
@ -6,11 +6,12 @@ import MainLayout from '../layout/MainLayout';
|
|||
import UserManagement from '../pages/config/users/usermanagement';
|
||||
import ConfigManagement from '../pages/config/users/configman';
|
||||
import ProxyManagement from '../pages/config/users/proxyman';
|
||||
import ServeApps from '../pages/servapps/servapps';
|
||||
import ServeAppsIndex from '../pages/servapps/';
|
||||
import { Navigate } from 'react-router';
|
||||
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';
|
||||
|
||||
|
||||
// render - dashboard
|
||||
|
@ -52,7 +53,7 @@ const MainRoutes = {
|
|||
},
|
||||
{
|
||||
path: '/ui/servapps',
|
||||
element: <ServeApps />
|
||||
element: <ServeAppsIndex />
|
||||
},
|
||||
{
|
||||
path: '/ui/config-users',
|
||||
|
@ -70,6 +71,10 @@ const MainRoutes = {
|
|||
path: '/ui/config-url/:routeName',
|
||||
element: <RouteConfigPage />,
|
||||
},
|
||||
{
|
||||
path: '/ui/servapps/containers/:containerName',
|
||||
element: <ContainerIndex />,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
@ -52,6 +52,10 @@ export const getFaviconURL = (route) => {
|
|||
return demoicons[route.Name] || logogray;
|
||||
}
|
||||
|
||||
if(!route) {
|
||||
return logogray;
|
||||
}
|
||||
|
||||
const addRemote = (url) => {
|
||||
return '/cosmos/api/favicon?q=' + encodeURIComponent(url)
|
||||
}
|
||||
|
@ -102,4 +106,11 @@ export const ValidateRoute = (routeConfig, config) => {
|
|||
return ['Route Name already exists. Name must be unique.'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export const getContainersRoutes = (config, containerName) => {
|
||||
return (config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
|
||||
let reg = new RegExp(`^(([a-z]+):\/\/)?${containerName}(:?[0-9]+)?$`, 'i');
|
||||
return route.Mode == "SERVAPP" && reg.test(route.Target)
|
||||
})) || [];
|
||||
}
|
73
package-lock.json
generated
73
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.3.0-unstable",
|
||||
"version": "0.4.0-unstable2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cosmos-server",
|
||||
"version": "0.3.0-unstable",
|
||||
"version": "0.4.0-unstable2",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
|
@ -41,6 +41,7 @@
|
|||
"react-router": "^6.4.1",
|
||||
"react-router-dom": "^6.4.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-terminal": "^1.3.1",
|
||||
"react-window": "^1.8.7",
|
||||
"redux": "^4.2.0",
|
||||
"simplebar": "^5.3.8",
|
||||
|
@ -8116,6 +8117,50 @@
|
|||
"react": ">= 0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-terminal": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-terminal/-/react-terminal-1.3.1.tgz",
|
||||
"integrity": "sha512-lbkrih1be0nlJptZR7uwV6YF8PMuxKJOKhGN+GVuFKp9dY/qpSYF76KMGZZJnWbbxAt5Bkf+aUt4iyy5F8NBdQ==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2",
|
||||
"react-device-detect": "2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-terminal/node_modules/react-device-detect": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.1.2.tgz",
|
||||
"integrity": "sha512-N42xttwez3ECgu4KpOL2ICesdfoz8NCBfmc1rH9FRYSjH7NmMyANPSrQ3EvAtJyj/6TzJNhrANSO38iXjCB2Ug==",
|
||||
"dependencies": {
|
||||
"ua-parser-js": "^0.7.30"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 0.14.0 < 18.0.0",
|
||||
"react-dom": ">= 0.14.0 < 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-terminal/node_modules/ua-parser-js": {
|
||||
"version": "0.7.35",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
|
||||
"integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ua-parser-js"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
|
@ -15065,6 +15110,30 @@
|
|||
"refractor": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"react-terminal": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-terminal/-/react-terminal-1.3.1.tgz",
|
||||
"integrity": "sha512-lbkrih1be0nlJptZR7uwV6YF8PMuxKJOKhGN+GVuFKp9dY/qpSYF76KMGZZJnWbbxAt5Bkf+aUt4iyy5F8NBdQ==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2",
|
||||
"react-device-detect": "2.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-device-detect": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.1.2.tgz",
|
||||
"integrity": "sha512-N42xttwez3ECgu4KpOL2ICesdfoz8NCBfmc1rH9FRYSjH7NmMyANPSrQ3EvAtJyj/6TzJNhrANSO38iXjCB2Ug==",
|
||||
"requires": {
|
||||
"ua-parser-js": "^0.7.30"
|
||||
}
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.35",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz",
|
||||
"integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.4.0-unstable2",
|
||||
"version": "0.4.0-unstable3",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
@ -41,6 +41,7 @@
|
|||
"react-router": "^6.4.1",
|
||||
"react-router-dom": "^6.4.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-terminal": "^1.3.1",
|
||||
"react-window": "^1.8.7",
|
||||
"redux": "^4.2.0",
|
||||
"simplebar": "^5.3.8",
|
||||
|
@ -53,11 +54,11 @@
|
|||
"scripts": {
|
||||
"client": "vite",
|
||||
"client-build": "vite build --base=/ui/",
|
||||
"start": "env CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
|
||||
"start": "env COSMOS_HOSTNAME=localhost CONFIG_FILE=./config_dev.json EZ=UTC build/cosmos",
|
||||
"build": " sh build.sh",
|
||||
"dev": "npm run build && npm run start",
|
||||
"dockerdevbuild": "sh build.sh && npm run client-build && docker build --tag cosmos-dev .",
|
||||
"dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_HOSTNAME=localhost -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
|
||||
"dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev",
|
||||
"dockerdev": "npm run dockerdevbuild && npm run dockerdevrun",
|
||||
"demo": "vite build --base=/ui/ --mode demo",
|
||||
"devdemo": "vite --mode demo"
|
||||
|
|
46
src/docker/api_getcontainers.go
Normal file
46
src/docker/api_getcontainers.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
func GetContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
containerId := vars["containerId"]
|
||||
|
||||
|
||||
if req.Method == "GET" {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("GetContainerRoute", errD)
|
||||
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||
return
|
||||
}
|
||||
|
||||
// get Docker container
|
||||
container, err := DockerClient.ContainerInspect(context.Background(), containerId)
|
||||
if err != nil {
|
||||
utils.Error("GetContainerRoute: Error while getting container", err)
|
||||
utils.HTTPError(w, "Container Get Error: " + err.Error(), http.StatusInternalServerError, "LN002")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": container,
|
||||
})
|
||||
} else {
|
||||
utils.Error("GetContainerRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
140
src/docker/api_getlogs.go
Normal file
140
src/docker/api_getlogs.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
type LogOutput struct {
|
||||
StreamType byte `json:"streamType"`
|
||||
Size uint32 `json:"size"`
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
// parseDockerLogHeader parses the first 8 bytes of a Docker log message
|
||||
// and returns the stream type, size, and the rest of the message as output.
|
||||
// It also checks if the message contains a log header and extracts the log message from it.
|
||||
func parseDockerLogHeader(data []byte) (LogOutput) {
|
||||
var logOutput LogOutput
|
||||
logOutput.StreamType = 1 // assume stdout if header not present
|
||||
logOutput.Size = uint32(len(data))
|
||||
logOutput.Output = string(data)
|
||||
|
||||
if len(data) < 8 {
|
||||
return logOutput
|
||||
}
|
||||
|
||||
// check if the output contains a log header
|
||||
hasHeader := true
|
||||
streamType := data[0]
|
||||
if(!(streamType >= 0 && streamType <= 2)) {
|
||||
hasHeader = false
|
||||
}
|
||||
if(data[1] != 0 || data[2] != 0 || data[3] != 0) {
|
||||
hasHeader = false
|
||||
}
|
||||
if hasHeader {
|
||||
sizeBytes := data[4:8]
|
||||
size := binary.BigEndian.Uint32(sizeBytes)
|
||||
|
||||
output := string(data[8:])
|
||||
|
||||
logOutput.StreamType = streamType
|
||||
logOutput.Size = size
|
||||
logOutput.Output = output
|
||||
}
|
||||
|
||||
return logOutput
|
||||
}
|
||||
|
||||
func FilterLogs(logReader io.Reader, searchQuery string, limit int) []LogOutput {
|
||||
scanner := bufio.NewScanner(logReader)
|
||||
logLines := make([]LogOutput, 0)
|
||||
|
||||
// Read all logs into a slice
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if len(searchQuery) > 0 && !strings.Contains(strings.ToUpper(line), strings.ToUpper(searchQuery)) {
|
||||
continue
|
||||
}
|
||||
|
||||
logLines = append(logLines, parseDockerLogHeader(([]byte)(line)))
|
||||
}
|
||||
|
||||
from := utils.Max(len(logLines)-limit, 0)
|
||||
logLines = logLines[from:]
|
||||
|
||||
return logLines
|
||||
}
|
||||
|
||||
func GetContainerLogsRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
containerId := vars["containerId"]
|
||||
|
||||
if req.Method == "GET" {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("GetContainerLogsRoute", errD)
|
||||
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||
return
|
||||
}
|
||||
|
||||
query := req.URL.Query()
|
||||
limit := 100
|
||||
lastReceivedLogs := ""
|
||||
|
||||
if query.Get("limit") != "" {
|
||||
limit, _ = strconv.Atoi(query.Get("limit"))
|
||||
}
|
||||
|
||||
if query.Get("lastReceivedLogs") != "" {
|
||||
lastReceivedLogs = query.Get("lastReceivedLogs")
|
||||
}
|
||||
|
||||
errorOnly := false
|
||||
if query.Get("errorOnly") != "" {
|
||||
errorOnly, _ = strconv.ParseBool(query.Get("errorOnly"))
|
||||
}
|
||||
|
||||
options := types.ContainerLogsOptions{
|
||||
ShowStdout: !errorOnly,
|
||||
ShowStderr: true,
|
||||
Timestamps: true,
|
||||
Until: lastReceivedLogs,
|
||||
}
|
||||
|
||||
logReader, err := DockerClient.ContainerLogs(context.Background(), containerId, options)
|
||||
if err != nil {
|
||||
utils.Error("GetContainerLogsRoute: Error while getting container logs", err)
|
||||
utils.HTTPError(w, "Container Logs Error: "+err.Error(), http.StatusInternalServerError, "LN002")
|
||||
return
|
||||
}
|
||||
defer logReader.Close()
|
||||
|
||||
lines := FilterLogs(logReader, query.Get("search"), limit)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": lines,
|
||||
})
|
||||
} else {
|
||||
utils.Error("GetContainerLogsRoute: Method not allowed "+req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
// "github.com/docker/docker/client"
|
||||
contstuff "github.com/docker/docker/api/types/container"
|
||||
doctype "github.com/docker/docker/api/types"
|
||||
)
|
||||
|
|
80
src/docker/api_networks.go
Normal file
80
src/docker/api_networks.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
func ListNetworksRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "GET" {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("ListNetworksRoute", 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
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": networks,
|
||||
})
|
||||
} else {
|
||||
utils.Error("ListNetworksRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func DeleteNetworkRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "DELETE" {
|
||||
// Get the network ID from URL
|
||||
vars := mux.Vars(req)
|
||||
networkID := vars["networkID"]
|
||||
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("DeleteNetworkRoute", errD)
|
||||
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "DN001")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the specified Docker network
|
||||
err := DockerClient.NetworkRemove(context.Background(), networkID)
|
||||
if err != nil {
|
||||
utils.Error("DeleteNetworkRoute: Error while deleting network", err)
|
||||
utils.HTTPError(w, "Network Deletion Error: " + err.Error(), http.StatusInternalServerError, "DN002")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": "Network deleted successfully",
|
||||
})
|
||||
} else {
|
||||
utils.Error("DeleteNetworkRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
|
|||
_, errEdit := EditContainer(container.ID, container)
|
||||
if errEdit != nil {
|
||||
utils.Error("ContainerSecureEdit", errEdit)
|
||||
utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS003")
|
||||
utils.HTTPError(w, "Internal server error: " + errEdit.Error(), http.StatusInternalServerError, "DS003")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
79
src/docker/api_volumes.go
Normal file
79
src/docker/api_volumes.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
filters "github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
func ListVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "GET" {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("ManageContainer", errD)
|
||||
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "LV001")
|
||||
return
|
||||
}
|
||||
|
||||
// List Docker volumes
|
||||
volumes, err := DockerClient.VolumeList(context.Background(), filters.Args{})
|
||||
if err != nil {
|
||||
utils.Error("ListVolumeRoute: Error while getting volumes", err)
|
||||
utils.HTTPError(w, "Volumes Get Error", http.StatusInternalServerError, "LV002")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": volumes,
|
||||
})
|
||||
} else {
|
||||
utils.Error("ListVolumeRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteVolumeRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method == "DELETE" {
|
||||
// Get the volume name from URL
|
||||
vars := mux.Vars(req)
|
||||
volumeName := vars["volumeName"]
|
||||
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("DeleteVolumeRoute", errD)
|
||||
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "DV001")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the specified Docker volume
|
||||
err := DockerClient.VolumeRemove(context.Background(), volumeName, true)
|
||||
if err != nil {
|
||||
utils.Error("DeleteVolumeRoute: Error while deleting volume", err)
|
||||
utils.HTTPError(w, "Volume Deletion Error", http.StatusInternalServerError, "DV002")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": "Volume deleted successfully",
|
||||
})
|
||||
} else {
|
||||
utils.Error("DeleteVolumeRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ var serverPortHTTP = ""
|
|||
var serverPortHTTPS = ""
|
||||
|
||||
func startHTTPServer(router *mux.Router) {
|
||||
utils.Log("Listening to HTTP on :" + serverPortHTTP)
|
||||
utils.Log("Listening to HTTP on : 0.0.0.0:" + serverPortHTTP)
|
||||
|
||||
err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, router)
|
||||
|
||||
|
@ -35,15 +35,14 @@ func startHTTPServer(router *mux.Router) {
|
|||
|
||||
func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||
config := utils.GetMainConfig()
|
||||
serverHostname := "0.0.0.0"
|
||||
|
||||
cfg := simplecert.Default
|
||||
|
||||
cfg.Domains = utils.GetAllHostnames()
|
||||
cfg.CacheDir = "/config/certificates"
|
||||
cfg.SSLEmail = config.HTTPConfig.SSLEmail
|
||||
cfg.HTTPAddress = serverHostname+":"+serverPortHTTP
|
||||
cfg.TLSAddress = serverHostname+":"+serverPortHTTPS
|
||||
cfg.HTTPAddress = "0.0.0.0:"+serverPortHTTP
|
||||
cfg.TLSAddress = "0.0.0.0:"+serverPortHTTPS
|
||||
|
||||
if config.HTTPConfig.DNSChallengeProvider != "" {
|
||||
cfg.DNSProvider = config.HTTPConfig.DNSChallengeProvider
|
||||
|
@ -59,7 +58,7 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
|||
certReloader, errSimCert = simplecert.Init(cfg, nil)
|
||||
if errSimCert != nil {
|
||||
// Temporary before we have a better way to handle this
|
||||
utils.Error("simplecert init failed, HTTPS wont renew", errSimCert)
|
||||
utils.Error("Failed to Init Let's Encrypt. HTTPS wont renew", errSimCert)
|
||||
startHTTPServer(router)
|
||||
return
|
||||
}
|
||||
|
@ -106,7 +105,7 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
|||
|
||||
server := http.Server{
|
||||
TLSConfig: tlsConf,
|
||||
Addr: serverHostname + ":" + serverPortHTTPS,
|
||||
Addr: "0.0.0.0:" + serverPortHTTPS,
|
||||
ReadTimeout: 0,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
WriteTimeout: 0,
|
||||
|
@ -150,7 +149,6 @@ func StartServer() {
|
|||
HTTPConfig := config.HTTPConfig
|
||||
serverPortHTTP = HTTPConfig.HTTPPort
|
||||
serverPortHTTPS = HTTPConfig.HTTPSPort
|
||||
// serverHostname := HTTPConfig.Hostname
|
||||
|
||||
var tlsCert = HTTPConfig.TLSCert
|
||||
var tlsKey= HTTPConfig.TLSKey
|
||||
|
@ -219,14 +217,22 @@ func StartServer() {
|
|||
|
||||
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
||||
srapi.HandleFunc("/api/users", user.UsersRoute)
|
||||
|
||||
srapi.HandleFunc("/api/volume/{volumeName}", docker.DeleteVolumeRoute)
|
||||
srapi.HandleFunc("/api/volumes", docker.ListVolumeRoute)
|
||||
|
||||
srapi.HandleFunc("/api/network/{networkID}", docker.DeleteNetworkRoute)
|
||||
srapi.HandleFunc("/api/networks", docker.ListNetworksRoute)
|
||||
|
||||
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}/", docker.GetContainerRoute)
|
||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||
|
||||
// if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
// srapi.Use(utils.EnsureHostname(serverHostname))
|
||||
// }
|
||||
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
srapi.Use(utils.EnsureHostname)
|
||||
}
|
||||
|
||||
srapi.Use(tokenMiddleware)
|
||||
srapi.Use(proxy.SmartShieldMiddleware(
|
||||
|
@ -236,10 +242,10 @@ func StartServer() {
|
|||
PerUserRequestLimit: 5000,
|
||||
},
|
||||
))
|
||||
srapi.Use(utils.MiddlewareTimeout(20 * time.Second))
|
||||
srapi.Use(utils.MiddlewareTimeout(30 * time.Second))
|
||||
srapi.Use(utils.BlockPostWithoutReferer)
|
||||
srapi.Use(proxy.BotDetectionMiddleware)
|
||||
srapi.Use(httprate.Limit(60, 1*time.Minute,
|
||||
srapi.Use(httprate.Limit(120, 1*time.Minute,
|
||||
httprate.WithKeyFuncs(httprate.KeyByIP),
|
||||
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||
utils.Error("Too many requests. Throttling", nil)
|
||||
|
@ -258,9 +264,9 @@ func StartServer() {
|
|||
|
||||
fs := spa.SpaHandler(pwd + "/static", "index.html")
|
||||
|
||||
// if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
// fs = utils.EnsureHostname(serverHostname)(fs)
|
||||
// }
|
||||
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
fs = utils.EnsureHostname(fs)
|
||||
}
|
||||
|
||||
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs))
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
|
|||
"HTTPSCertificateMode": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode,
|
||||
"needsRestart": utils.NeedsRestart,
|
||||
"newVersionAvailable": utils.NewVersionAvailable,
|
||||
"hostname": utils.GetMainConfig().HTTPConfig.Hostname,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
|
|
@ -166,3 +166,37 @@ func BlockPostWithoutReferer(next http.Handler) http.Handler {
|
|||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func EnsureHostname(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Debug("Request requested resource from : " + r.Host)
|
||||
|
||||
og := GetMainConfig().HTTPConfig.Hostname
|
||||
ni := GetMainConfig().NewInstall
|
||||
|
||||
if ni || og == "0.0.0.0" {
|
||||
next.ServeHTTP(w, r)
|
||||
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()
|
||||
|
||||
for _, hostname := range hostnames {
|
||||
if r.Host != hostname + port {
|
||||
Error("Invalid Hostname " + r.Host + "for request. Expecting " + hostname, nil)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
http.Error(w, "Bad Request: Invalid hostname.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
|
@ -68,7 +68,7 @@ var DefaultConfig = Config{
|
|||
GenerateMissingAuthCert: true,
|
||||
HTTPPort: "80",
|
||||
HTTPSPort: "443",
|
||||
Hostname: "localhost",
|
||||
Hostname: "0.0.0.0",
|
||||
ProxyConfig: ProxyConfig{
|
||||
Routes: []ProxyRouteConfig{},
|
||||
},
|
||||
|
@ -230,26 +230,6 @@ func GetConfigFileName() string {
|
|||
return configFile
|
||||
}
|
||||
|
||||
func EnsureHostname(hostname string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Debug("Request requested resource from : " + r.Host)
|
||||
port := ""
|
||||
if (IsHTTPS && MainConfig.HTTPConfig.HTTPSPort != "443") {
|
||||
port = ":" + MainConfig.HTTPConfig.HTTPSPort
|
||||
} else if (!IsHTTPS && MainConfig.HTTPConfig.HTTPPort != "80") {
|
||||
port = ":" + MainConfig.HTTPConfig.HTTPPort
|
||||
}
|
||||
if r.Host != hostname + port {
|
||||
Error("Invalid Hostname " + r.Host + "for request. Expecting " + hostname, nil)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprint(w, "Bad Request.")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func CreateDefaultConfigFileIfNecessary() bool {
|
||||
configFile := GetConfigFileName()
|
||||
|
@ -420,3 +400,10 @@ func ImageToBase64(path string) (string, error) {
|
|||
dataURI := fmt.Sprintf("data:%s;base64,%s", mimeType, encodedData)
|
||||
return dataURI, nil
|
||||
}
|
||||
|
||||
func Max(x, y int) int {
|
||||
if x < y {
|
||||
return y
|
||||
}
|
||||
return x
|
||||
}
|
Loading…
Reference in a new issue