v0.1.0 servapps management
This commit is contained in:
parent
822d4bc057
commit
cc2c749250
25 changed files with 491 additions and 83 deletions
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
|||
16
|
||||
16
|
||||
|
|
|
@ -3,4 +3,5 @@ env GOARCH=arm64 go build -o build/cosmos src/*.go
|
|||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
cp -r static build/
|
||||
cp -r static build/
|
||||
cp package.json build/
|
7
build.sh
7
build.sh
|
@ -3,4 +3,9 @@ go build -o build/cosmos src/*.go
|
|||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
cp -r static build/
|
||||
cp -r static build/
|
||||
echo '{' > build/meta.json
|
||||
cat package.json | grep -E '"version"' >> build/meta.json
|
||||
echo ' "buildDate": "'`date`'",' >> build/meta.json
|
||||
echo ' "built from": "'`hostname`'"' >> build/meta.json
|
||||
echo '}' >> build/meta.json
|
|
@ -8,6 +8,15 @@ function list() {
|
|||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function secure(id, res) {
|
||||
return wrap(fetch('/cosmos/api/servapps/' + id + '/secure/'+res, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const newDB = () => {
|
||||
return wrap(fetch('/cosmos/api/newDB', {
|
||||
|
@ -21,4 +30,5 @@ const newDB = () => {
|
|||
export {
|
||||
list,
|
||||
newDB,
|
||||
secure
|
||||
};
|
|
@ -19,7 +19,7 @@ const AuthWrapper = ({ children }) => {
|
|||
const darkMode = theme.palette.mode === 'dark';
|
||||
|
||||
return <Box sx={{ minHeight: '100vh',
|
||||
background: darkMode ? 'none' : '#f0efef' }}>
|
||||
background: darkMode ? 'none' : '#fafafb' }}>
|
||||
<AuthBackground />
|
||||
<Grid
|
||||
container
|
||||
|
|
|
@ -31,7 +31,7 @@ import CircularProgress from '@mui/material/CircularProgress';
|
|||
|
||||
import * as API from '../../../api';
|
||||
|
||||
export function CosmosContainerPicker({formik}) {
|
||||
export function CosmosContainerPicker({formik, lockTarget, TargetContainer}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [containers, setContainers] = React.useState([]);
|
||||
const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
|
||||
|
@ -43,11 +43,13 @@ export function CosmosContainerPicker({formik}) {
|
|||
const name = "Target"
|
||||
const label = "Container Name"
|
||||
let targetResult = {
|
||||
container: "container",
|
||||
container: 'null',
|
||||
port: "",
|
||||
protocol: "http",
|
||||
}
|
||||
|
||||
let preview = formik.values[name];
|
||||
|
||||
if(preview && preview.includes("://") && preview.includes(":")) {
|
||||
let p1_ = preview.split("://")[1]
|
||||
targetResult = {
|
||||
|
@ -83,6 +85,12 @@ export function CosmosContainerPicker({formik}) {
|
|||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if(lockTarget) {
|
||||
onContainerChange(TargetContainer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
|
@ -93,6 +101,7 @@ export function CosmosContainerPicker({formik}) {
|
|||
(async () => {
|
||||
const res = await API.docker.list()
|
||||
setContainers(res.data);
|
||||
|
||||
|
||||
let names = res.data.map((container) => container.Names[0])
|
||||
|
||||
|
@ -118,27 +127,27 @@ export function CosmosContainerPicker({formik}) {
|
|||
<Autocomplete
|
||||
id={name + "-autocomplete"}
|
||||
open={open}
|
||||
disabled={lockTarget}
|
||||
onOpen={() => {
|
||||
setOpen(true);
|
||||
!lockTarget && setOpen(true);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
!lockTarget && setOpen(false);
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
onContainerChange(newValue)
|
||||
!lockTarget && onContainerChange(newValue)
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
console.log(option.Names[0], value.Names[0])
|
||||
return option.Names[0] === value.Names[0]
|
||||
return !lockTarget && (option.Names[0] === value.Names[0])
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
return option.Names[0]
|
||||
return !lockTarget ? option.Names[0] : TargetContainer.Names[0]
|
||||
}}
|
||||
options={containers}
|
||||
loading={loading}
|
||||
freeSolo={true}
|
||||
placeholder={"Please select a container"}
|
||||
defaultValue={targetResult.containerObject}
|
||||
defaultValue={lockTarget ? TargetContainer : targetResult.containerObject}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
|
|
|
@ -27,9 +27,9 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
|
|||
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
|
||||
export const CosmosInputText = ({ name, type, placeholder, onChange, label, formik }) => {
|
||||
export const CosmosInputText = ({ name, style, type, placeholder, onChange, label, formik }) => {
|
||||
return <Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<Stack spacing={1} style={style}>
|
||||
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||
<OutlinedInput
|
||||
id={name}
|
||||
|
@ -128,7 +128,7 @@ export const CosmosInputPassword = ({ name, type, placeholder, onChange, label,
|
|||
</Grid>
|
||||
}
|
||||
|
||||
export const CosmosSelect = ({ name, label, formik, options }) => {
|
||||
export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
|
||||
return <Grid item xs={12}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||
|
@ -137,6 +137,7 @@ export const CosmosSelect = ({ name, label, formik, options }) => {
|
|||
variant="outlined"
|
||||
name={name}
|
||||
id={name}
|
||||
disabled={disabled}
|
||||
select
|
||||
value={formik.values[name]}
|
||||
onChange={formik.handleChange}
|
||||
|
@ -158,7 +159,7 @@ export const CosmosSelect = ({ name, label, formik, options }) => {
|
|||
</Grid>;
|
||||
}
|
||||
|
||||
export const CosmosCheckbox = ({ name, label, formik }) => {
|
||||
export const CosmosCheckbox = ({ name, label, formik, style }) => {
|
||||
return <Grid item xs={12}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
|
||||
<Field
|
||||
|
@ -167,6 +168,7 @@ export const CosmosCheckbox = ({ name, label, formik }) => {
|
|||
as={FormControlLabel}
|
||||
control={<Checkbox size="large" />}
|
||||
label={label}
|
||||
style={style}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
@ -182,7 +184,8 @@ export const CosmosCollapse = ({ children, title }) => {
|
|||
aria-controls="panel1a-content"
|
||||
id="panel1a-header"
|
||||
>
|
||||
<Typography>{title}</Typography>
|
||||
<Typography variant="h6">
|
||||
{title}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{children}
|
||||
|
|
|
@ -95,6 +95,7 @@ const ProxyManagement = () => {
|
|||
routes.push({
|
||||
Name: 'New Route',
|
||||
Description: 'New Route',
|
||||
Mode: "SERVAPP",
|
||||
UseHost: false,
|
||||
Host: '',
|
||||
UsePathPrefix: false,
|
||||
|
@ -103,8 +104,7 @@ const ProxyManagement = () => {
|
|||
ThrottlePerMinute: 100,
|
||||
CORSOrigin: '',
|
||||
StripPathPrefix: false,
|
||||
Static: false,
|
||||
SPAMode: false,
|
||||
AuthEnabled: false,
|
||||
});
|
||||
updateRoutes(routes);
|
||||
}}>Create</Button>
|
||||
|
@ -114,7 +114,8 @@ const ProxyManagement = () => {
|
|||
{config && <>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
{routes && routes.map((route,key) => (<>
|
||||
<RouteManagement routeConfig={route} setRouteConfig={(newRoute) => {
|
||||
<RouteManagement routeConfig={route}
|
||||
setRouteConfig={(newRoute) => {
|
||||
routes[key] = newRoute;
|
||||
}}
|
||||
up={() => up(key)}
|
||||
|
|
|
@ -33,7 +33,7 @@ import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, Cos
|
|||
import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { CosmosContainerPicker } from './containerPicker';
|
||||
|
||||
const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute }) => {
|
||||
const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockTarget=false, setRouteConfig, up, down, deleteRoute }) => {
|
||||
const [confirmDelete, setConfirmDelete] = React.useState(false);
|
||||
|
||||
return <div style={{ maxWidth: '1000px', margin: '' }}>
|
||||
|
@ -67,6 +67,7 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
|
|||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<MainCard title={
|
||||
noControls ? 'New Route' :
|
||||
<div>{routeConfig.Name}
|
||||
<Chip label={<UpOutlined />} onClick={() => up()}/>
|
||||
<Chip label={<DownOutlined />} onClick={() => down()}/>
|
||||
|
@ -93,11 +94,15 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
|
|||
<CosmosCollapse title="Settings">
|
||||
<Grid container spacing={2}>
|
||||
<CosmosFormDivider title={'Target Type'}/>
|
||||
<Grid item xs={12}>
|
||||
<Alert color='info'>What are you trying to access with this route?</Alert>
|
||||
</Grid>
|
||||
|
||||
<CosmosSelect
|
||||
name="Mode"
|
||||
label="Mode"
|
||||
formik={formik}
|
||||
disabled={lockTarget}
|
||||
options={[
|
||||
["SERVAPP", "ServApp - Docker Container"],
|
||||
["PROXY", "Proxy"],
|
||||
|
@ -109,20 +114,24 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
|
|||
<CosmosFormDivider title={'Target Settings'}/>
|
||||
|
||||
{
|
||||
formik.values.Mode === "SERVAPP" ?
|
||||
(formik.values.Mode === "SERVAPP")?
|
||||
<CosmosContainerPicker
|
||||
formik={formik}
|
||||
lockTarget={lockTarget}
|
||||
TargetContainer={TargetContainer}
|
||||
/>
|
||||
: <CosmosInputText
|
||||
name="Target"
|
||||
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
|
||||
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
|
||||
formik={formik}
|
||||
/>
|
||||
name="Target"
|
||||
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
|
||||
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
|
||||
formik={formik}
|
||||
/>
|
||||
}
|
||||
|
||||
<CosmosFormDivider title={'Source'}/>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Alert color='info'>What URL do you want to access your target from?</Alert>
|
||||
</Grid>
|
||||
<CosmosCheckbox
|
||||
name="UseHost"
|
||||
label="Use Host"
|
||||
|
@ -134,6 +143,7 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
|
|||
label="Host"
|
||||
placeholder="Host"
|
||||
formik={formik}
|
||||
style={{paddingLeft: '20px'}}
|
||||
/>}
|
||||
|
||||
<CosmosCheckbox
|
||||
|
@ -147,16 +157,22 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
|
|||
label="Path Prefix"
|
||||
placeholder="Path Prefix"
|
||||
formik={formik}
|
||||
style={{paddingLeft: '20px'}}
|
||||
/>}
|
||||
|
||||
{formik.values.UsePathPrefix && <CosmosCheckbox
|
||||
name="StripPathPrefix"
|
||||
label="Strip Path Prefix"
|
||||
formik={formik}
|
||||
style={{paddingLeft: '20px'}}
|
||||
/>}
|
||||
|
||||
<CosmosFormDivider title={'Security'}/>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
|
||||
</Grid>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="AuthEnabled"
|
||||
label="Authentication Required"
|
||||
|
|
|
@ -1,17 +1,276 @@
|
|||
// material-ui
|
||||
import { Alert, Typography } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { AppstoreAddOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Input, InputAdornment, TextField, Tooltip, Typography } 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';
|
||||
|
||||
// project import
|
||||
import MainCard from '../../components/MainCard';
|
||||
import * as API from '../../api';
|
||||
import isLoggedIn from '../../isLoggedIn';
|
||||
import RestartModal from '../config/users/restart';
|
||||
import RouteManagement from '../config/users/routeman';
|
||||
|
||||
// ==============================|| SAMPLE PAGE ||============================== //
|
||||
const Item = styled(Paper)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
|
||||
...theme.typography.body2,
|
||||
padding: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
color: theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const ServeApps = () => {
|
||||
const {serveApps, setServeApps} = useState([]);
|
||||
isLoggedIn();
|
||||
|
||||
const [serveApps, setServeApps] = useState([]);
|
||||
const [isUpdating, setIsUpdating] = useState({});
|
||||
const [search, setSearch] = useState("");
|
||||
const [config, setConfig] = useState(null);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [newRoute, setNewRoute] = useState(null);
|
||||
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);
|
||||
});
|
||||
API.config.get().then((res) => {
|
||||
setConfig(res.data);
|
||||
});
|
||||
setIsUpdating({});
|
||||
};
|
||||
|
||||
const setIsUpdatingId = (id, value) => {
|
||||
setIsUpdating({
|
||||
...isUpdating,
|
||||
[id]: value
|
||||
});
|
||||
}
|
||||
|
||||
const getContainersRoutes = (containerName) => {
|
||||
return config && config.HTTPConfig && config.HTTPConfig.ProxyConfig.Routes.filter((route) => {
|
||||
return route.Mode == "SERVAPP" && (
|
||||
route.Target.startsWith(containerName) ||
|
||||
route.Target.split('://')[1].startsWith(containerName)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshServeApps();
|
||||
}, []);
|
||||
|
||||
function updateRoutes() {
|
||||
let con = {
|
||||
...config,
|
||||
HTTPConfig: {
|
||||
...config.HTTPConfig,
|
||||
ProxyConfig: {
|
||||
...config.HTTPConfig.ProxyConfig,
|
||||
Routes: [
|
||||
...config.HTTPConfig.ProxyConfig.Routes,
|
||||
newRoute,
|
||||
]
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
API.config.set(con).then((res) => {
|
||||
setOpenModal(false);
|
||||
setOpenRestartModal(true);
|
||||
});
|
||||
}
|
||||
const gridAnim = {
|
||||
transition: 'all 0.2s ease',
|
||||
opacity: 1,
|
||||
transform: 'translateY(0px)',
|
||||
'&.MuiGrid2-item--hidden': {
|
||||
opacity: 0,
|
||||
transform: 'translateY(-20px)',
|
||||
},
|
||||
};
|
||||
|
||||
return <div>
|
||||
<Alert severity="info">Implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord!</Alert>
|
||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>Connect ServApp</DialogTitle>
|
||||
{openModal && <>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
Welcome to the Connect Wizard. This interface will help you expose your ServApp securely to the internet.
|
||||
</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] + ":8080",
|
||||
Mode: "SERVAPP",
|
||||
Name: openModal.Names[0].replace('/', ''),
|
||||
Description: "Expose " + openModal.Names[0].replace('/', '') + " to the internet",
|
||||
UseHost: false,
|
||||
Host: '',
|
||||
UsePathPrefix: false,
|
||||
PathPrefix: '',
|
||||
Timeout: 30000,
|
||||
ThrottlePerMinute: 100,
|
||||
CORSOrigin: '',
|
||||
StripPathPrefix: false,
|
||||
AuthEnabled: false,
|
||||
}}
|
||||
setRouteConfig={(_newRoute) => {
|
||||
setNewRoute(_newRoute);
|
||||
}}
|
||||
up={() => {}}
|
||||
down={() => {}}
|
||||
deleteRoute={() => {}}
|
||||
noControls
|
||||
lockTarget
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenModal(false)}>Cancel</Button>
|
||||
<Button onClick={() => {
|
||||
updateRoutes()
|
||||
}}>Connect</Button>
|
||||
</DialogActions>
|
||||
</>}
|
||||
</Dialog>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Input placeholder="Search"
|
||||
value={search}
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<SearchOutlined />
|
||||
</InputAdornment>
|
||||
}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" startIcon={<ReloadOutlined />} onClick={() => {
|
||||
refreshServeApps();
|
||||
}}>Refresh</Button>
|
||||
<Tooltip title="This is not implemented yet.">
|
||||
<span style={{ cursor: 'not-allowed' }}>
|
||||
<Button variant="contained" startIcon={<AppstoreAddOutlined />} disabled>Start ServApp</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
{serveApps && serveApps.filter(app => search.length < 2 || app.Names[0].includes(search)).map((app) => {
|
||||
return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4}>
|
||||
<Item>
|
||||
<Stack justifyContent='space-around' direction="column" spacing={2} padding={2} divider={<Divider orientation="horizontal" flexItem />}>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{
|
||||
({
|
||||
"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" />,
|
||||
})[app.State]
|
||||
}
|
||||
</Typography>
|
||||
<Stack direction="column" spacing={0} alignItems="flex-start">
|
||||
<Typography variant="h5" color="text.secondary">
|
||||
{app.Names[0].replace('/', '')}
|
||||
</Typography>
|
||||
<Typography style={{ fontSize: '80%' }} color="text.secondary">
|
||||
{app.Image}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Ports
|
||||
</Typography>
|
||||
<Stack margin={1} direction="row" spacing={1}>
|
||||
{app.Ports.map((port) => {
|
||||
return <Tooltip title={port.PublicPort ? 'Warning, this port is publicly accessible' : ''}>
|
||||
<Chip style={{ fontSize: '80%' }} label={":" + port.PrivatePort} color={port.PublicPort ? 'warning' : 'default'} />
|
||||
</Tooltip>
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Networks
|
||||
</Typography>
|
||||
<Stack margin={1} direction="row" spacing={1}>
|
||||
{app.NetworkSettings.Networks && Object.keys(app.NetworkSettings.Networks).map((network) => {
|
||||
return <Chip style={{ fontSize: '80%' }} label={network} color={network === 'bridge' ? 'warning' : 'default'} />
|
||||
})}
|
||||
</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>}
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Proxies
|
||||
</Typography>
|
||||
<Stack spacing={2} direction="row">
|
||||
{getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
|
||||
return <Chip label={route.Host + route.PathPrefix} color="info" />
|
||||
})}
|
||||
{getContainersRoutes(app.Names[0].replace('/', '')).length == 0 &&
|
||||
<Chip label="No Proxy Setup" />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button variant="contained" color="primary" onClick={() => {
|
||||
setOpenModal(app);
|
||||
}}>Connect</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Item></Grid2>
|
||||
})
|
||||
}
|
||||
</Grid2>
|
||||
</Stack>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ export default function ThemeCustomization({ children }) {
|
|||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ?
|
||||
'dark' : 'light');
|
||||
|
||||
console.log(theme)
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const themeTypography = Typography(`'Public Sans', sans-serif`);
|
||||
const themeCustomShadows = useMemo(() => CustomShadows(theme), [theme]);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
export default function Button(theme) {
|
||||
const disabledStyle = {
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: theme.palette.grey[200]
|
||||
backgroundColor: theme.palette.grey[400]
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -55,24 +55,26 @@ const Palette = (mode) => {
|
|||
}
|
||||
}
|
||||
} : {
|
||||
mode,
|
||||
common: {
|
||||
black: '#000',
|
||||
white: '#fff'
|
||||
},
|
||||
...paletteColor,
|
||||
text: {
|
||||
primary: paletteColor.grey[700],
|
||||
secondary: paletteColor.grey[500],
|
||||
disabled: paletteColor.grey[400]
|
||||
},
|
||||
action: {
|
||||
disabled: paletteColor.grey[300]
|
||||
},
|
||||
divider: paletteColor.grey[200],
|
||||
background: {
|
||||
paper: paletteColor.grey[0],
|
||||
default: paletteColor.grey.A50
|
||||
palette: {
|
||||
mode,
|
||||
common: {
|
||||
black: '#000',
|
||||
white: '#fff'
|
||||
},
|
||||
...paletteColor,
|
||||
text: {
|
||||
primary: paletteColor.grey[700],
|
||||
secondary: paletteColor.grey[600],
|
||||
disabled: paletteColor.grey[500]
|
||||
},
|
||||
action: {
|
||||
disabled: paletteColor.grey[300]
|
||||
},
|
||||
divider: paletteColor.grey[200],
|
||||
background: {
|
||||
paper: paletteColor.grey[0],
|
||||
default: paletteColor.grey.A50
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
1
go.mod
1
go.mod
|
@ -64,6 +64,7 @@ require (
|
|||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/imdario/mergo v0.3.14 // indirect
|
||||
github.com/jarcoal/httpmock v1.0.7 // indirect
|
||||
github.com/jasonlvhit/gocron v0.0.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.10 // indirect
|
||||
|
|
5
go.sum
5
go.sum
|
@ -187,6 +187,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
||||
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||
github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
|
||||
github.com/go-resty/resty/v2 v2.4.0 h1:s6TItTLejEI+2mn98oijC5w/Rk2YU+OA6x0mnZN6r6k=
|
||||
github.com/go-resty/resty/v2 v2.4.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
|
||||
|
@ -302,6 +303,8 @@ github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h
|
|||
github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jarcoal/httpmock v1.0.7 h1:d1a2VFpSdm5gtjhCPWsQHSnx8+5V3ms5431YwvmkuNk=
|
||||
github.com/jarcoal/httpmock v1.0.7/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
|
||||
github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
|
@ -391,7 +394,9 @@ github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4r
|
|||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.0.9",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
69
src/CRON.go
Normal file
69
src/CRON.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/jasonlvhit/gocron"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func checkVersion() {
|
||||
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
exPath := filepath.Dir(ex)
|
||||
|
||||
pjs, errPR := os.Open(exPath + "/meta.json")
|
||||
if errPR != nil {
|
||||
utils.Error("checkVersion", errPR)
|
||||
return
|
||||
}
|
||||
|
||||
packageJson, _ := ioutil.ReadAll(pjs)
|
||||
|
||||
utils.Debug("checkVersion" + string(packageJson))
|
||||
|
||||
var version Version
|
||||
errJ := json.Unmarshal(packageJson, &version)
|
||||
if errJ != nil {
|
||||
utils.Error("checkVersion", errJ)
|
||||
return
|
||||
}
|
||||
|
||||
myVersion := version.Version
|
||||
|
||||
response, err := http.Get("https://comos-technologies.com/versions/" + myVersion)
|
||||
if err != nil {
|
||||
utils.Error("checkVersion", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
utils.Error("checkVersion", err)
|
||||
return
|
||||
}
|
||||
|
||||
if string(body) != myVersion {
|
||||
utils.Log("New version available: " + string(body))
|
||||
// update
|
||||
} else {
|
||||
utils.Log("No new version available")
|
||||
}
|
||||
}
|
||||
|
||||
func CRON() {
|
||||
gocron.Every(1).Day().At("00:00").Do(checkVersion)
|
||||
<-gocron.Start()
|
||||
}
|
|
@ -33,6 +33,7 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
|
|||
config := utils.GetBaseMainConfig()
|
||||
request.HTTPConfig.AuthPrivateKey = config.HTTPConfig.AuthPrivateKey
|
||||
request.HTTPConfig.TLSKey = config.HTTPConfig.TLSKey
|
||||
request.NewInstall = config.NewInstall
|
||||
|
||||
utils.SaveConfigTofile(request)
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
containerName := utils.Sanitize(vars["container"])
|
||||
containerName := utils.Sanitize(vars["containerId"])
|
||||
status := utils.Sanitize(vars["status"])
|
||||
|
||||
if(req.Method == "GET") {
|
||||
container, err := DockerClient.ContainerInspect(DockerContext, containerName)
|
||||
|
@ -26,10 +27,10 @@ func SecureContainerRoute(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
AddLabels(container, map[string]string{
|
||||
"cosmos-force-network-secured": "true",
|
||||
"cosmos-force-network-secured": status,
|
||||
});
|
||||
|
||||
utils.Log("API: Add Force network secured: " + containerName)
|
||||
utils.Log("API: Set Force network secured "+status+" : " + containerName)
|
||||
|
||||
_, errEdit := EditContainer(container.ID, container)
|
||||
if errEdit != nil {
|
||||
|
|
|
@ -138,7 +138,9 @@ func ListContainers() ([]types.Container, error) {
|
|||
return nil, errD
|
||||
}
|
||||
|
||||
containers, err := DockerClient.ContainerList(DockerContext, types.ContainerListOptions{})
|
||||
containers, err := DockerClient.ContainerList(DockerContext, types.ContainerListOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -202,7 +202,7 @@ func StartServer() {
|
|||
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
||||
srapi.HandleFunc("/api/users", user.UsersRoute)
|
||||
|
||||
srapi.HandleFunc("/api/servapps/{container}/secure", docker.SecureContainerRoute)
|
||||
srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute)
|
||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||
|
||||
srapi.Use(tokenMiddleware)
|
||||
|
|
|
@ -14,6 +14,8 @@ func main() {
|
|||
|
||||
LoadConfig()
|
||||
|
||||
go CRON()
|
||||
|
||||
docker.Test()
|
||||
|
||||
docker.DockerListenEvents()
|
||||
|
|
|
@ -15,7 +15,7 @@ func BuildFromConfig(router *mux.Router, config utils.ProxyConfig) *mux.Router {
|
|||
|
||||
for i := len(config.Routes)-1; i >= 0; i-- {
|
||||
routeConfig := config.Routes[i]
|
||||
RouterGen(routeConfig, router, RouteTo(routeConfig.Target))
|
||||
RouterGen(routeConfig, router, RouteTo(routeConfig))
|
||||
}
|
||||
|
||||
return router
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
spa "github.com/roberthodgen/spa-server"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
// "io/ioutil"
|
||||
// "io"
|
||||
|
@ -20,7 +21,6 @@ func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
|
|||
|
||||
proxy := httputil.NewSingleHostReverseProxy(url)
|
||||
|
||||
// upgrade the request to websocket
|
||||
proxy.ModifyResponse = func(resp *http.Response) error {
|
||||
utils.Debug("Response from backend: " + resp.Status)
|
||||
utils.Debug("URL was " + resp.Request.URL.String())
|
||||
|
@ -30,20 +30,31 @@ func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
|
|||
return proxy, nil
|
||||
}
|
||||
|
||||
// ProxyRequestHandler handles the http request using proxy
|
||||
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func RouteTo(destination string) *httputil.ReverseProxy /*func(http.ResponseWriter, *http.Request)*/ {
|
||||
func RouteTo(route utils.ProxyRouteConfig) http.Handler /*func(http.ResponseWriter, *http.Request)*/ {
|
||||
// initialize a reverse proxy and pass the actual backend server url here
|
||||
proxy, err := NewProxy(destination)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// create a handler function which uses the reverse proxy
|
||||
return proxy //ProxyRequestHandler(proxy)
|
||||
destination := route.Target
|
||||
routeType := route.Mode
|
||||
|
||||
if(routeType == "SERVAPP" || routeType == "PROXY") {
|
||||
proxy, err := NewProxy(destination)
|
||||
if err != nil {
|
||||
utils.Error("Create Route", err)
|
||||
}
|
||||
|
||||
// create a handler function which uses the reverse proxy
|
||||
return proxy
|
||||
} else if (routeType == "STATIC") {
|
||||
return http.FileServer(http.Dir(destination))
|
||||
} else if (routeType == "SPA") {
|
||||
return spa.SpaHandler(destination, "index.html")
|
||||
} else if(routeType == "REDIRECT") {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, destination, 302)
|
||||
})
|
||||
} else {
|
||||
utils.Error("Invalid route type", nil)
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package proxy
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"github.com/gorilla/mux"
|
||||
"time"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
|
@ -44,10 +43,7 @@ func tokenMiddleware(enabled bool) func(next http.Handler) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *httputil.ReverseProxy) *mux.Route {
|
||||
var realDestination http.Handler
|
||||
realDestination = destination
|
||||
|
||||
func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination http.Handler) *mux.Route {
|
||||
origin := router.Methods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD")
|
||||
|
||||
if(route.UseHost) {
|
||||
|
@ -55,11 +51,17 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *ht
|
|||
}
|
||||
|
||||
if(route.UsePathPrefix) {
|
||||
if(route.PathPrefix != "" && route.PathPrefix[0] != '/') {
|
||||
utils.Error("PathPrefix must start with a /", nil)
|
||||
}
|
||||
origin = origin.PathPrefix(route.PathPrefix)
|
||||
}
|
||||
|
||||
if(route.UsePathPrefix && route.StripPathPrefix) {
|
||||
realDestination = http.StripPrefix(route.PathPrefix, destination)
|
||||
if(route.PathPrefix != "" && route.PathPrefix[0] != '/') {
|
||||
utils.Error("PathPrefix must start with a /", nil)
|
||||
}
|
||||
destination = http.StripPrefix(route.PathPrefix, destination)
|
||||
}
|
||||
timeout := route.Timeout
|
||||
|
||||
|
@ -83,6 +85,10 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *ht
|
|||
}
|
||||
}
|
||||
|
||||
if(route.UsePathPrefix && !route.StripPathPrefix && (route.Mode == "STATIC" || route.Mode == "SPA")) {
|
||||
utils.Warn("PathPrefix is used, but StripPathPrefix is false. The route mode is " + (string)(route.Mode) + ". This will likely cause issues with the route. Ignore this warning if you know what you are doing.")
|
||||
}
|
||||
|
||||
origin.Handler(
|
||||
tokenMiddleware(route.AuthEnabled)(
|
||||
utils.CORSHeader(originCORS)(
|
||||
|
@ -95,7 +101,9 @@ func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *ht
|
|||
http.StatusTooManyRequests, "HTTP003")
|
||||
return
|
||||
}),
|
||||
)(realDestination)))))
|
||||
)(destination)))))
|
||||
|
||||
utils.Log("Added route: ["+ (string)(route.Mode) + "] " + route.Host + route.PathPrefix + " to " + route.Target + "")
|
||||
|
||||
return origin
|
||||
}
|
Loading…
Add table
Reference in a new issue