[release] v0.4.0-unstable2
This commit is contained in:
parent
efab134d73
commit
241abdfca4
|
@ -1,5 +1,7 @@
|
||||||
## Version 0.4.0
|
## Version 0.4.0
|
||||||
- Protect server against direct IP access
|
- Protect server against direct IP access
|
||||||
|
- Stop / Start / Restart / Remove / Kill containers
|
||||||
|
-
|
||||||
|
|
||||||
## Version 0.3.0
|
## Version 0.3.0
|
||||||
- Implement 2 FA
|
- Implement 2 FA
|
||||||
|
|
|
@ -22,8 +22,17 @@ const newDB = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const manageContainer = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolve({
|
||||||
|
"status": "ok",
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
list,
|
list,
|
||||||
newDB,
|
newDB,
|
||||||
secure
|
secure,
|
||||||
|
manageContainer
|
||||||
};
|
};
|
|
@ -26,9 +26,19 @@ const newDB = () => {
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const manageContainer = (id, action) => {
|
||||||
|
return wrap(fetch('/cosmos/api/servapps/' + id + '/manage/' + action, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
list,
|
list,
|
||||||
newDB,
|
newDB,
|
||||||
secure
|
secure,
|
||||||
|
manageContainer
|
||||||
};
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
// material-ui
|
// material-ui
|
||||||
import { AppstoreAddOutlined, PlusCircleOutlined, ReloadOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
|
import { AppstoreAddOutlined, CloseSquareOutlined, DeleteOutlined, PauseCircleOutlined, PlaySquareOutlined, PlusCircleOutlined, ReloadOutlined, RollbackOutlined, SearchOutlined, SettingOutlined, StopOutlined, UpCircleOutlined, UpSquareFilled } 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 { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, IconButton, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material';
|
||||||
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
|
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
|
||||||
import { Stack } from '@mui/system';
|
import { Stack } from '@mui/system';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
@ -126,6 +126,88 @@ 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>
|
return <div>
|
||||||
<IsLoggedIn />
|
<IsLoggedIn />
|
||||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||||
|
@ -229,31 +311,40 @@ const ServeApps = () => {
|
||||||
return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4}>
|
return <Grid2 style={gridAnim} xs={12} sm={6} md={6} lg={6} xl={4}>
|
||||||
<Item>
|
<Item>
|
||||||
<Stack justifyContent='space-around' direction="column" spacing={2} padding={2} divider={<Divider orientation="horizontal" flexItem />}>
|
<Stack justifyContent='space-around' direction="column" spacing={2} padding={2} divider={<Divider orientation="horizontal" flexItem />}>
|
||||||
<Stack style={{position: 'relative', overflowX: 'hidden'}} direction="row" spacing={2} alignItems="center">
|
<Stack direction="column" spacing={0} alignItems="flex-start">
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Stack style={{position: 'relative', overflowX: 'hidden'}} direction="row" spacing={2} alignItems="center">
|
||||||
{
|
<Typography variant="body2" color="text.secondary">
|
||||||
({
|
{
|
||||||
"created": <Chip label="Created" color="warning" />,
|
({
|
||||||
"restarting": <Chip label="Restarting" color="warning" />,
|
"created": <Chip label="Created" color="warning" />,
|
||||||
"running": <Chip label="Running" color="success" />,
|
"restarting": <Chip label="Restarting" color="warning" />,
|
||||||
"removing": <Chip label="Removing" color="error" />,
|
"running": <Chip label="Running" color="success" />,
|
||||||
"paused": <Chip label="Paused" color="info" />,
|
"removing": <Chip label="Removing" color="error" />,
|
||||||
"exited": <Chip label="Exited" color="error" />,
|
"paused": <Chip label="Paused" color="info" />,
|
||||||
"dead": <Chip label="Dead" color="error" />,
|
"exited": <Chip label="Exited" color="error" />,
|
||||||
})[app.State]
|
"dead": <Chip label="Dead" color="error" />,
|
||||||
}
|
})[app.State]
|
||||||
</Typography>
|
}
|
||||||
<Stack direction="row" spacing={2} alignItems="center">
|
</Typography>
|
||||||
<img src={getFirstRouteFavIcon(app)} width="40px" />
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
<Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px'}}>
|
<img src={getFirstRouteFavIcon(app)} width="40px" />
|
||||||
<Typography variant="h5" color="text.secondary">
|
<Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px'}}>
|
||||||
{app.Names[0].replace('/', '')}
|
<Typography variant="h5" color="text.secondary">
|
||||||
</Typography>
|
{app.Names[0].replace('/', '')}
|
||||||
<Typography style={{ fontSize: '80%' }} color="text.secondary">
|
</Typography>
|
||||||
{app.Image}
|
<Typography style={{ fontSize: '80%' }} color="text.secondary">
|
||||||
</Typography>
|
{app.Image}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
{/* <Button variant="contained" size="small" onClick={() => {}}>
|
||||||
|
Update
|
||||||
|
</Button> */}
|
||||||
|
|
||||||
|
{getActions(app)}
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||||
<Typography variant="h6" color="text.secondary">
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.4.0-unstable",
|
"version": "0.4.0-unstable2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "test-server.js",
|
"main": "test-server.js",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
94
src/docker/api_managecont.go
Normal file
94
src/docker/api_managecont.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ManageContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("ManageContainer", errD)
|
||||||
|
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(req)
|
||||||
|
containerName := utils.Sanitize(vars["containerId"])
|
||||||
|
// stop, start, restart, kill, remove, pause, unpause, recreate
|
||||||
|
action := utils.Sanitize(vars["action"])
|
||||||
|
|
||||||
|
utils.Log("ManageContainer " + containerName)
|
||||||
|
|
||||||
|
if req.Method == "GET" {
|
||||||
|
container, err := DockerClient.ContainerInspect(DockerContext, containerName)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("ManageContainer", err)
|
||||||
|
utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS002")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imagename := container.Config.Image
|
||||||
|
|
||||||
|
utils.Log("API: " + action + " " + containerName)
|
||||||
|
|
||||||
|
// switch action
|
||||||
|
switch action {
|
||||||
|
case "stop":
|
||||||
|
err = DockerClient.ContainerStop(DockerContext, container.ID, contstuff.StopOptions{})
|
||||||
|
case "start":
|
||||||
|
err = DockerClient.ContainerStart(DockerContext, container.ID, doctype.ContainerStartOptions{})
|
||||||
|
case "restart":
|
||||||
|
err = DockerClient.ContainerRestart(DockerContext, container.ID, contstuff.StopOptions{})
|
||||||
|
case "kill":
|
||||||
|
err = DockerClient.ContainerKill(DockerContext, container.ID, "")
|
||||||
|
case "remove":
|
||||||
|
err = DockerClient.ContainerRemove(DockerContext, container.ID, doctype.ContainerRemoveOptions{})
|
||||||
|
case "pause":
|
||||||
|
err = DockerClient.ContainerPause(DockerContext, container.ID)
|
||||||
|
case "unpause":
|
||||||
|
err = DockerClient.ContainerUnpause(DockerContext, container.ID)
|
||||||
|
case "recreate":
|
||||||
|
_, err = EditContainer(container.ID, container)
|
||||||
|
case "update":
|
||||||
|
pull, errPull := DockerClient.ImagePull(DockerContext, imagename, doctype.ImagePullOptions{})
|
||||||
|
if errPull != nil {
|
||||||
|
utils.Error("Docker Pull", errPull)
|
||||||
|
utils.HTTPError(w, "Cannot pull new image", http.StatusBadRequest, "DS004")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
io.Copy(os.Stdout, pull)
|
||||||
|
_, err = EditContainer(container.ID, container)
|
||||||
|
default:
|
||||||
|
utils.HTTPError(w, "Invalid action", http.StatusBadRequest, "DS003")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("ManageContainer: " + action, err)
|
||||||
|
utils.HTTPError(w, "Internal server error: " + err.Error(), http.StatusInternalServerError, "DS004")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("ManageContainer: Method not allowed" + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -190,24 +190,6 @@ func ListContainers() ([]types.Container, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// for _, container := range containers {
|
|
||||||
// fmt.Println("ID - ", container.ID)
|
|
||||||
// fmt.Println("ID - ", container.Names)
|
|
||||||
// fmt.Println("ID - ", container.Image)
|
|
||||||
// fmt.Println("ID - ", container.Command)
|
|
||||||
// fmt.Println("ID - ", container.State)
|
|
||||||
// fmt.Println("Ports - ", container.Ports)
|
|
||||||
// fmt.Println("HostConfig - ", container.HostConfig)
|
|
||||||
// fmt.Println("ID - ", container.Labels)
|
|
||||||
// fmt.Println("NetworkSettings - ", container.NetworkSettings)
|
|
||||||
// if(container.NetworkSettings.Networks["cosmos-network"] != nil) {
|
|
||||||
// fmt.Println("IP COSMOS - ", container.NetworkSettings.Networks["cosmos-network"].IPAddress);
|
|
||||||
// }
|
|
||||||
// if(container.NetworkSettings.Networks["bridge"] != nil) {
|
|
||||||
// fmt.Println("IP bridge - ", container.NetworkSettings.Networks["bridge"].IPAddress);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return containers, nil
|
return containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -220,10 +220,14 @@ func StartServer() {
|
||||||
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
||||||
srapi.HandleFunc("/api/users", user.UsersRoute)
|
srapi.HandleFunc("/api/users", user.UsersRoute)
|
||||||
|
|
||||||
|
srapi.HandleFunc("/api/servapps/{containerId}/manage/{action}", docker.ManageContainerRoute)
|
||||||
srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute)
|
srapi.HandleFunc("/api/servapps/{containerId}/secure/{status}", docker.SecureContainerRoute)
|
||||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||||
|
|
||||||
srapi.Use(utils.EnsureHostname(serverHostname))
|
// if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||||
|
// srapi.Use(utils.EnsureHostname(serverHostname))
|
||||||
|
// }
|
||||||
|
|
||||||
srapi.Use(tokenMiddleware)
|
srapi.Use(tokenMiddleware)
|
||||||
srapi.Use(proxy.SmartShieldMiddleware(
|
srapi.Use(proxy.SmartShieldMiddleware(
|
||||||
utils.SmartShieldPolicy{
|
utils.SmartShieldPolicy{
|
||||||
|
@ -251,8 +255,14 @@ func StartServer() {
|
||||||
utils.Fatal("Static folder not found at " + pwd + "/static", err)
|
utils.Fatal("Static folder not found at " + pwd + "/static", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fs := spa.SpaHandler(pwd + "/static", "index.html")
|
fs := spa.SpaHandler(pwd + "/static", "index.html")
|
||||||
router.PathPrefix("/ui").Handler(utils.EnsureHostname(serverHostname)(http.StripPrefix("/ui", fs)))
|
|
||||||
|
// if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||||
|
// fs = utils.EnsureHostname(serverHostname)(fs)
|
||||||
|
// }
|
||||||
|
|
||||||
|
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs))
|
||||||
|
|
||||||
router = proxy.BuildFromConfig(router, HTTPConfig.ProxyConfig)
|
router = proxy.BuildFromConfig(router, HTTPConfig.ProxyConfig)
|
||||||
|
|
||||||
|
|
|
@ -10,14 +10,11 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
utils.Log("Starting...")
|
utils.Log("Starting...")
|
||||||
// utils.Log("Smart Shield estimates the capacity at " + strconv.Itoa((int)(proxy.MaxUsers)) + " concurrent users")
|
|
||||||
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
|
|
||||||
checkVersion()
|
|
||||||
|
|
||||||
go CRON()
|
go CRON()
|
||||||
|
|
||||||
docker.Test()
|
docker.Test()
|
||||||
|
|
|
@ -101,6 +101,7 @@ type HTTPConfig struct {
|
||||||
ProxyConfig ProxyConfig
|
ProxyConfig ProxyConfig
|
||||||
Hostname string `validate:"required,excludesall=0x2C/ "`
|
Hostname string `validate:"required,excludesall=0x2C/ "`
|
||||||
SSLEmail string `validate:"omitempty,email"`
|
SSLEmail string `validate:"omitempty,email"`
|
||||||
|
AcceptAllInsecureHostname bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
Loading…
Reference in a new issue