[release] version 0.5.0-unstable6

This commit is contained in:
Yann Stepienik 2023-05-14 15:48:15 +01:00
parent 1ffbb8b39b
commit 5545163768
8 changed files with 366 additions and 8 deletions

View file

@ -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 { export {
list, list,
get, get,
@ -240,4 +288,5 @@ export {
attachTerminal, attachTerminal,
createTerminal, createTerminal,
createService, createService,
pullImage,
}; };

View 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;

View file

@ -7,6 +7,7 @@ import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect }
import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons';
import * as API from '../../../api'; import * as API from '../../../api';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import LogsInModal from '../../../components/logsInModal';
const containerInfoFrom = (values) => { const containerInfoFrom = (values) => {
const labels = {}; const labels = {};
@ -33,9 +34,11 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
['on-failure', 'Restart On Failure'], ['on-failure', 'Restart On Failure'],
['unless-stopped', 'Restart Unless Stopped'], ['unless-stopped', 'Restart Unless Stopped'],
]; ];
const [pullRequest, setPullRequest] = React.useState(null);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const padding = isMobile ? '6px 4px' : '12px 10px'; const padding = isMobile ? '6px 4px' : '12px 10px';
const [latestImage, setLatestImage] = React.useState(containerInfo.Config.Image);
return ( return (
<div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}> <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
@ -73,6 +76,11 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
return errors; return errors;
}} }}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
if(values.image !== latestImage) {
setPullRequest(() => ((cb) => API.docker.pullImage(values.image,cb, true)));
return;
}
if(newContainer) return false; if(newContainer) return false;
delete values.name; delete values.name;
@ -95,6 +103,14 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
> >
{(formik) => ( {(formik) => (
<form noValidate onSubmit={formik.handleSubmit}> <form noValidate onSubmit={formik.handleSubmit}>
<LogsInModal
request={pullRequest}
title="Pulling New Image..."
OnSuccess={() => {
setPullRequest(null);
setLatestImage(formik.values.image);
}}
/>
<Stack spacing={2}> <Stack spacing={2}>
<MainCard title={'Docker Container Setup'}> <MainCard title={'Docker Container Setup'}>
{containerInfo.State && containerInfo.State.Status !== 'running' && ( {containerInfo.State && containerInfo.State.Status !== 'running' && (
@ -268,6 +284,9 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
<FormHelperText error>{formik.errors.submit}</FormHelperText> <FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid> </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 <LoadingButton
fullWidth fullWidth
disableElevation disableElevation
@ -278,7 +297,7 @@ const DockerContainerSetup = ({config, containerInfo, OnChange, refresh, newCont
variant="contained" variant="contained"
color="primary" color="primary"
> >
Update {formik.values.image !== latestImage ? 'Pull New Image' : 'Update Container'}
</LoadingButton> </LoadingButton>
</Stack> </Stack>
</MainCard>} </MainCard>}

View file

@ -1,6 +1,6 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.5.0-unstable5", "version": "0.5.0-unstable6",
"description": "", "description": "",
"main": "test-server.js", "main": "test-server.js",
"bugs": { "bugs": {

View file

@ -6,7 +6,6 @@ import (
"github.com/azukaar/cosmos-server/src/utils" "github.com/azukaar/cosmos-server/src/utils"
"github.com/gorilla/mux"
) )
func InspectImageRoute(w http.ResponseWriter, req *http.Request) { func InspectImageRoute(w http.ResponseWriter, req *http.Request) {
@ -21,8 +20,7 @@ func InspectImageRoute(w http.ResponseWriter, req *http.Request) {
return return
} }
vars := mux.Vars(req) imageName := utils.SanitizeSafe(req.URL.Query().Get("imageName"))
imageName := utils.SanitizeSafe(vars["imageName"])
utils.Log("InspectImage " + imageName) utils.Log("InspectImage " + imageName)

182
src/docker/api_update.go Normal file
View 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
}
}

View file

@ -5,6 +5,7 @@ import (
"errors" "errors"
"time" "time"
"fmt" "fmt"
"bufio"
"github.com/azukaar/cosmos-server/src/utils" "github.com/azukaar/cosmos-server/src/utils"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -116,6 +117,24 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string
return "", err 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 // if no name, use the same one, that will force Docker to create a hostname if not set
newName = oldContainer.Name newName = oldContainer.Name
newConfig.Config.Hostname = newName newConfig.Config.Hostname = newName
@ -147,7 +166,7 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON) (string
} else { } else {
utils.Log("EditContainer - Revert started") utils.Log("EditContainer - Revert started")
} }
// recreate container with new informations // recreate container with new informations
createResponse, createError := DockerClient.ContainerCreate( createResponse, createError := DockerClient.ContainerCreate(
DockerContext, DockerContext,

View file

@ -218,7 +218,9 @@ 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/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/volume/{volumeName}", docker.DeleteVolumeRoute)
srapi.HandleFunc("/api/volumes", docker.VolumesRoute) srapi.HandleFunc("/api/volumes", docker.VolumesRoute)
@ -234,6 +236,7 @@ func StartServer() {
srapi.HandleFunc("/api/servapps/{containerId}/", docker.GetContainerRoute) srapi.HandleFunc("/api/servapps/{containerId}/", docker.GetContainerRoute)
srapi.HandleFunc("/api/servapps/{containerId}/network/{networkId}", docker.NetworkContainerRoutes) srapi.HandleFunc("/api/servapps/{containerId}/network/{networkId}", docker.NetworkContainerRoutes)
srapi.HandleFunc("/api/servapps/{containerId}/networks", 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/servapps", docker.ContainersRoute)
srapi.HandleFunc("/api/docker-service", docker.CreateServiceRoute) srapi.HandleFunc("/api/docker-service", docker.CreateServiceRoute)
@ -250,7 +253,7 @@ func StartServer() {
PerUserRequestLimit: 5000, PerUserRequestLimit: 5000,
}, },
)) ))
srapi.Use(utils.MiddlewareTimeout(30 * time.Second)) srapi.Use(utils.MiddlewareTimeout(45 * time.Second))
srapi.Use(utils.BlockPostWithoutReferer) srapi.Use(utils.BlockPostWithoutReferer)
srapi.Use(proxy.BotDetectionMiddleware) srapi.Use(proxy.BotDetectionMiddleware)
srapi.Use(httprate.Limit(120, 1*time.Minute, srapi.Use(httprate.Limit(120, 1*time.Minute,