diff --git a/.github/workflows/build-scan-push.yaml b/.github/workflows/build-scan-push.yaml index fd89c15..2247fc1 100644 --- a/.github/workflows/build-scan-push.yaml +++ b/.github/workflows/build-scan-push.yaml @@ -77,6 +77,11 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64 + build-args: | + BUGSNAG_RELEASE_STAGE="" + BUGSNAG_APP_VERSION="" + secrets: | + BUGSNAG_API_KEY="" - name: Run Trivy for all CVEs (non-blocking) uses: aquasecurity/trivy-action@master @@ -123,6 +128,7 @@ jobs: org.opencontainers.image.title=Volumes Backup & Share org.opencontainers.image.description=Back up, clone, restore, and share Docker volumes effortlessly. org.opencontainers.image.vendor=Docker Inc. + - name: Docker Build and Push to Docker Hub if: ${{ !github.event.pull_request.head.repo.fork }} uses: docker/build-push-action@v2 @@ -133,6 +139,12 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64,linux/arm64 + build-args: | + BUGSNAG_RELEASE_STAGE=production + BUGSNAG_APP_VERSION=${{ github.event.release.tag_name }} + secrets: | + BUGSNAG_API_KEY=${{ secrets.BUGSNAG_API_KEY }} + # If PR, put image tags in the PR comments # from https://github.com/marketplace/actions/create-or-update-comment - name: Find comment for image tags diff --git a/.github/workflows/hadolint.yaml b/.github/workflows/hadolint.yaml index 74ecec4..d6c30fa 100644 --- a/.github/workflows/hadolint.yaml +++ b/.github/workflows/hadolint.yaml @@ -13,4 +13,4 @@ jobs: - uses: hadolint/hadolint-action@v2.0.0 with: dockerfile: Dockerfile - ignore: DL3048,DL3025 + ignore: DL3048,DL3025,DL3018 diff --git a/Dockerfile b/Dockerfile index c477ebc..ab827f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,18 @@ RUN --mount=type=cache,target=/usr/src/app/.npm \ npm ci # install COPY ui /ui -RUN npm run build +RUN --mount=type=secret,id=BUGSNAG_API_KEY \ + REACT_APP_BUGSNAG_API_KEY=$(cat /run/secrets/BUGSNAG_API_KEY) \ + npm run build + +FROM alpine:3.16 as base +ARG CLI_VERSION=20.10.17 +SHELL ["/bin/ash", "-eo", "pipefail", "-c"] +RUN apk update \ + && apk add --no-cache ca-certificates curl \ + && rm -rf /var/cache/apk/* +RUN curl -fL "https://download.docker.com/linux/static/stable/$(uname -m)/docker-${CLI_VERSION}.tgz" | tar zxf - --strip-components 1 docker/docker \ + && chmod +x /docker FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS docker-credentials-client-builder ENV CGO_ENABLED=0 @@ -32,6 +43,13 @@ COPY client . RUN make cross FROM busybox:1.35.0 + +ARG BUGSNAG_RELEASE_STAGE="local" +ARG BUGSNAG_APP_VERSION="latest" + +ENV BUGSNAG_RELEASE_STAGE=$BUGSNAG_RELEASE_STAGE +ENV BUGSNAG_APP_VERSION=$BUGSNAG_APP_VERSION + LABEL org.opencontainers.image.title="Volumes Backup & Share" \ org.opencontainers.image.description="Back up, clone, restore, and share Docker volumes effortlessly." \ org.opencontainers.image.vendor="Docker Inc." \ @@ -76,10 +94,16 @@ WORKDIR / COPY docker-compose.yaml . COPY metadata.json . COPY icon.svg . +COPY --from=base /etc/ssl/certs /etc/ssl/certs +COPY --from=base /docker /usr/local/bin/docker COPY --from=builder /backend/bin/service / COPY --from=client-builder /ui/build ui COPY --from=docker-credentials-client-builder output/dist ./host RUN mkdir -p /vackup -CMD /service -socket /run/guest-services/ext.sock \ No newline at end of file +RUN --mount=type=secret,id=BUGSNAG_API_KEY \ + BUGSNAG_API_KEY=$(cat /run/secrets/BUGSNAG_API_KEY); \ + echo "$BUGSNAG_API_KEY" > /tmp/bugsnag-api-key.txt + +ENTRYPOINT ["/bin/sh", "-c", "BUGSNAG_API_KEY=$(cat /tmp/bugsnag-api-key.txt); rm -rf /tmp/bugsnag-api-key.txt; BUGSNAG_API_KEY=$BUGSNAG_API_KEY /service -socket /run/guest-services/ext.sock"] diff --git a/Makefile b/Makefile index 0cc0ad6..9222817 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,21 @@ IMAGE?=docker/volumes-backup-extension TAG?=latest - BUILDER=buildx-multi-arch +export BUGSNAG_API_KEY?= +export BUGSNAG_RELEASE_STAGE?=local + INFO_COLOR = \033[0;36m NO_COLOR = \033[m build-extension: ## Build service image to be deployed as a desktop extension - docker buildx build --load --tag=$(IMAGE):$(TAG) . + docker buildx build \ + --secret id=BUGSNAG_API_KEY \ + --build-arg BUGSNAG_RELEASE_STAGE=$(BUGSNAG_RELEASE_STAGE) \ + --build-arg BUGSNAG_APP_VERSION=$(TAG) \ + --load \ + --tag=$(IMAGE):$(TAG) \ + . install-extension: build-extension ## Install the extension docker extension install $(IMAGE):$(TAG) @@ -26,7 +34,7 @@ prepare-buildx: ## Create buildx builder for multi-arch build, if not exists docker buildx inspect $(BUILDER) || docker buildx create --name=$(BUILDER) --driver=docker-container --driver-opt=network=host push-extension: prepare-buildx ## Build & Upload extension image to hub. Do not push if tag already exists: make push-extension tag=0.1 - docker pull $(IMAGE):$(TAG) && echo "Failure: Tag already exists" || docker buildx build --push --builder=$(BUILDER) --platform=linux/amd64,linux/arm64 --build-arg TAG=$(TAG) --tag=$(IMAGE):$(TAG) . + docker pull $(IMAGE):$(TAG) && echo "Failure: Tag already exists" || docker buildx build --secret id=BUGSNAG_API_KEY --build-arg BUGSNAG_RELEASE_STAGE=$(BUGSNAG_RELEASE_STAGE) --build-arg BUGSNAG_APP_VERSION=$(TAG) --push --builder=$(BUILDER) --platform=linux/amd64,linux/arm64 --build-arg TAG=$(TAG) --tag=$(IMAGE):$(TAG) . help: ## Show this help @echo Please specify a build target. The choices are: diff --git a/ui/.env b/ui/.env index b0f5823..b8f6a3d 100644 --- a/ui/.env +++ b/ui/.env @@ -1,2 +1,3 @@ PUBLIC_URL=. BROWSER=none +REACT_APP_BUGSNAG_API_KEY= \ No newline at end of file diff --git a/vm/go.mod b/vm/go.mod index b214163..3fb9d45 100644 --- a/vm/go.mod +++ b/vm/go.mod @@ -17,6 +17,8 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bugsnag/bugsnag-go/v2 v2.1.2 // indirect + github.com/bugsnag/panicwrap v1.3.4 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect @@ -24,9 +26,11 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect + github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.4.3 // indirect github.com/gorilla/mux v1.8.0 // indirect + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/labstack/gommon v0.3.1 // indirect github.com/mattn/go-colorable v0.1.11 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/vm/go.sum b/vm/go.sum index 4811c65..d812553 100644 --- a/vm/go.sum +++ b/vm/go.sum @@ -12,6 +12,12 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bugsnag/bugsnag-go/v2 v2.1.2 h1:R5rgYn5w5LhVIere+n+Ah49pa4E1b3OP2bSwHtURmv8= +github.com/bugsnag/bugsnag-go/v2 v2.1.2/go.mod h1:mJCnw33SPVYPFTsAeSR/kpwuyvjTXrPK5w/XZfTUwMU= +github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= +github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= @@ -45,6 +51,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -75,6 +83,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -82,6 +92,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= diff --git a/vm/internal/backend/auth.go b/vm/internal/backend/auth.go deleted file mode 100644 index d4a6d92..0000000 --- a/vm/internal/backend/auth.go +++ /dev/null @@ -1,57 +0,0 @@ -package backend - -import ( - "context" - "encoding/base64" - "encoding/json" - "github.com/docker/cli/cli/config" - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/client" - "github.com/docker/docker/registry" - "os" -) - -func ResolveAuthConfig(ctx context.Context, index *registrytypes.IndexInfo) (types.AuthConfig, error) { - configKey := index.Name - - if index.Official { - cfgKey, err := electAuthServer(ctx) - if err != nil { - return types.AuthConfig{}, err - } - - configKey = cfgKey - } - - cfg := config.LoadDefaultConfigFile(os.Stderr) - a, _ := cfg.GetAuthConfig(configKey) - - return types.AuthConfig(a), nil -} - -func electAuthServer(ctx context.Context) (string, error) { - c, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - return "", err - } - - info, err := c.Info(ctx) - if err != nil { - return registry.IndexServer, nil - } - - if info.IndexServerAddress == "" { - return registry.IndexServer, nil - } - - return info.IndexServerAddress, nil -} - -func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { - buf, err := json.Marshal(authConfig) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(buf), nil -} diff --git a/vm/internal/backend/containers.go b/vm/internal/backend/containers.go index d3b40cc..900b447 100644 --- a/vm/internal/backend/containers.go +++ b/vm/internal/backend/containers.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/bugsnag/bugsnag-go/v2" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -24,6 +25,7 @@ func GetContainersForVolume(ctx context.Context, cli *client.Client, volumeName }) if err != nil { log.Error(err) + _ = bugsnag.Notify(err, ctx) } containerNames := make([]string, 0, len(containers)) diff --git a/vm/internal/backend/driver.go b/vm/internal/backend/driver.go index 4161c9d..6aa3eec 100644 --- a/vm/internal/backend/driver.go +++ b/vm/internal/backend/driver.go @@ -2,14 +2,16 @@ package backend import ( "context" + "github.com/bugsnag/bugsnag-go/v2" "github.com/docker/docker/client" - "github.com/sirupsen/logrus" + "github.com/docker/volumes-backup-extension/internal/log" ) func GetVolumeDriver(ctx context.Context, cli *client.Client, volumeName string) string { resp, err := cli.VolumeInspect(ctx, volumeName) if err != nil { - logrus.Error(err) + log.Error(err) + _ = bugsnag.Notify(err, ctx) } return resp.Driver diff --git a/vm/internal/backend/size.go b/vm/internal/backend/size.go index fd1e6bf..845e2cb 100644 --- a/vm/internal/backend/size.go +++ b/vm/internal/backend/size.go @@ -23,17 +23,19 @@ type VolumeSize struct { Human string } -func GetVolumesSize(ctx context.Context, cli *client.Client, volumeName string) map[string]VolumeSize { +func GetVolumesSize(ctx context.Context, cli *client.Client, volumeName string) (map[string]VolumeSize, error) { + m := make(map[string]VolumeSize) + // Ensure the image is present before creating the container reader, err := cli.ImagePull(ctx, internal.NsenterImage, types.ImagePullOptions{ Platform: "linux/" + runtime.GOARCH, }) if err != nil { - log.Error(err) + return m, err } _, err = io.Copy(os.Stdout, reader) if err != nil { - log.Error(err) + return m, err } resp, err := cli.ContainerCreate(ctx, &container.Config{ @@ -52,37 +54,36 @@ func GetVolumesSize(ctx context.Context, cli *client.Client, volumeName string) Privileged: true, }, nil, nil, "") if err != nil { - log.Error(err) + return m, err } if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { - log.Error(err) + return m, err } statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { - panic(err) + return m, err } case <-statusCh: } out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true}) if err != nil { - log.Error(err) + return m, err } buf := new(bytes.Buffer) _, err = buf.ReadFrom(out) if err != nil { - log.Error(err) + return m, err } output := buf.String() lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") - m := make(map[string]VolumeSize) for _, line := range lines { s := strings.Split(line, "\t") // e.g. 924 /var/lib/docker/volumes/my-volume if len(s) != 2 { @@ -118,10 +119,10 @@ func GetVolumesSize(ctx context.Context, cli *client.Client, volumeName string) err = cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{}) if err != nil { - log.Error(err) + return m, err } - return m + return m, nil } // byteCountSI converts a size in bytes to a human-readable string in SI (decimal) format. diff --git a/vm/internal/handler/clone.go b/vm/internal/handler/clone.go index dd9fe71..2cc5083 100644 --- a/vm/internal/handler/clone.go +++ b/vm/internal/handler/clone.go @@ -18,6 +18,7 @@ import ( ) func (h *Handler) CloneVolume(ctx echo.Context) error { + ctxReq := ctx.Request().Context() volumeName := ctx.Param("volume") destVolume := ctx.QueryParam("destVolume") @@ -33,35 +34,32 @@ func (h *Handler) CloneVolume(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() delete(h.ProgressCache.m, volumeName) h.ProgressCache.Unlock() - _ = backend.TriggerUIRefresh(ctx.Request().Context(), cli) + _ = backend.TriggerUIRefresh(ctxReq, cli) }() h.ProgressCache.Lock() h.ProgressCache.m[volumeName] = "clone" h.ProgressCache.Unlock() - if err := backend.TriggerUIRefresh(ctx.Request().Context(), cli); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := backend.TriggerUIRefresh(ctxReq, cli); err != nil { + return err } // Stop container(s) - stoppedContainers, err := backend.StopContainersAttachedToVolume(ctx.Request().Context(), cli, volumeName) + stoppedContainers, err := backend.StopContainersAttachedToVolume(ctxReq, cli, volumeName) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Ensure the image is present before creating the container - reader, err := cli.ImagePull(ctx.Request().Context(), internal.BusyboxImage, types.ImagePullOptions{ + reader, err := cli.ImagePull(ctxReq, internal.BusyboxImage, types.ImagePullOptions{ Platform: "linux/" + runtime.GOARCH, }) if err != nil { @@ -86,7 +84,7 @@ func (h *Handler) CloneVolume(ctx echo.Context) error { } // Clone - resp, err := cli.ContainerCreate(ctx.Request().Context(), &container.Config{ + resp, err := cli.ContainerCreate(ctxReq, &container.Config{ Image: internal.BusyboxImage, AttachStdout: true, AttachStderr: true, @@ -107,55 +105,48 @@ func (h *Handler) CloneVolume(ctx echo.Context) error { }, }, nil, nil, "") if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } - if err := cli.ContainerStart(ctx.Request().Context(), resp.ID, types.ContainerStartOptions{}); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := cli.ContainerStart(ctxReq, resp.ID, types.ContainerStartOptions{}); err != nil { + return err } var exitCode int64 - statusCh, errCh := cli.ContainerWait(ctx.Request().Context(), resp.ID, container.WaitConditionNotRunning) + statusCh, errCh := cli.ContainerWait(ctxReq, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } case status := <-statusCh: log.Infof("status: %#+v\n", status) exitCode = status.StatusCode } - out, err := cli.ContainerLogs(ctx.Request().Context(), resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}) + out, err := cli.ContainerLogs(ctxReq, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, out) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } if exitCode != 0 { return ctx.String(http.StatusInternalServerError, fmt.Sprintf("container exited with status code %d\n", exitCode)) } - err = cli.ContainerRemove(ctx.Request().Context(), resp.ID, types.ContainerRemoveOptions{}) + err = cli.ContainerRemove(ctxReq, resp.ID, types.ContainerRemoveOptions{}) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Start container(s) - err = backend.StartContainersAttachedToVolume(ctx.Request().Context(), cli, stoppedContainers) + err = backend.StartContainersAttachedToVolume(ctxReq, cli, stoppedContainers) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } return ctx.String(http.StatusCreated, "") diff --git a/vm/internal/handler/clone_test.go b/vm/internal/handler/clone_test.go index f8882ad..b2f9b59 100644 --- a/vm/internal/handler/clone_test.go +++ b/vm/internal/handler/clone_test.go @@ -101,7 +101,8 @@ func TestCloneVolume(t *testing.T) { t.Fatal(err) } require.Len(t, clonedVolumeResp.Volumes, 1) - sizes := backend.GetVolumesSize(context.Background(), dockerClient, destVolume) + sizes, err := backend.GetVolumesSize(context.Background(), dockerClient, destVolume) + require.NoError(t, err) require.Equal(t, int64(16000), sizes[destVolume].Bytes) require.Equal(t, "16.0 kB", sizes[destVolume].Human) diff --git a/vm/internal/handler/delete.go b/vm/internal/handler/delete.go index 2dff6e3..a7dad64 100644 --- a/vm/internal/handler/delete.go +++ b/vm/internal/handler/delete.go @@ -9,6 +9,7 @@ import ( ) func (h *Handler) DeleteVolume(ctx echo.Context) error { + ctxReq := ctx.Request().Context() volumeName := ctx.Param("volume") if volumeName == "" { @@ -19,31 +20,28 @@ func (h *Handler) DeleteVolume(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() delete(h.ProgressCache.m, volumeName) h.ProgressCache.Unlock() - _ = backend.TriggerUIRefresh(ctx.Request().Context(), cli) + _ = backend.TriggerUIRefresh(ctxReq, cli) }() h.ProgressCache.Lock() h.ProgressCache.m[volumeName] = "delete" h.ProgressCache.Unlock() - if err := backend.TriggerUIRefresh(ctx.Request().Context(), cli); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := backend.TriggerUIRefresh(ctxReq, cli); err != nil { + return err } // Delete volume - err = cli.VolumeRemove(ctx.Request().Context(), volumeName, true) + err = cli.VolumeRemove(ctxReq, volumeName, true) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } return ctx.String(http.StatusNoContent, "") diff --git a/vm/internal/handler/export.go b/vm/internal/handler/export.go index 287136e..d7b9f64 100644 --- a/vm/internal/handler/export.go +++ b/vm/internal/handler/export.go @@ -9,17 +9,17 @@ import ( "runtime" "strings" - "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/volumes-backup-extension/internal" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/volumes-backup-extension/internal" "github.com/docker/volumes-backup-extension/internal/backend" "github.com/docker/volumes-backup-extension/internal/log" "github.com/labstack/echo" ) func (h *Handler) ExportVolume(ctx echo.Context) error { + ctxReq := ctx.Request().Context() volumeName := ctx.Param("volume") path := ctx.QueryParam("path") fileName := ctx.QueryParam("fileName") @@ -40,31 +40,28 @@ func (h *Handler) ExportVolume(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() delete(h.ProgressCache.m, volumeName) h.ProgressCache.Unlock() - _ = backend.TriggerUIRefresh(ctx.Request().Context(), cli) + _ = backend.TriggerUIRefresh(ctxReq, cli) }() h.ProgressCache.Lock() h.ProgressCache.m[volumeName] = "export" h.ProgressCache.Unlock() - if err := backend.TriggerUIRefresh(ctx.Request().Context(), cli); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := backend.TriggerUIRefresh(ctxReq, cli); err != nil { + return err } // Stop container(s) - stoppedContainers, err := backend.StopContainersAttachedToVolume(ctx.Request().Context(), cli, volumeName) + stoppedContainers, err := backend.StopContainersAttachedToVolume(ctxReq, cli, volumeName) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Export @@ -87,7 +84,7 @@ func (h *Handler) ExportVolume(ctx echo.Context) error { log.Infof("binds: %+v", binds) // Ensure the image is present before creating the container - reader, err := cli.ImagePull(ctx.Request().Context(), internal.BusyboxImage, types.ImagePullOptions{ + reader, err := cli.ImagePull(ctxReq, internal.BusyboxImage, types.ImagePullOptions{ Platform: "linux/" + runtime.GOARCH, }) if err != nil { @@ -98,7 +95,7 @@ func (h *Handler) ExportVolume(ctx echo.Context) error { return err } - resp, err := cli.ContainerCreate(ctx.Request().Context(), &container.Config{ + resp, err := cli.ContainerCreate(ctxReq, &container.Config{ Image: internal.BusyboxImage, AttachStdout: true, AttachStderr: true, @@ -117,55 +114,48 @@ func (h *Handler) ExportVolume(ctx echo.Context) error { Binds: binds, }, nil, nil, "") if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } - if err := cli.ContainerStart(ctx.Request().Context(), resp.ID, types.ContainerStartOptions{}); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := cli.ContainerStart(ctxReq, resp.ID, types.ContainerStartOptions{}); err != nil { + return err } var exitCode int64 - statusCh, errCh := cli.ContainerWait(ctx.Request().Context(), resp.ID, container.WaitConditionNotRunning) + statusCh, errCh := cli.ContainerWait(ctxReq, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } case status := <-statusCh: log.Infof("status: %#+v\n", status) exitCode = status.StatusCode } - out, err := cli.ContainerLogs(ctx.Request().Context(), resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}) + out, err := cli.ContainerLogs(ctxReq, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, out) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } if exitCode != 0 { return ctx.String(http.StatusInternalServerError, fmt.Sprintf("container exited with status code %d\n", exitCode)) } - err = cli.ContainerRemove(ctx.Request().Context(), resp.ID, types.ContainerRemoveOptions{}) + err = cli.ContainerRemove(ctxReq, resp.ID, types.ContainerRemoveOptions{}) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Start container(s) - err = backend.StartContainersAttachedToVolume(ctx.Request().Context(), cli, stoppedContainers) + err = backend.StartContainersAttachedToVolume(ctxReq, cli, stoppedContainers) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } return ctx.String(http.StatusCreated, "") diff --git a/vm/internal/handler/import.go b/vm/internal/handler/import.go index 4fc09cb..20850da 100644 --- a/vm/internal/handler/import.go +++ b/vm/internal/handler/import.go @@ -17,6 +17,7 @@ import ( ) func (h *Handler) ImportTarGzFile(ctx echo.Context) error { + ctxReq := ctx.Request().Context() volumeName := ctx.Param("volume") path := ctx.QueryParam("path") @@ -32,31 +33,28 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() delete(h.ProgressCache.m, volumeName) h.ProgressCache.Unlock() - _ = backend.TriggerUIRefresh(ctx.Request().Context(), cli) + _ = backend.TriggerUIRefresh(ctxReq, cli) }() h.ProgressCache.Lock() h.ProgressCache.m[volumeName] = "import" h.ProgressCache.Unlock() - if err := backend.TriggerUIRefresh(ctx.Request().Context(), cli); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := backend.TriggerUIRefresh(ctxReq, cli); err != nil { + return err } // Stop container(s) - stoppedContainers, err := backend.StopContainersAttachedToVolume(ctx.Request().Context(), cli, volumeName) + stoppedContainers, err := backend.StopContainersAttachedToVolume(ctxReq, cli, volumeName) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Import @@ -67,7 +65,7 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error { log.Infof("binds: %+v", binds) // Ensure the image is present before creating the container - reader, err := cli.ImagePull(ctx.Request().Context(), internal.BusyboxImage, types.ImagePullOptions{ + reader, err := cli.ImagePull(ctxReq, internal.BusyboxImage, types.ImagePullOptions{ Platform: "linux/" + runtime.GOARCH, }) if err != nil { @@ -78,7 +76,7 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error { return err } - resp, err := cli.ContainerCreate(ctx.Request().Context(), &container.Config{ + resp, err := cli.ContainerCreate(ctxReq, &container.Config{ Image: internal.BusyboxImage, AttachStdout: true, AttachStderr: true, @@ -98,55 +96,48 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error { Binds: binds, }, nil, nil, "") if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } - if err := cli.ContainerStart(ctx.Request().Context(), resp.ID, types.ContainerStartOptions{}); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := cli.ContainerStart(ctxReq, resp.ID, types.ContainerStartOptions{}); err != nil { + return err } var exitCode int64 - statusCh, errCh := cli.ContainerWait(ctx.Request().Context(), resp.ID, container.WaitConditionNotRunning) + statusCh, errCh := cli.ContainerWait(ctxReq, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } case status := <-statusCh: log.Infof("status: %#+v\n", status) exitCode = status.StatusCode } - out, err := cli.ContainerLogs(ctx.Request().Context(), resp.ID, types.ContainerLogsOptions{ShowStdout: true}) + out, err := cli.ContainerLogs(ctxReq, resp.ID, types.ContainerLogsOptions{ShowStdout: true}) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, out) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } if exitCode != 0 { return ctx.String(http.StatusInternalServerError, fmt.Sprintf("container exited with status code %d\n", exitCode)) } - err = cli.ContainerRemove(ctx.Request().Context(), resp.ID, types.ContainerRemoveOptions{}) + err = cli.ContainerRemove(ctxReq, resp.ID, types.ContainerRemoveOptions{}) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Start container(s) - err = backend.StartContainersAttachedToVolume(ctx.Request().Context(), cli, stoppedContainers) + err = backend.StartContainersAttachedToVolume(ctxReq, cli, stoppedContainers) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } return ctx.String(http.StatusOK, "") diff --git a/vm/internal/handler/import_test.go b/vm/internal/handler/import_test.go index 7d03585..a547b0a 100644 --- a/vm/internal/handler/import_test.go +++ b/vm/internal/handler/import_test.go @@ -61,7 +61,8 @@ func TestImportTarGzFile(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, rec.Code) - sizes := backend.GetVolumesSize(c.Request().Context(), cli, volume) + sizes, err := backend.GetVolumesSize(c.Request().Context(), cli, volume) + require.NoError(t, err) require.Equal(t, int64(16000), sizes[volume].Bytes) require.Equal(t, "16.0 kB", sizes[volume].Human) } @@ -142,7 +143,8 @@ func TestImportTarGzFileShouldRemovePreviousVolumeData(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, rec.Code) - sizes := backend.GetVolumesSize(c.Request().Context(), cli, volume) + sizes, err := backend.GetVolumesSize(c.Request().Context(), cli, volume) + require.NoError(t, err) require.Equal(t, int64(16000), sizes[volume].Bytes) require.Equal(t, "16.0 kB", sizes[volume].Human) } diff --git a/vm/internal/handler/load.go b/vm/internal/handler/load.go index a0ad2f3..c4be4b7 100644 --- a/vm/internal/handler/load.go +++ b/vm/internal/handler/load.go @@ -3,12 +3,14 @@ package handler import ( "net/http" + "github.com/bugsnag/bugsnag-go/v2" "github.com/docker/volumes-backup-extension/internal/backend" "github.com/docker/volumes-backup-extension/internal/log" "github.com/labstack/echo" ) func (h *Handler) LoadImage(ctx echo.Context) error { + ctxReq := ctx.Request().Context() volumeName := ctx.Param("volume") image := ctx.QueryParam("image") @@ -24,43 +26,40 @@ func (h *Handler) LoadImage(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() delete(h.ProgressCache.m, volumeName) h.ProgressCache.Unlock() - _ = backend.TriggerUIRefresh(ctx.Request().Context(), cli) + _ = backend.TriggerUIRefresh(ctxReq, cli) }() h.ProgressCache.Lock() h.ProgressCache.m[volumeName] = "load" h.ProgressCache.Unlock() - if err := backend.TriggerUIRefresh(ctx.Request().Context(), cli); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + if err := backend.TriggerUIRefresh(ctxReq, cli); err != nil { + return err } - stoppedContainers, err := backend.StopContainersAttachedToVolume(ctx.Request().Context(), cli, volumeName) + stoppedContainers, err := backend.StopContainersAttachedToVolume(ctxReq, cli, volumeName) if err != nil { - log.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return err } // Load - err = backend.Load(ctx.Request().Context(), cli, volumeName, image) + err = backend.Load(ctxReq, cli, volumeName, image) if err != nil { - log.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return err } // Start container(s) - err = backend.StartContainersAttachedToVolume(ctx.Request().Context(), cli, stoppedContainers) + err = backend.StartContainersAttachedToVolume(ctxReq, cli, stoppedContainers) if err != nil { log.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + _ = bugsnag.Notify(err, ctxReq) + return err } return ctx.String(http.StatusOK, "") diff --git a/vm/internal/handler/load_test.go b/vm/internal/handler/load_test.go index d96fa93..e8ae1df 100644 --- a/vm/internal/handler/load_test.go +++ b/vm/internal/handler/load_test.go @@ -74,7 +74,8 @@ func TestLoadImage(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, rec.Code) - sizes := backend.GetVolumesSize(c.Request().Context(), cli, volume) + sizes, err := backend.GetVolumesSize(c.Request().Context(), cli, volume) + require.NoError(t, err) t.Logf("Volume size after loading image into it: %+v", sizes[volume]) require.Equal(t, int64(16000), sizes[volume].Bytes) require.Equal(t, "16.0 kB", sizes[volume].Human) @@ -169,7 +170,8 @@ func TestLoadImageShouldRemovePreviousVolumeData(t *testing.T) { require.NoError(t, err) require.Equal(t, http.StatusOK, rec.Code) - sizes := backend.GetVolumesSize(c.Request().Context(), cli, volume) + sizes, err := backend.GetVolumesSize(c.Request().Context(), cli, volume) + require.NoError(t, err) t.Logf("Volume size after loading image into it: %+v", sizes[volume]) require.Equal(t, int64(16000), sizes[volume].Bytes) require.Equal(t, "16.0 kB", sizes[volume].Human) diff --git a/vm/internal/handler/pull.go b/vm/internal/handler/pull.go index 37552f2..f14bf94 100644 --- a/vm/internal/handler/pull.go +++ b/vm/internal/handler/pull.go @@ -34,8 +34,7 @@ func (h *Handler) PullVolume(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() @@ -50,8 +49,7 @@ func (h *Handler) PullVolume(ctx echo.Context) error { err = backend.TriggerUIRefresh(ctxReq, cli) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // To provide backwards compatibility with older versions of Docker Desktop, @@ -78,19 +76,17 @@ func (h *Handler) PullVolume(ctx echo.Context) error { }) if err != nil { - log.Error(err) - if strings.Contains(err.Error(), "unauthorized: authentication required") { return ctx.String(http.StatusUnauthorized, err.Error()) } - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer pullResp.Close() pullRespBytes, err := ioutil.ReadAll(pullResp) if err != nil { - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } for _, line := range strings.Split(string(pullRespBytes), "\n") { @@ -100,22 +96,19 @@ func (h *Handler) PullVolume(ctx echo.Context) error { // Stop container(s) stoppedContainers, err := backend.StopContainersAttachedToVolume(ctxReq, cli, volumeName) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Load the image into the volume log.Infof("Loading image %s into volume %s...", parsedRef.String(), volumeName) if err := backend.Load(ctxReq, cli, volumeName, parsedRef.String()); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Start container(s) err = backend.StartContainersAttachedToVolume(ctxReq, cli, stoppedContainers) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } return ctx.String(http.StatusCreated, "") diff --git a/vm/internal/handler/pull_test.go b/vm/internal/handler/pull_test.go index eeb3b5e..f5264ac 100644 --- a/vm/internal/handler/pull_test.go +++ b/vm/internal/handler/pull_test.go @@ -175,7 +175,8 @@ func TestPullVolume(t *testing.T) { require.Equal(t, http.StatusCreated, rec.Code) // Check the content of the volume - m := backend.GetVolumesSize(c.Request().Context(), cli, volume) + m, err := backend.GetVolumesSize(c.Request().Context(), cli, volume) + require.NoError(t, err) require.Equal(t, int64(16000), m[volume].Bytes) require.Equal(t, "16.0 kB", m[volume].Human) } @@ -342,7 +343,8 @@ func TestPullVolumeUsingCorrectAuth(t *testing.T) { require.Equal(t, http.StatusCreated, rec.Code) // Check the content of the volume - m := backend.GetVolumesSize(c.Request().Context(), cli, volume) + m, err := backend.GetVolumesSize(c.Request().Context(), cli, volume) + require.NoError(t, err) require.Equal(t, int64(16000), m[volume].Bytes) require.Equal(t, "16.0 kB", m[volume].Human) } @@ -481,7 +483,8 @@ func TestPullVolumeUsingWrongAuthShouldFail(t *testing.T) { require.Equal(t, http.StatusUnauthorized, rec.Code) // Check the content of the volume - m := backend.GetVolumesSize(c.Request().Context(), cli, volume) + m, err := backend.GetVolumesSize(c.Request().Context(), cli, volume) + require.NoError(t, err) require.Equal(t, int64(0), m[volume].Bytes) require.Equal(t, "0 B", m[volume].Human) } diff --git a/vm/internal/handler/push.go b/vm/internal/handler/push.go index fc1063d..7764556 100644 --- a/vm/internal/handler/push.go +++ b/vm/internal/handler/push.go @@ -29,12 +29,13 @@ type ErrorDetail struct { // PushVolume pushes a volume to a registry. // The user must be previously authenticated to the registry with `docker login `, otherwise it returns 401 StatusUnauthorized. func (h *Handler) PushVolume(ctx echo.Context) error { + ctxReq := ctx.Request().Context() + var request PushRequest if err := ctx.Bind(&request); err != nil { return err } - ctxReq := ctx.Request().Context() volumeName := ctx.Param("volume") log.Infof("volumeName: %s", volumeName) log.Infof("reference: %s", request.Reference) @@ -42,8 +43,7 @@ func (h *Handler) PushVolume(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() @@ -58,8 +58,7 @@ func (h *Handler) PushVolume(ctx echo.Context) error { err = backend.TriggerUIRefresh(ctxReq, cli) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // To provide backwards compatibility with older versions of Docker Desktop, @@ -82,14 +81,12 @@ func (h *Handler) PushVolume(ctx echo.Context) error { // Stop container(s) stoppedContainers, err := backend.StopContainersAttachedToVolume(ctxReq, cli, volumeName) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Save the content of the volume into an image if err := backend.Save(ctxReq, cli, volumeName, parsedRef.String()); err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Push the image to registry @@ -97,8 +94,7 @@ func (h *Handler) PushVolume(ctx echo.Context) error { RegistryAuth: request.Base64EncodedAuth, }) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer pushResp.Close() @@ -117,7 +113,6 @@ func (h *Handler) PushVolume(ctx echo.Context) error { // {"errorDetail":{"message":"unauthorized: authentication required"},"error":"unauthorized: authentication required"} // or // {"errorDetail":{"message":"no basic auth credentials"},"error":"no basic auth credentials"} - log.Error(err) if pel.Error == "unauthorized: authentication required" || pel.Error == "no basic auth credentials" { return ctx.String(http.StatusUnauthorized, pel.Error) } else { @@ -129,8 +124,7 @@ func (h *Handler) PushVolume(ctx echo.Context) error { // Start container(s) err = backend.StartContainersAttachedToVolume(ctxReq, cli, stoppedContainers) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } return ctx.String(http.StatusCreated, "") diff --git a/vm/internal/handler/save.go b/vm/internal/handler/save.go index 56a15f6..fa06ff2 100644 --- a/vm/internal/handler/save.go +++ b/vm/internal/handler/save.go @@ -25,8 +25,7 @@ func (h *Handler) SaveVolume(ctx echo.Context) error { cli, err := h.DockerClient() if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } defer func() { h.ProgressCache.Lock() @@ -41,27 +40,23 @@ func (h *Handler) SaveVolume(ctx echo.Context) error { err = backend.TriggerUIRefresh(ctxReq, cli) if err != nil { - log.Error(err) - return ctx.String(http.StatusInternalServerError, err.Error()) + return err } // Stop container(s) stoppedContainers, err := backend.StopContainersAttachedToVolume(ctxReq, cli, volumeName) if err != nil { - log.Error(err) return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } // Save volume into an image if err := backend.Save(ctxReq, cli, volumeName, image); err != nil { - log.Error(err) return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } // Start container(s) err = backend.StartContainersAttachedToVolume(ctxReq, cli, stoppedContainers) if err != nil { - log.Error(err) return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } diff --git a/vm/internal/handler/size.go b/vm/internal/handler/size.go index cf20ad1..237e4d1 100644 --- a/vm/internal/handler/size.go +++ b/vm/internal/handler/size.go @@ -4,19 +4,21 @@ import ( "net/http" "github.com/docker/volumes-backup-extension/internal/backend" - "github.com/docker/volumes-backup-extension/internal/log" "github.com/labstack/echo" ) func (h *Handler) VolumeSize(ctx echo.Context) error { + ctxReq := ctx.Request().Context() volumeName := ctx.Param("volume") cli, err := h.DockerClient() if err != nil { - log.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return err } - m := backend.GetVolumesSize(ctx.Request().Context(), cli, volumeName) + m, err := backend.GetVolumesSize(ctxReq, cli, volumeName) + if err != nil { + return err + } return ctx.JSON(http.StatusOK, m[volumeName]) } diff --git a/vm/internal/handler/sizes.go b/vm/internal/handler/sizes.go index 17ee95e..281212d 100644 --- a/vm/internal/handler/sizes.go +++ b/vm/internal/handler/sizes.go @@ -13,7 +13,10 @@ func (h *Handler) VolumesSize(ctx echo.Context) error { return err } - m := backend.GetVolumesSize(ctx.Request().Context(), cli, "*") + m, err := backend.GetVolumesSize(ctx.Request().Context(), cli, "*") + if err != nil { + return err + } return ctx.JSON(http.StatusOK, m) } diff --git a/vm/internal/handler/volumes.go b/vm/internal/handler/volumes.go index df6b007..e6df94a 100644 --- a/vm/internal/handler/volumes.go +++ b/vm/internal/handler/volumes.go @@ -5,7 +5,6 @@ import ( "sync" "github.com/docker/docker/api/types/filters" - "github.com/docker/volumes-backup-extension/internal/log" "github.com/labstack/echo" ) @@ -22,14 +21,16 @@ type VolumeData struct { } func (h *Handler) Volumes(ctx echo.Context) error { + ctxReq := ctx.Request().Context() + cli, err := h.DockerClient() if err != nil { - log.Error(err) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return err } - v, err := cli.VolumeList(ctx.Request().Context(), filters.NewArgs()) + + v, err := cli.VolumeList(ctxReq, filters.NewArgs()) if err != nil { - log.Error(err) + return err } var res = VolumesResponse{ diff --git a/vm/internal/setup/bugsnag.go b/vm/internal/setup/bugsnag.go new file mode 100644 index 0000000..4ae6196 --- /dev/null +++ b/vm/internal/setup/bugsnag.go @@ -0,0 +1,74 @@ +package setup + +import ( + "github.com/docker/volumes-backup-extension/internal/log" + "github.com/labstack/echo" + "net/http" + "os" + "os/exec" + "runtime" + + "github.com/bugsnag/bugsnag-go/v2" +) + +func ConfigureBugsnag() { + bugsnagAPIKey := os.Getenv("BUGSNAG_API_KEY") + if bugsnagAPIKey == "" { + log.Warn(`Bugsnag configuration not added as environment variable "BUGSNAG_API_KEY" is empty.`) + return + } + + bugsnag.Configure(bugsnag.Configuration{ + APIKey: bugsnagAPIKey, + ReleaseStage: os.Getenv("BUGSNAG_RELEASE_STAGE"), + // The import paths for the Go packages containing your source files + ProjectPackages: []string{"main", "github.com/docker/volumes-backup-extension"}, + AppVersion: os.Getenv("BUGSNAG_APP_VERSION"), + }) + + bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error { + event.MetaData.Add("OS", "Architecture", runtime.GOARCH) + event.MetaData.Add("OS", "Docker Desktop Version", getDockerDesktopVersion()) + return nil + }) + + log.Info("Bugsnag configuration added successfully.") +} + +// ConfigureBugsnagHandler uses bugsnag.Handler(nil) to wrap the default http handlers +// so that Bugsnag is automatically notified about panics. +// See: https://docs.bugsnag.com/platforms/go/net-http/#basic-configuration +func ConfigureBugsnagHandler(server *http.Server) { + bugsnagAPIKey := os.Getenv("BUGSNAG_API_KEY") + if bugsnagAPIKey == "" { + log.Warn(`Bugsnag handler to notify about panics not configured as environment variable "BUGSNAG_API_KEY" is empty.`) + return + } + server.Handler = bugsnag.Handler(nil) + log.Info("Bugsnag handler configured successfully.") +} + +func ConfigureBugsnagHTTPErrorHandler(err error, c echo.Context) { + if os.Getenv("BUGSNAG_API_KEY") == "" { + return + } + + he, ok := err.(*echo.HTTPError) + if ok && he.Code != http.StatusInternalServerError { + return + } + + log.Error(err) + _ = bugsnag.Notify(err, c.Request().Context()) +} + +func getDockerDesktopVersion() string { + cmd := exec.Command("docker", "version", "--format", "{{ json .Server.Platform.Name }}") // e.g. "Docker Desktop 4.12.0 (85790)" + stdout, err := cmd.Output() + if err != nil { + log.Error(err) + return "" + } + + return string(stdout) +} diff --git a/vm/main.go b/vm/main.go index 7f4f4c8..f7f638e 100644 --- a/vm/main.go +++ b/vm/main.go @@ -3,6 +3,8 @@ package main import ( "context" "flag" + "github.com/bugsnag/bugsnag-go/v2" + "github.com/labstack/echo/middleware" "net" "net/http" "os" @@ -12,8 +14,8 @@ import ( "github.com/docker/docker/client" "github.com/docker/volumes-backup-extension/internal/handler" "github.com/docker/volumes-backup-extension/internal/log" + "github.com/docker/volumes-backup-extension/internal/setup" "github.com/labstack/echo" - "github.com/labstack/echo/middleware" ) var ( @@ -25,6 +27,8 @@ func main() { flag.StringVar(&socketPath, "socket", "/run/guest/ext.sock", "Unix domain socket to listen on") flag.Parse() + setup.ConfigureBugsnag() + _ = os.RemoveAll(socketPath) // Output to stdout instead of the default stderr @@ -32,7 +36,12 @@ func main() { router := echo.New() router.HideBanner = true - router.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + router.HTTPErrorHandler = func(err error, c echo.Context) { + setup.ConfigureBugsnagHTTPErrorHandler(err, c) + router.DefaultHTTPErrorHandler(err, c) + } + + logMiddleware := middleware.LoggerWithConfig(middleware.LoggerConfig{ Skipper: middleware.DefaultSkipper, Format: `{"time":"${time_rfc3339_nano}","id":"${id}",` + `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + @@ -40,20 +49,22 @@ func main() { `,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", CustomTimeFormat: "2006-01-02 15:04:05.00000", Output: os.Stdout, - })) + }) + router.Use(logMiddleware) log.Infof("Starting listening on %s\n", socketPath) ln, err := net.Listen("unix", socketPath) if err != nil { + _ = bugsnag.Notify(err) log.Fatal(err) } router.Listener = ln cliFactory := func() (*client.Client, error) { - return client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) } if err != nil { + _ = bugsnag.Notify(err) log.Fatal(err) } @@ -75,7 +86,13 @@ func main() { // Start server go func() { - if err := router.Start(""); err != nil && err != http.ErrServerClosed { + server := &http.Server{ + Addr: "", + } + setup.ConfigureBugsnagHandler(server) + + if err := router.StartServer(server); err != nil && err != http.ErrServerClosed { + _ = bugsnag.Notify(err) log.Fatal("shutting down the server") } }() @@ -88,6 +105,7 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := router.Shutdown(ctx); err != nil { + _ = bugsnag.Notify(err) log.Fatal(err) } }