v0.1.0 servapps management

This commit is contained in:
Yann Stepienik 2023-03-31 20:19:38 +01:00
parent 822d4bc057
commit cc2c749250
25 changed files with 491 additions and 83 deletions

2
.nvmrc
View File

@ -1 +1 @@
16
16

View File

@ -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/

View File

@ -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

View File

@ -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
};

View File

@ -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

View File

@ -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}

View File

@ -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}

View File

@ -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)}

View File

@ -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} &nbsp;
<Chip label={<UpOutlined />} onClick={() => up()}/> &nbsp;
<Chip label={<DownOutlined />} onClick={() => down()}/> &nbsp;
@ -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"

View File

@ -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('/', '')}&nbsp;
</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>
}

View File

@ -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]);

View File

@ -3,7 +3,7 @@
export default function Button(theme) {
const disabledStyle = {
'&.Mui-disabled': {
backgroundColor: theme.palette.grey[200]
backgroundColor: theme.palette.grey[400]
}
};

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
View 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()
}

View File

@ -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)

View File

@ -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 {

View File

@ -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
}

View File

@ -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)

View File

@ -14,6 +14,8 @@ func main() {
LoadConfig()
go CRON()
docker.Test()
docker.DockerListenEvents()

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}