mirror of
https://github.com/amir20/dozzle.git
synced 2026-01-03 11:35:00 +01:00
feat: supports downloading a group of containers in a zip file (#3490)
This commit is contained in:
100
internal/web/download.go
Normal file
100
internal/web/download.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
hostIds := strings.Split(chi.URLParam(r, "hostIds"), ",")
|
||||
if len(hostIds) == 0 {
|
||||
http.Error(w, "no container ids provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
usersFilter := h.config.Filter
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerFilter.Exists() {
|
||||
usersFilter = user.ContainerFilter
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
nowFmt := now.Format("2006-01-02T15-04-05")
|
||||
|
||||
var stdTypes docker.StdType
|
||||
if r.URL.Query().Has("stdout") {
|
||||
stdTypes |= docker.STDOUT
|
||||
}
|
||||
if r.URL.Query().Has("stderr") {
|
||||
stdTypes |= docker.STDERR
|
||||
}
|
||||
|
||||
if stdTypes == 0 {
|
||||
http.Error(w, "stdout or stderr is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Set headers for zip file
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=container-logs-%s.zip", nowFmt))
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
|
||||
// Create zip writer
|
||||
zw := zip.NewWriter(w)
|
||||
defer zw.Close()
|
||||
|
||||
// Process each container
|
||||
for _, hostId := range hostIds {
|
||||
parts := strings.Split(hostId, ":")
|
||||
if len(parts) != 2 {
|
||||
http.Error(w, fmt.Sprintf("invalid host id: %s", hostId), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
host := parts[0]
|
||||
id := parts[1]
|
||||
containerService, err := h.multiHostService.FindContainer(host, id, usersFilter)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error finding container %s: %v", id, err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new file in zip for this container's logs
|
||||
fileName := fmt.Sprintf("%s-%s.log", containerService.Container.Name, nowFmt)
|
||||
f, err := zw.Create(fileName)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error creating zip entry: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get container logs
|
||||
reader, err := containerService.RawLogs(r.Context(), time.Time{}, now, stdTypes)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("error getting logs for container %s: %v", id, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy logs directly to zip entry
|
||||
if containerService.Container.Tty {
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err := stdcopy.StdCopy(f, f, reader); err != nil {
|
||||
http.Error(w, fmt.Sprintf("error copying logs for container %s: %v", id, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
@@ -11,14 +10,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/beme/abide"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_handler_download_logs(t *testing.T) {
|
||||
id := "123456"
|
||||
req, err := http.NewRequest("GET", "/api/hosts/localhost/containers/"+id+"/logs/download?stdout=1", nil)
|
||||
req, err := http.NewRequest("GET", "/api/containers/localhost:"+id+"/download?stdout=1", nil)
|
||||
require.NoError(t, err, "NewRequest should not return an error.")
|
||||
|
||||
mockedClient := new(MockedClient)
|
||||
@@ -40,7 +38,6 @@ func Test_handler_download_logs(t *testing.T) {
|
||||
handler := createDefaultHandler(mockedClient)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
reader, _ := gzip.NewReader(rr.Body)
|
||||
abide.AssertReader(t, t.Name(), reader)
|
||||
require.Equal(t, http.StatusOK, rr.Code, "Status code should be 200.")
|
||||
mockedClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
@@ -23,75 +21,12 @@ import (
|
||||
"github.com/amir20/dozzle/internal/support/search"
|
||||
support_web "github.com/amir20/dozzle/internal/support/web"
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
usersFilter := h.config.Filter
|
||||
if h.config.Authorization.Provider != NONE {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user.ContainerFilter.Exists() {
|
||||
usersFilter = user.ContainerFilter
|
||||
}
|
||||
}
|
||||
|
||||
containerService, err := h.multiHostService.FindContainer(hostKey(r), id, usersFilter)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
nowFmt := now.Format("2006-01-02T15-04-05")
|
||||
|
||||
contentDisposition := fmt.Sprintf("attachment; filename=%s-%s.log", containerService.Container.Name, nowFmt)
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Disposition", contentDisposition)
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Set("Content-Type", "application/text")
|
||||
} else {
|
||||
w.Header().Set("Content-Disposition", contentDisposition+".gz")
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
}
|
||||
|
||||
var stdTypes docker.StdType
|
||||
if r.URL.Query().Has("stdout") {
|
||||
stdTypes |= docker.STDOUT
|
||||
}
|
||||
if r.URL.Query().Has("stderr") {
|
||||
stdTypes |= docker.STDERR
|
||||
}
|
||||
|
||||
if stdTypes == 0 {
|
||||
http.Error(w, "stdout or stderr is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
zw := gzip.NewWriter(w)
|
||||
defer zw.Close()
|
||||
zw.Name = fmt.Sprintf("%s-%s.log", containerService.Container.Name, nowFmt)
|
||||
zw.Comment = "Logs generated by Dozzle"
|
||||
zw.ModTime = now
|
||||
|
||||
reader, err := containerService.RawLogs(r.Context(), time.Time{}, now, stdTypes)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if containerService.Container.Tty {
|
||||
io.Copy(zw, reader)
|
||||
} else {
|
||||
stdcopy.StdCopy(zw, zw, reader)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/x-jsonl; charset=UTF-8")
|
||||
|
||||
|
||||
@@ -94,9 +94,9 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Use(auth.RequireAuthentication)
|
||||
}
|
||||
r.Get("/hosts/{host}/containers/{id}/logs/stream", h.streamContainerLogs)
|
||||
r.Get("/hosts/{host}/containers/{id}/logs/download", h.downloadLogs)
|
||||
r.Get("/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates)
|
||||
r.Get("/hosts/{host}/logs/mergedStream/{ids}", h.streamLogsMerged)
|
||||
r.Get("/containers/{hostIds}/download", h.downloadLogs) // formatted as host:container,host:container
|
||||
r.Get("/stacks/{stack}/logs/stream", h.streamStackLogs)
|
||||
r.Get("/services/{service}/logs/stream", h.streamServiceLogs)
|
||||
r.Get("/groups/{group}/logs/stream", h.streamGroupedLogs)
|
||||
|
||||
Reference in New Issue
Block a user