[release] version 0.5.0-unstable6
This commit is contained in:
parent
1ffbb8b39b
commit
5545163768
|
@ -220,6 +220,54 @@ function createService(serviceData, onProgress) {
|
|||
});
|
||||
}
|
||||
|
||||
function pullImage(imageName, onProgress, ifMissing) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
};
|
||||
|
||||
const imageNameEncoded = encodeURIComponent(imageName);
|
||||
|
||||
return fetch(`/cosmos/api/images/${ifMissing ? 'pull-if-missing' : 'pull'}?imageName=${imageNameEncoded}`, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
|
||||
const reader = response.body.getReader();
|
||||
|
||||
// Read the stream and pass chunks to the callback as they arrive
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
function read() {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Decode the UTF-8 text
|
||||
let text = new TextDecoder().decode(value);
|
||||
// Split by lines in case there are multiple lines in one chunk
|
||||
let lines = text.split('\n');
|
||||
for (let line of lines) {
|
||||
if (line) {
|
||||
// Call the progress callback
|
||||
onProgress(line);
|
||||
}
|
||||
}
|
||||
controller.enqueue(value);
|
||||
return read();
|
||||
});
|
||||
}
|
||||
return read();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
get,
|
||||
|
@ -240,4 +288,5 @@ export {
|
|||
attachTerminal,
|
||||
createTerminal,
|
||||
createService,
|
||||
pullImage,
|
||||
};
|
88
client/src/components/logsInModal.jsx
Normal file
88
client/src/components/logsInModal.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
// material-ui
|
||||
import * as React from 'react';
|
||||
import { Alert, Button, Stack, Typography } from '@mui/material';
|
||||
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined } from '@ant-design/icons';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { smartDockerLogConcat, tryParseProgressLog } from '../utils/docker';
|
||||
|
||||
const preStyle = {
|
||||
backgroundColor: '#000',
|
||||
color: '#fff',
|
||||
padding: '10px',
|
||||
borderRadius: '5px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '500px',
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
margin: '0',
|
||||
position: 'relative',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
wordBreak: 'break-all',
|
||||
lineHeight: '1.5',
|
||||
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '10px',
|
||||
marginTop: '10px',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
}
|
||||
|
||||
const LogsInModal = ({title, request, OnSuccess, OnError, closeAnytime, initialLogs = [], }) => {
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [logs, setLogs] = useState(initialLogs);
|
||||
const [done, setDone] = useState(closeAnytime);
|
||||
const preRef = React.useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLogs(initialLogs);
|
||||
setDone(closeAnytime);
|
||||
|
||||
if(request === null) return;
|
||||
|
||||
request((newlog) => {
|
||||
setLogs((old) => smartDockerLogConcat(old, newlog));
|
||||
|
||||
if(preRef.current)
|
||||
preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||
|
||||
if (newlog.includes('[OPERATION SUCCEEDED]')) {
|
||||
setDone(true);
|
||||
setOpenModal(false);
|
||||
OnSuccess && OnSuccess();
|
||||
} else if (newlog.includes('[OPERATION FAILED]')) {
|
||||
setDone(true);
|
||||
OnError && OnError(newlog);
|
||||
} else {
|
||||
setOpenModal(true);
|
||||
}
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
return <>
|
||||
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<pre style={preStyle} ref={preRef}>
|
||||
{logs.map((l) => {
|
||||
return <div>{tryParseProgressLog(l)}</div>
|
||||
})}
|
||||
</pre>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default LogsInModal;
|
|
@ -7,6 +7,7 @@ import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect }
|
|||
import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
||||
import * as API from '../../../api';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import LogsInModal from '../../../components/logsInModal';
|
||||
|
||||
const containerInfoFrom = (values) => {
|
||||
const labels = {};
|
||||
|
@ -33,9 +34,11 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
|
|||
['on-failure', 'Restart On Failure'],
|
||||
['unless-stopped', 'Restart Unless Stopped'],
|
||||
];
|
||||
const [pullRequest, setPullRequest] = React.useState(null);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const padding = isMobile ? '6px 4px' : '12px 10px';
|
||||
const [latestImage, setLatestImage] = React.useState(containerInfo.Config.Image);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||
|
@ -73,6 +76,11 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
|
|||
return errors;
|
||||
}}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
if(values.image !== latestImage) {
|
||||
setPullRequest(() => ((cb) => API.docker.pullImage(values.image,cb, true)));
|
||||
return;
|
||||
}
|
||||
|
||||
if(newContainer) return false;
|
||||
delete values.name;
|
||||
|
||||
|
@ -95,6 +103,14 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
|
|||
>
|
||||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<LogsInModal
|
||||
request={pullRequest}
|
||||
title="Pulling New Image..."
|
||||
OnSuccess={() => {
|
||||
setPullRequest(null);
|
||||
setLatestImage(formik.values.image);
|
||||
}}
|
||||
/>
|
||||
<Stack spacing={2}>
|
||||
<MainCard title={'Docker Container Setup'}>
|
||||
{containerInfo.State && containerInfo.State.Status !== 'running' && (
|
||||
|
@ -268,6 +284,9 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
|
|||
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||
</Grid>
|
||||
)}
|
||||
{formik.values.image !== latestImage && <Alert severity="warning" style={{ marginBottom: '15px' }}>
|
||||
You have updated the image. Clicking the button below will pull the new image, and then only can you update the container.
|
||||
</Alert>}
|
||||
<LoadingButton
|
||||
fullWidth
|
||||
disableElevation
|
||||
|
@ -278,7 +297,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
|
|||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Update
|
||||
{formik.values.image !== latestImage ? 'Pull New Image' : 'Update Container'}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</MainCard>}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.5.0-unstable5",
|
||||
"version": "0.5.0-unstable6",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func InspectImageRoute(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -21,8 +20,7 @@ func InspectImageRoute(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
imageName := utils.SanitizeSafe(vars["imageName"])
|
||||
imageName := utils.SanitizeSafe(req.URL.Query().Get("imageName"))
|
||||
|
||||
utils.Log("InspectImage " + imageName)
|
||||
|
||||
|
|
182
src/docker/api_update.go
Normal file
182
src/docker/api_update.go
Normal file
|
@ -0,0 +1,182 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"bufio"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
func PullImageIfMissing(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
imageName := utils.SanitizeSafe(req.URL.Query().Get("imageName"))
|
||||
|
||||
if req.Method == "GET" {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("PullImageIfMissing", errD)
|
||||
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||
return
|
||||
}
|
||||
|
||||
// Enable streaming of response by setting appropriate headers
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, errImage := DockerClient.ImageInspectWithRaw(DockerContext, imageName)
|
||||
if errImage != nil {
|
||||
utils.Log("PullImageIfMissing - Image not found, pulling " + imageName)
|
||||
fmt.Fprintf(w, "PullImageIfMissing - Image not found, pulling " + imageName + "\n")
|
||||
flusher.Flush()
|
||||
out, errPull := DockerClient.ImagePull(DockerContext, imageName, types.ImagePullOptions{})
|
||||
if errPull != nil {
|
||||
utils.Error("PullImageIfMissing - Image not found.", errPull)
|
||||
fmt.Fprintf(w, "[OPERATION FAILED] PullImageIfMissing - Image not found. " + errPull.Error() + "\n")
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// wait for image pull to finish
|
||||
scanner := bufio.NewScanner(out)
|
||||
for scanner.Scan() {
|
||||
utils.Log(scanner.Text())
|
||||
fmt.Fprintf(w, scanner.Text() + "\n")
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
utils.Log("PullImageIfMissing - Image pulled " + imageName)
|
||||
fmt.Fprintf(w, "[OPERATION SUCCEEDED]")
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log("PullImageIfMissing - Image found, skipping " + imageName)
|
||||
fmt.Fprintf(w, "[OPERATION SUCCEEDED]")
|
||||
flusher.Flush()
|
||||
} else {
|
||||
utils.Error("PullImageIfMissing: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func PullImage(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
imageName := utils.SanitizeSafe(req.URL.Query().Get("imageName"))
|
||||
|
||||
if req.Method == "GET" {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("PullImageIfMissing", errD)
|
||||
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||
return
|
||||
}
|
||||
|
||||
// Enable streaming of response by setting appropriate headers
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log("PullImageIfMissing - Image not found, pulling " + imageName)
|
||||
fmt.Fprintf(w, "PullImageIfMissing - Image not found, pulling " + imageName + "\n")
|
||||
flusher.Flush()
|
||||
out, errPull := DockerClient.ImagePull(DockerContext, imageName, types.ImagePullOptions{})
|
||||
if errPull != nil {
|
||||
utils.Error("PullImageIfMissing - Image not found.", errPull)
|
||||
fmt.Fprintf(w, "[OPERATION FAILED] PullImageIfMissing - Image not found. " + errPull.Error() + "\n")
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// wait for image pull to finish
|
||||
scanner := bufio.NewScanner(out)
|
||||
for scanner.Scan() {
|
||||
utils.Log(scanner.Text())
|
||||
fmt.Fprintf(w, scanner.Text() + "\n")
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
utils.Log("PullImageIfMissing - Image pulled " + imageName)
|
||||
fmt.Fprintf(w, "[OPERATION SUCCEEDED]")
|
||||
flusher.Flush()
|
||||
return
|
||||
} else {
|
||||
utils.Error("PullImageIfMissing: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func CanUpdateImageRoute(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(req)
|
||||
containerId := vars["containerId"]
|
||||
|
||||
if req.Method == "GET" {
|
||||
errD := Connect()
|
||||
if errD != nil {
|
||||
utils.Error("CanUpdateImageRoute", errD)
|
||||
utils.HTTPError(w, "Internal server error: "+errD.Error(), http.StatusInternalServerError, "LN001")
|
||||
return
|
||||
}
|
||||
|
||||
// get Docker container
|
||||
container, err := DockerClient.ContainerInspect(context.Background(), containerId)
|
||||
if err != nil {
|
||||
utils.Error("CanUpdateImageRoute: Error while getting container", err)
|
||||
utils.HTTPError(w, "Container Get Error: " + err.Error(), http.StatusInternalServerError, "LN002")
|
||||
return
|
||||
}
|
||||
|
||||
// check if the container's image can be updated
|
||||
canUpdate := false
|
||||
imageName := container.Image
|
||||
image, _, err := DockerClient.ImageInspectWithRaw(context.Background(), imageName)
|
||||
if err != nil {
|
||||
utils.Error("CanUpdateImageRoute: Error while inspecting image", err)
|
||||
utils.HTTPError(w, "Image Inspection Error: " + err.Error(), http.StatusInternalServerError, "LN003")
|
||||
return
|
||||
}
|
||||
if len(image.RepoDigests) > 0 {
|
||||
canUpdate = true
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": canUpdate,
|
||||
})
|
||||
} else {
|
||||
utils.Error("CanUpdateImageRoute: Method not allowed " + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
"fmt"
|
||||
"bufio"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
@ -116,6 +117,24 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string
|
|||
return "", err
|
||||
}
|
||||
|
||||
// check if new image exists, if not, pull it
|
||||
_, _, errImage := DockerClient.ImageInspectWithRaw(DockerContext, newConfig.Config.Image)
|
||||
if errImage != nil {
|
||||
utils.Log("EditContainer - Image not found, pulling " + newConfig.Config.Image)
|
||||
out, errPull := DockerClient.ImagePull(DockerContext, newConfig.Config.Image, types.ImagePullOptions{})
|
||||
if errPull != nil {
|
||||
utils.Error("EditContainer - Image not found.", errPull)
|
||||
return "", errors.New("Image not found. " + errPull.Error())
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// wait for image pull to finish
|
||||
scanner := bufio.NewScanner(out)
|
||||
for scanner.Scan() {
|
||||
utils.Log(scanner.Text())
|
||||
}
|
||||
}
|
||||
|
||||
// if no name, use the same one, that will force Docker to create a hostname if not set
|
||||
newName = oldContainer.Name
|
||||
newConfig.Config.Hostname = newName
|
||||
|
|
|
@ -218,7 +218,9 @@ func StartServer() {
|
|||
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
|
||||
srapi.HandleFunc("/api/users", user.UsersRoute)
|
||||
|
||||
srapi.HandleFunc("/api/images/{imageName}", docker.InspectImageRoute)
|
||||
srapi.HandleFunc("/api/images/pull-if-missing", docker.PullImageIfMissing)
|
||||
srapi.HandleFunc("/api/images/pull", docker.PullImage)
|
||||
srapi.HandleFunc("/api/images", docker.InspectImageRoute)
|
||||
|
||||
srapi.HandleFunc("/api/volume/{volumeName}", docker.DeleteVolumeRoute)
|
||||
srapi.HandleFunc("/api/volumes", docker.VolumesRoute)
|
||||
|
@ -234,6 +236,7 @@ func StartServer() {
|
|||
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/{containerId}/check-update", docker.CanUpdateImageRoute)
|
||||
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
|
||||
|
||||
srapi.HandleFunc("/api/docker-service", docker.CreateServiceRoute)
|
||||
|
@ -250,7 +253,7 @@ func StartServer() {
|
|||
PerUserRequestLimit: 5000,
|
||||
},
|
||||
))
|
||||
srapi.Use(utils.MiddlewareTimeout(30 * time.Second))
|
||||
srapi.Use(utils.MiddlewareTimeout(45 * time.Second))
|
||||
srapi.Use(utils.BlockPostWithoutReferer)
|
||||
srapi.Use(proxy.BotDetectionMiddleware)
|
||||
srapi.Use(httprate.Limit(120, 1*time.Minute,
|
||||
|
|
Loading…
Reference in a new issue