[release] version 0.5.0-unstable
This commit is contained in:
parent
b76f0650d8
commit
8b4d738c2e
16 changed files with 486 additions and 14 deletions
|
@ -157,6 +157,22 @@ function createVolume(values) {
|
|||
}))
|
||||
}
|
||||
|
||||
function attachTerminal(containerId) {
|
||||
let protocol = 'ws://';
|
||||
if (window.location.protocol === 'https:') {
|
||||
protocol = 'wss://';
|
||||
}
|
||||
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/attach');
|
||||
}
|
||||
|
||||
function createTerminal(containerId) {
|
||||
let protocol = 'ws://';
|
||||
if (window.location.protocol === 'https:') {
|
||||
protocol = 'wss://';
|
||||
}
|
||||
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/new');
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
get,
|
||||
|
@ -174,4 +190,6 @@ export {
|
|||
attachNetwork,
|
||||
detachNetwork,
|
||||
createVolume,
|
||||
attachTerminal,
|
||||
createTerminal,
|
||||
};
|
|
@ -56,7 +56,6 @@ const LogLine = ({ message, docker, isMobile }) => {
|
|||
if(docker) {
|
||||
let parts = html.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)
|
||||
if(!parts) {
|
||||
console.error('Could not parse log line', html)
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
let restString = html.replace(parts[0], '')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Button, useMediaQuery, IconButton } from "@mui/material";
|
||||
|
||||
|
||||
const ResponsiveButton = ({ children, startIcon, size, style, ...props }) => {
|
||||
const ResponsiveButton = ({ children, startIcon, endIcon, size, style, ...props }) => {
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||
let newStyle = style || {};
|
||||
if (isMobile) {
|
||||
|
@ -10,8 +10,18 @@ const ResponsiveButton = ({ children, startIcon, size, style, ...props }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Button className="responsive-button" size={isMobile ? 'large' : size} startIcon={isMobile ? null : startIcon} {...props} style={newStyle}>
|
||||
{isMobile ? startIcon : children}
|
||||
<Button
|
||||
className="responsive-button"
|
||||
size={isMobile ? 'large' : size}
|
||||
startIcon={isMobile ? null : startIcon}
|
||||
endIcon={isMobile ? null : endIcon}
|
||||
{...props} style={newStyle}>
|
||||
{(isMobile) ? startIcon : (
|
||||
startIcon ? children : null
|
||||
)}
|
||||
{(isMobile) ? endIcon : (
|
||||
endIcon ? children : null
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
62
client/src/components/terminal.jsx
Normal file
62
client/src/components/terminal.jsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
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 LogLine from '../components/logLine';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
const Terminal = ({ logs, setLogs, fetchLogs, docker }) => {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [hasScrolled, setHasScrolled] = useState(false);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
const screenMin = useMediaQuery((theme) => theme.breakpoints.up('sm'))
|
||||
|
||||
const bottomRef = useRef(null);
|
||||
const terminalRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
bottomRef.current.scrollIntoView({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasScrolled) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
const handleScroll = (event) => {
|
||||
if (event.target.scrollHeight - event.target.scrollTop === event.target.clientHeight) {
|
||||
setHasScrolled(false);
|
||||
}else {
|
||||
setHasScrolled(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={terminalRef}
|
||||
sx={{
|
||||
minHeight: '50px',
|
||||
maxHeight: 'calc(1vh * 80 - 200px)',
|
||||
overflow: 'auto',
|
||||
padding: '10px',
|
||||
wordBreak: 'break-all',
|
||||
background: '#272d36',
|
||||
color: '#fff',
|
||||
borderTop: '3px solid ' + theme.palette.primary.main
|
||||
}}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{logs && logs.map((log, index) => (
|
||||
<div key={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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Terminal;
|
|
@ -16,6 +16,7 @@ import Logs from './logs';
|
|||
import DockerContainerSetup from './setup';
|
||||
import NetworkContainerSetup from './network';
|
||||
import VolumeContainerSetup from './volumes';
|
||||
import DockerTerminal from './terminal';
|
||||
|
||||
const ContainerIndex = () => {
|
||||
const { containerName } = useParams();
|
||||
|
@ -56,9 +57,7 @@ const ContainerIndex = () => {
|
|||
},
|
||||
{
|
||||
title: 'Terminal',
|
||||
children: <div>
|
||||
<Alert severity="info">This feature is not yet implemented. It is planned for next version: 0.5.0</Alert>
|
||||
</div>
|
||||
children: <DockerTerminal refresh={refreshContainer} containerInfo={container} config={config}/>
|
||||
},
|
||||
{
|
||||
title: 'Links',
|
||||
|
|
|
@ -32,6 +32,7 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
labels: Object.keys(containerInfo.Config.Labels).map((key) => {
|
||||
return { key, value: containerInfo.Config.Labels[key] };
|
||||
}),
|
||||
interactive: containerInfo.Config.Tty && containerInfo.Config.OpenStdin,
|
||||
}}
|
||||
validate={(values) => {
|
||||
const errors = {};
|
||||
|
@ -65,6 +66,8 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
envVars: envVars,
|
||||
labels: labels,
|
||||
};
|
||||
realvalues.interactive = realvalues.interactive ? 2 : 1;
|
||||
|
||||
return API.docker.updateContainer(containerInfo.Name.replace('/', ''), realvalues)
|
||||
.then((res) => {
|
||||
setStatus({ success: true });
|
||||
|
@ -101,6 +104,12 @@ const DockerContainerSetup = ({config, containerInfo, refresh}) => {
|
|||
options={restartPolicies}
|
||||
formik={formik}
|
||||
/>
|
||||
<CosmosCheckbox
|
||||
name="interactive"
|
||||
label="Interactive Mode"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosFormDivider title={'Environment Variables'} />
|
||||
<Grid item xs={12}>
|
||||
{formik.values.envVars.map((envVar, idx) => (
|
||||
|
|
171
client/src/pages/servapps/containers/terminal.jsx
Normal file
171
client/src/pages/servapps/containers/terminal.jsx
Normal file
|
@ -0,0 +1,171 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import * as API from '../../../api';
|
||||
import { Alert, Input, Stack, useMediaQuery, useTheme } from '@mui/material';
|
||||
import Terminal from '../../../components/terminal';
|
||||
import { ApiOutlined, SendOutlined } from '@ant-design/icons';
|
||||
import ResponsiveButton from '../../../components/responseiveButton';
|
||||
|
||||
const DockerTerminal = ({containerInfo, refresh}) => {
|
||||
const { Name, Config, NetworkSettings, State } = containerInfo;
|
||||
const isInteractive = Config.Tty;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'))
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
const [output, setOutput] = useState([
|
||||
{
|
||||
output: 'Not Connected.',
|
||||
type: 'stdout'
|
||||
}
|
||||
]);
|
||||
const ws = useRef(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const makeInteractive = () => {
|
||||
API.docker.updateContainer(Name.slice(1), {interactive: 2})
|
||||
.then(() => {
|
||||
refresh && refresh();
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
refresh && refresh();
|
||||
});
|
||||
};
|
||||
|
||||
const connect = (newProc) => {
|
||||
if(ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
|
||||
ws.current = newProc ?
|
||||
API.docker.createTerminal(Name.slice(1))
|
||||
: API.docker.attachTerminal(Name.slice(1));
|
||||
|
||||
ws.current.onmessage = (event) => {
|
||||
try {
|
||||
let data = JSON.parse(event.data);
|
||||
setOutput((prevOutput) => [...prevOutput, ...data]);
|
||||
} catch (e) {
|
||||
console.error("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.current.onclose = () => {
|
||||
setIsConnected(false);
|
||||
let terminalBoldRed = '\x1b[1;31m';
|
||||
setOutput((prevOutput) => [...prevOutput,
|
||||
{output: terminalBoldRed + 'Disconnected from ' + (newProc ? 'bash' : 'main process TTY'), type: 'stdout'}]);
|
||||
};
|
||||
|
||||
ws.current.onopen = () => {
|
||||
setIsConnected(true);
|
||||
let terminalBoldGreen = '\x1b[1;32m';
|
||||
setOutput((prevOutput) => [...prevOutput,
|
||||
{output: terminalBoldGreen + 'Connected to ' + (newProc ? 'bash' : 'main process TTY'), type: 'stdout'}]);
|
||||
};
|
||||
|
||||
return () => {
|
||||
setIsConnected(false);
|
||||
ws.current.close();
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
}, []);
|
||||
|
||||
|
||||
const sendMessage = () => {
|
||||
if (ws.current) {
|
||||
ws.current.send(message);
|
||||
setMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="terminal-container" onKeyDown={handleKeyDown}>
|
||||
{(!isInteractive) && (
|
||||
<Alert severity="warning">
|
||||
This container is not interactive.
|
||||
If you want to connect to the main process,
|
||||
<Button onClick={() => makeInteractive()}>Enable TTY</Button>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Terminal
|
||||
logs={output}
|
||||
setLogs={setOutput}
|
||||
docker
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={1}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{
|
||||
background: '#272d36',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '125%',
|
||||
padding: '10px 0',
|
||||
}}>
|
||||
{
|
||||
isConnected ? (
|
||||
<ApiOutlined style={{color: '#00ff00'}} />
|
||||
) : (
|
||||
<ApiOutlined style={{color: '#ff0000'}} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
multiline
|
||||
fullWidth
|
||||
placeholder={isMobile ? "Enter command" : "Enter command (CTRL+Enter to send)"}
|
||||
style={{
|
||||
fontSize: '125%',
|
||||
padding: '10px 10px',
|
||||
background: 'rgba(0,0,0,0.1)',
|
||||
}}
|
||||
disableUnderline
|
||||
disabled={!isConnected}
|
||||
/>
|
||||
|
||||
<ResponsiveButton variant="outlined" disabled={!isConnected} onClick={sendMessage} endIcon={
|
||||
<SendOutlined />
|
||||
}>Send</ResponsiveButton>
|
||||
</Stack>
|
||||
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
>
|
||||
<Button variant="outlined"
|
||||
onClick={() => connect(false)}>Connect</Button>
|
||||
<Button variant="outlined" onClick={() => connect(true)}>New Shell</Button>
|
||||
{isConnected && (
|
||||
<Button variant="outlined" onClick={() => ws.current.close()}>Disconnect</Button>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DockerTerminal;
|
1
go.mod
1
go.mod
|
@ -74,6 +74,7 @@ require (
|
|||
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
|
||||
github.com/gophercloud/gophercloud v0.15.0 // indirect
|
||||
github.com/gophercloud/utils v0.0.0-20210113034859-6f548432055a // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -326,6 +326,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
|||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.4.3",
|
||||
"version": "0.5.0-unstable",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
|
@ -21,10 +21,10 @@ type LogOutput struct {
|
|||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
// parseDockerLogHeader parses the first 8 bytes of a Docker log message
|
||||
// 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) {
|
||||
func ParseDockerLogHeader(data []byte) (LogOutput) {
|
||||
var logOutput LogOutput
|
||||
logOutput.StreamType = 1 // assume stdout if header not present
|
||||
logOutput.Size = uint32(len(data))
|
||||
|
@ -65,11 +65,11 @@ func FilterLogs(logReader io.Reader, searchQuery string, limit int) []LogOutput
|
|||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if len(searchQuery) > 0 && !strings.Contains(strings.ToUpper(line), strings.ToUpper(searchQuery)) {
|
||||
if len(searchQuery) > 3 && !strings.Contains(strings.ToUpper(line), strings.ToUpper(searchQuery)) {
|
||||
continue
|
||||
}
|
||||
|
||||
logLines = append(logLines, parseDockerLogHeader(([]byte)(line)))
|
||||
logLines = append(logLines, ParseDockerLogHeader(([]byte)(line)))
|
||||
}
|
||||
|
||||
from := utils.Max(len(logLines)-limit, 0)
|
||||
|
|
|
@ -25,8 +25,6 @@ func ManageContainerRoute(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
vars := mux.Vars(req)
|
||||
containerName := utils.SanitizeSafe(vars["containerId"])
|
||||
// stop, start, restart, kill, remove, pause, unpause, recreate
|
||||
|
|
194
src/docker/api_terminal.go
Normal file
194
src/docker/api_terminal.go
Normal file
|
@ -0,0 +1,194 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
func splitIntoChunks(input string, chunkSize int) [][]LogOutput {
|
||||
lines := strings.Split(input, "\n")
|
||||
var chunks [][]LogOutput
|
||||
|
||||
for i := 0; i < len(lines); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
|
||||
// Avoid going over the end of the array
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
|
||||
var chunk []LogOutput
|
||||
for j := i; j < end; j++ {
|
||||
chunk = append(chunk, ParseDockerLogHeader(([]byte)(
|
||||
lines[j],
|
||||
)))
|
||||
}
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
func TerminalRoute(w http.ResponseWriter, r *http.Request) {
|
||||
if utils.AdminOnly(w, r) != nil {
|
||||
return
|
||||
}
|
||||
utils.Log("Attempting to attach container")
|
||||
|
||||
upgrader.ReadBufferSize = 1024 * 4 // Increase the buffer size as needed
|
||||
// Upgrade initial GET request to a websocket
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
utils.Error("Failed to set websocket upgrade: ", err)
|
||||
http.Error(w, "Failed to set websocket upgrade: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("ManageContainer", errD)
|
||||
utils.HTTPError(w, "Internal server error: " + errD.Error(), http.StatusInternalServerError, "DS002")
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
containerID := utils.SanitizeSafe(vars["containerId"])
|
||||
|
||||
if containerID == "" {
|
||||
utils.Error("containerID is required: ", nil)
|
||||
http.Error(w, "containerID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
action := utils.Sanitize(vars["action"])
|
||||
|
||||
utils.Log("Attaching container " + containerID + " to websocket")
|
||||
|
||||
var resp types.HijackedResponse
|
||||
|
||||
if action == "new" {
|
||||
execConfig := types.ExecConfig{
|
||||
Tty: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Cmd: []string{"/bin/sh"},
|
||||
}
|
||||
|
||||
execStart := types.ExecStartCheck{
|
||||
Tty: true,
|
||||
}
|
||||
|
||||
execResp, errExec := DockerClient.ContainerExecCreate(ctx, containerID, execConfig)
|
||||
if errExec != nil {
|
||||
utils.Error("ContainerExecCreate failed: ", errExec)
|
||||
http.Error(w, "ContainerExecCreate failed: "+errExec.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err = DockerClient.ContainerExecAttach(ctx, execResp.ID, execStart)
|
||||
if err != nil {
|
||||
utils.Error("ContainerExecAttach failed: ", err)
|
||||
http.Error(w, "ContainerExecAttach failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log("Created new shell and attached to it in container " + containerID)
|
||||
} else {
|
||||
options := types.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: true,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
}
|
||||
|
||||
// Attach to the container
|
||||
resp, err = DockerClient.ContainerAttach(ctx, containerID, options)
|
||||
if err != nil {
|
||||
utils.Error("ContainerAttach failed: ", err)
|
||||
http.Error(w, "ContainerAttach failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log("Attached to existing process in container " + containerID)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
utils.Log("Attached container " + containerID + " to websocket")
|
||||
|
||||
var WSChan = make(chan []byte, 1024*1024*4)
|
||||
var DockerChan = make(chan []byte, 1024*1024*4)
|
||||
|
||||
// Start a goroutine to read from our websocket and write to the container
|
||||
go (func() {
|
||||
for {
|
||||
utils.Debug("Waiting for message from websocket")
|
||||
_, message, err := ws.ReadMessage()
|
||||
utils.Debug("Got message from websocket")
|
||||
if err != nil {
|
||||
utils.Error("Failed to read from websocket: ", err)
|
||||
break
|
||||
}
|
||||
WSChan <- []byte((string)(message) + "\n")
|
||||
}
|
||||
})()
|
||||
|
||||
// Start a goroutine to read from the container and write to our websocket
|
||||
go (func() {
|
||||
for {
|
||||
buf := make([]byte, 1024*1024*4)
|
||||
utils.Debug("Waiting for message from container")
|
||||
n, err := resp.Reader.Read(buf)
|
||||
utils.Debug("Got message from container")
|
||||
if err != nil {
|
||||
utils.Error("Failed to read from container: ", err)
|
||||
break
|
||||
}
|
||||
DockerChan <- buf[:n]
|
||||
}
|
||||
})()
|
||||
|
||||
for {
|
||||
select {
|
||||
case message := <-WSChan:
|
||||
utils.Debug("Writing message to container")
|
||||
_, err := resp.Conn.Write(message)
|
||||
if err != nil {
|
||||
utils.Error("Failed to write to container: ", err)
|
||||
return
|
||||
}
|
||||
utils.Debug("Wrote message to container")
|
||||
case message := <-DockerChan:
|
||||
utils.Debug("Writing message to websocket")
|
||||
|
||||
messages := splitIntoChunks(string(message), 5)
|
||||
for _, messageSplit := range messages {
|
||||
messageJSON, err := json.Marshal(messageSplit)
|
||||
if err != nil {
|
||||
utils.Error("Failed to marshal message: ", err)
|
||||
return
|
||||
}
|
||||
err = ws.WriteMessage(websocket.TextMessage, messageJSON)
|
||||
if err != nil {
|
||||
utils.Error("Failed to write to websocket: ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
utils.Debug("Wrote message to websocket")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@ type ContainerForm struct {
|
|||
Labels map[string]string `json:"labels"`
|
||||
PortBindings nat.PortMap `json:"portBindings"`
|
||||
Volumes []mount.Mount `json:"Volumes"`
|
||||
// we make this a int so that we can ignore 0
|
||||
Interactive int `json:"interactive"`
|
||||
}
|
||||
|
||||
func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -84,6 +86,10 @@ func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
|
|||
container.HostConfig.Mounts = form.Volumes
|
||||
container.HostConfig.Binds = []string{}
|
||||
}
|
||||
if(form.Interactive != 0) {
|
||||
container.Config.Tty = form.Interactive == 2
|
||||
container.Config.OpenStdin = form.Interactive == 2
|
||||
}
|
||||
|
||||
_, err = EditContainer(container.ID, container)
|
||||
if err != nil {
|
||||
|
|
|
@ -227,11 +227,13 @@ func StartServer() {
|
|||
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}/terminal/{action}", docker.TerminalRoute)
|
||||
srapi.HandleFunc("/api/servapps/{containerId}/update", docker.UpdateContainerRoute)
|
||||
srapi.HandleFunc("/api/servapps/{containerId}/", docker.GetContainerRoute)
|
||||
srapi.HandleFunc("/api/servapps/{containerId}/network/{networkId}", docker.NetworkContainerRoutes)
|
||||
srapi.HandleFunc("/api/servapps/{containerId}/networks", docker.NetworkContainerRoutes)
|
||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||
|
||||
|
||||
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
srapi.Use(utils.EnsureHostname)
|
||||
|
|
|
@ -14,6 +14,7 @@ export default defineConfig({
|
|||
'/cosmos/api': {
|
||||
target: 'https://localhost:8443',
|
||||
secure: false,
|
||||
ws: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue