Pass auth in body instead of in the req headers (#44)

* Pass auth in body instead of in the req headers

* Check for the right error msg

* Remove unnecessary images to pull

* Ensure image is present before creating the container
This commit is contained in:
Felipe Cruz Martinez 2022-08-23 16:08:16 +02:00 коммит произвёл GitHub
Родитель 7a8979969f
Коммит 66b768117a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
26 изменённых файлов: 288 добавлений и 504 удалений

4
.github/workflows/build-scan-push.yaml поставляемый
Просмотреть файл

@ -70,8 +70,6 @@ jobs:
- name: Build and export to Docker
uses: docker/build-push-action@v2
with:
build-args: |
EXTENSION_INSTALL_DIR_NAME=${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
push: false
load: true # Export to Docker Engine rather than pushing to a registry
tags: ${{ github.run_id }}
@ -121,8 +119,6 @@ jobs:
- name: Docker Build and Push to Docker Hub
uses: docker/build-push-action@v2
with:
build-args: |
EXTENSION_INSTALL_DIR_NAME=${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}

Просмотреть файл

@ -11,7 +11,6 @@ RUN --mount=type=cache,target=/go/pkg/mod \
go build -trimpath -ldflags="-s -w" -o bin/service
FROM --platform=$BUILDPLATFORM node:17.7-alpine3.14 AS client-builder
ARG EXTENSION_INSTALL_DIR_NAME
WORKDIR /ui
# cache packages in layer
COPY ui/package.json /ui/package.json
@ -21,10 +20,9 @@ RUN --mount=type=cache,target=/usr/src/app/.npm \
npm ci
# install
COPY ui /ui
RUN echo "REACT_APP_EXTENSION_INSTALLATION_DIR_NAME=$EXTENSION_INSTALL_DIR_NAME" >> /ui/.env \
&& npm run build
RUN npm run build
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS volume-share-client-builder
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS docker-credentials-client-builder
ENV CGO_ENABLED=0
WORKDIR /output
RUN apk update \
@ -75,7 +73,7 @@ COPY metadata.json .
COPY icon.svg .
COPY --from=builder /backend/bin/service /
COPY --from=client-builder /ui/build ui
COPY --from=volume-share-client-builder output/dist ./host
COPY --from=docker-credentials-client-builder output/dist ./host
RUN mkdir -p /vackup

Просмотреть файл

@ -7,7 +7,7 @@ INFO_COLOR = \033[0;36m
NO_COLOR = \033[m
build-extension: ## Build service image to be deployed as a desktop extension
docker build --build-arg=EXTENSION_INSTALL_DIR_NAME=$(subst /,_,$(IMAGE)) --tag=$(IMAGE):$(TAG) .
docker build --tag=$(IMAGE):$(TAG) .
install-extension: build-extension ## Install the extension
docker extension install $(IMAGE):$(TAG)
@ -26,7 +26,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 --build-arg=EXTENSION_INSTALL_DIR_NAME=$(subst /,_,$(IMAGE)) --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 --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:

2
client/.gitignore поставляемый
Просмотреть файл

@ -13,4 +13,4 @@ cov
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# The binary
volumes-share-client
docker-credentials-client

Просмотреть файл

@ -1,4 +1,4 @@
BINARY?=volumes-share-client
BINARY?=docker-credentials-client
LDFLAGS="-s -w"
GO_BUILD=$(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS)

Просмотреть файл

@ -1,17 +1,15 @@
# volumes-share-client
# docker-credentials-client
This is a client cli for volume share.
## Pushing a volume
Run
This is a client cli to retrieve the Docker credentials in base64.
```shell
$ ./volumes-share-client push VOLUME REFERENCE
$ ./docker-credentials-client get-creds REFERENCE
```
For example, if there is a volume named `my-volume` to push it to
`rumpl/volume:1.0.0` you can run `./volumes-share-client push rumpl/volume:1.0.0 my-volume`
For example, if there is an image with reference `john/my-image:1.0.0` (or equally `docker.io/john/my-image:1.0.0`, you can retrieve the Docker credentials from the `docker.io` (DockerHub) registry running:
```shell
./docker-credentials-client get-creds john/my-image:1.0.0
ey...
```
To create a new volume named `my-pulled-volume` with the contents of `rumpl/volume:1.0.0` you can run
`./volumes-share-client pull rumpl/volume:1.0.0 my-pulled-volume`.

Просмотреть файл

@ -1,142 +0,0 @@
//go:build !windows
// +build !windows
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/sirupsen/logrus"
)
type VolumePushOptions struct {
RegistryAuth string
}
type VolumePullOptions struct {
RegistryAuth string
}
type Client interface {
Push(ctx context.Context, reference string, volume string, options VolumePushOptions) error
Pull(ctx context.Context, reference string, volume string, options VolumePullOptions) error
}
type cl struct {
httpc http.Client
}
// New returns a new volume client
func New(extensionDir string) (Client, error) {
// e.g. from "felipecruz/vackup-docker-extension" to "felipecruz_vackup-docker-extension"
safeExtensionDir := strings.Replace(extensionDir, "/", "_", 1)
logrus.Infof("safeExtensionDir: %s", safeExtensionDir)
c := &cl{
httpc: http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
hd, err := os.UserHomeDir()
if err != nil {
return nil, err
}
var socket string
switch runtime.GOOS {
// The socket name in the **host** is no longer the one defined in the "metadata.json" of the extension.
// It is the extension installation directory name followed by ".sock".
case "darwin":
// e.g. "/Users/felipecruz/.docker/ext-sockets/felipecruz_vackup-docker-extension.sock"
socket = filepath.Join(hd, ".docker", "ext-sockets", safeExtensionDir+".sock")
case "linux":
// e.g. "/home/felipecruz/.docker/desktop/ext-sockets/felipecruz_vackup-docker-extension.sock"
socket = filepath.Join(hd, ".docker", "desktop", "ext-sockets", safeExtensionDir+".sock")
}
logrus.Infof("unix socket: %s", socket)
return net.Dial("unix", socket)
},
},
},
}
return c, nil
}
type PushRequest struct {
Reference string `json:"reference"`
}
func (c *cl) Push(ctx context.Context, ref string, volume string, options VolumePushOptions) error {
auth := options.RegistryAuth
request := PushRequest{
Reference: ref,
}
data, err := json.Marshal(request)
if err != nil {
return err
}
req, err := http.NewRequest("POST", fmt.Sprintf("http://unix/volumes/%s/push", volume), bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("X-Registry-Auth", auth)
req.Header.Set("Content-Type", "application/json")
res, err := c.httpc.Do(req)
if err != nil {
return err
}
if res.StatusCode != http.StatusCreated {
b, _ := io.ReadAll(res.Body)
return errors.New(string(b))
}
return err
}
type PullRequest struct {
Reference string `json:"reference"`
}
func (c *cl) Pull(ctx context.Context, reference string, volume string, options VolumePullOptions) error {
auth := options.RegistryAuth
request := PullRequest{
Reference: reference,
}
data, err := json.Marshal(request)
if err != nil {
return err
}
req, err := http.NewRequest("POST", fmt.Sprintf("http://unix/volumes/%s/pull", volume), bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("X-Registry-Auth", auth)
req.Header.Set("Content-Type", "application/json")
res, err := c.httpc.Do(req)
if res.StatusCode != http.StatusCreated {
b, _ := io.ReadAll(res.Body)
return errors.New(string(b))
}
return err
}

Просмотреть файл

@ -1,130 +0,0 @@
//go:build windows
// +build windows
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/sirupsen/logrus"
"golang.org/x/text/cases"
"golang.org/x/text/language"
npipe "gopkg.in/natefinch/npipe.v2"
)
type VolumePushOptions struct {
RegistryAuth string
}
type VolumePullOptions struct {
RegistryAuth string
}
type Client interface {
Push(ctx context.Context, reference string, volume string, options VolumePushOptions) error
Pull(ctx context.Context, reference string, volume string, options VolumePullOptions) error
}
type cl struct {
httpc http.Client
}
// New returns a new volume client
func New(extensionDir string) (Client, error) {
logrus.Infof("extensionDir not used on Windows as the socket name doesn't depend on it.")
metadataExtensionSocket := "ext.sock" // name of the socket in metadata.json
c := &cl{
httpc: http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
var socket string
metadataExtensionSocket = strings.TrimSuffix(strings.ReplaceAll(metadataExtensionSocket, "-", ""), ".sock")
socket = `\\.\pipe\dockerDesktopPlugin` + cases.Title(language.English, cases.NoLower).String(metadataExtensionSocket)
logrus.Infof("npipe: %s", socket)
return npipe.Dial(socket)
},
},
},
}
return c, nil
}
type PushRequest struct {
Reference string `json:"reference"`
}
func (c *cl) Push(ctx context.Context, ref string, volume string, options VolumePushOptions) error {
auth := options.RegistryAuth
request := PushRequest{
Reference: ref,
}
data, err := json.Marshal(request)
if err != nil {
return err
}
req, err := http.NewRequest("POST", fmt.Sprintf("http://unix/volumes/%s/push", volume), bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("X-Registry-Auth", auth)
req.Header.Set("Content-Type", "application/json")
res, err := c.httpc.Do(req)
if err != nil {
return err
}
if res.StatusCode != http.StatusCreated {
b, _ := io.ReadAll(res.Body)
return errors.New(string(b))
}
return err
}
type PullRequest struct {
Reference string `json:"reference"`
}
func (c *cl) Pull(ctx context.Context, reference string, volume string, options VolumePullOptions) error {
auth := options.RegistryAuth
request := PullRequest{
Reference: reference,
}
data, err := json.Marshal(request)
if err != nil {
return err
}
req, err := http.NewRequest("POST", fmt.Sprintf("http://unix/volumes/%s/pull", volume), bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("X-Registry-Auth", auth)
req.Header.Set("Content-Type", "application/json")
res, err := c.httpc.Do(req)
if res.StatusCode != http.StatusCreated {
b, _ := io.ReadAll(res.Body)
return errors.New(string(b))
}
return err
}

Просмотреть файл

@ -1,4 +1,4 @@
module github.com/docker/volumes-share-client
module github.com/docker/docker-credentials-client
go 1.17
@ -8,8 +8,6 @@ require (
github.com/docker/docker v20.10.8+incompatible
github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0
golang.org/x/text v0.3.7
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce
)
require (
@ -41,6 +39,7 @@ require (
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
google.golang.org/grpc v1.33.2 // indirect
google.golang.org/protobuf v1.28.1 // indirect

Просмотреть файл

@ -1010,8 +1010,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=

Просмотреть файл

@ -2,44 +2,26 @@ package main
import (
"context"
"fmt"
"os"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/registry"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
func main() {
var extensionDir string
app := &cli.App{
Name: "vpush",
Usage: "Push/Pull volumes",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "extension-dir",
Required: true,
Usage: "The directory name where the extension is installed.",
Destination: &extensionDir,
},
},
Before: func(c *cli.Context) error {
return nil
},
Name: "Docker Credentials client",
Usage: "Read the Docker credentials.",
Commands: []*cli.Command{
{
Name: "pull",
UsageText: "vs-client pull REFERENCE VOLUME",
Name: "get-creds",
UsageText: "docker-credentials-client get-creds REFERENCE",
Description: "Returns the Docker credentials (in base64) for the registry that is specified in the REFERENCE.",
Action: func(c *cli.Context) error {
ref := c.Args().Get(0)
volume := c.Args().Get(1)
volumeClient, err := New(extensionDir)
if err != nil {
return err
}
parsedRef, err := reference.ParseNormalizedNamed(ref)
if err != nil {
@ -56,58 +38,13 @@ func main() {
return err
}
encodedAuth, err := encodeAuthToBase64(types.AuthConfig(authConfig))
encodedAuth, err := encodeAuthToBase64(authConfig)
if err != nil {
return err
}
return volumeClient.Pull(context.Background(), parsedRef.String(), volume, VolumePullOptions{
RegistryAuth: encodedAuth,
})
},
},
{
Name: "push",
UsageText: "vs-client push REFERENCE VOLUME",
Action: func(c *cli.Context) error {
ref := c.Args().Get(0)
volume := c.Args().Get(1)
volumeClient, err := New(extensionDir)
if err != nil {
return err
}
parsedRef, err := reference.ParseNormalizedNamed(ref)
if err != nil {
return err
}
logrus.Infof("parsedRef: %+v", parsedRef)
repoInfo, err := registry.ParseRepositoryInfo(parsedRef)
if err != nil {
return err
}
logrus.Infof("repoInfo.Index: %+v", repoInfo.Index)
authConfig, err := resolveAuthConfig(context.Background(), repoInfo.Index)
if err != nil {
return err
}
logrus.Infof("authConfig.Username: %s", authConfig.Username)
logrus.Infof("authConfig.ServerAddress: %s", authConfig.ServerAddress)
encodedAuth, err := encodeAuthToBase64(types.AuthConfig(authConfig))
if err != nil {
return err
}
return volumeClient.Push(context.Background(), parsedRef.String(), volume, VolumePushOptions{
RegistryAuth: encodedAuth,
})
fmt.Print(encodedAuth)
return nil
},
},
},

Просмотреть файл

@ -21,17 +21,17 @@
{
"darwin": [
{
"path": "/host/darwin-amd64/volumes-share-client"
"path": "/host/darwin-amd64/docker-credentials-client"
}
],
"linux": [
{
"path": "/host/linux-amd64/volumes-share-client"
"path": "/host/linux-amd64/docker-credentials-client"
}
],
"windows": [
{
"path": "/host/windows-amd64/volumes-share-client.exe"
"path": "/host/windows-amd64/docker-credentials-client.exe"
}
]
}

Просмотреть файл

@ -1,2 +0,0 @@
// The Extension SDK version from which the push/pull to a registry features will be compatible.
export const USE_REGISTRY_VERSION = "0.2.4";

Просмотреть файл

@ -28,7 +28,6 @@ import { useExportToImage } from "../hooks/useExportToImage";
import { NewImageInput } from "./NewImageInput";
import { usePushVolumeToRegistry } from "../hooks/usePushVolumeToRegistry";
import { RegistryImageInput } from "./RegistryImageInput";
import { USE_REGISTRY_VERSION } from "../common/version";
const ddClient = createDockerDesktopClient();
@ -39,8 +38,6 @@ interface Props {
export default function ExportDialog({ open, onClose }: Props) {
const context = useContext(MyContext);
const sdkVersion = context.store.sdkVersion;
const canUseRegistry = sdkVersion >= USE_REGISTRY_VERSION;
const [fromRadioValue, setFromRadioValue] = useState<
"directory" | "local-image" | "new-image" | "push-registry"
@ -270,7 +267,7 @@ export default function ExportDialog({ open, onClose }: Props) {
{renderDirectoryRadioButton()}
{renderLocalImageRadioButton()}
{renderNewImageRadioButton()}
{canUseRegistry && renderPushToRegistryRadioButton()}
{renderPushToRegistryRadioButton()}
</RadioGroup>
</FormControl>
</DialogContent>

Просмотреть файл

@ -28,7 +28,6 @@ import { MyContext } from "..";
import { VolumeOrInput } from "./VolumeOrInput";
import { RegistryImageInput } from "./RegistryImageInput";
import { usePullFromRegistry } from "../hooks/usePullFromRegistry";
import { USE_REGISTRY_VERSION } from "../common/version";
const ddClient = createDockerDesktopClient();
@ -52,8 +51,6 @@ export default function ImportDialog({ volumes, open, onClose }: Props) {
// when executed from a Volume context we don't need to create it.
const context = useContext(MyContext);
const selectedVolumeName = context.store.volume?.volumeName;
const sdkVersion = context.store.sdkVersion;
const canUseRegistry = sdkVersion >= USE_REGISTRY_VERSION;
const { createVolume, isInProgress: isCreating } = useCreateVolume();
const { importVolume, isInProgress: isImportingFromPath } =
@ -238,7 +235,7 @@ export default function ImportDialog({ volumes, open, onClose }: Props) {
>
{renderFormControlFile()}
{renderImageRadioButton()}
{canUseRegistry && renderPullFromRegistryRadioButton()}
{renderPullFromRegistryRadioButton()}
</RadioGroup>
</FormControl>

Просмотреть файл

@ -20,33 +20,58 @@ export const usePullFromRegistry = () => {
setIsLoading(true);
return ddClient.extension.host.cli
.exec("volumes-share-client", [
"--extension-dir",
process.env["REACT_APP_EXTENSION_INSTALLATION_DIR_NAME"],
"pull",
imageName,
volumeId || context.store.volume.volumeName,
])
.exec("docker-credentials-client", ["get-creds", imageName])
.then((result) => {
sendNotification(
`Volume ${
volumeId || context.store.volume.volumeName
} pulled as ${imageName} from registry`,
[
{
name: "See volume",
onClick: () =>
ddClient.desktopUI.navigate.viewVolume(
volumeId || context.store.volume.volumeName
),
},
]
);
let data = { reference: imageName, base64EncodedAuth: "" };
const base64EncodedAuth = result.stdout;
// If the decoded base64 string is "e30=", it means is an empty JSON "{}"
if (base64EncodedAuth !== "e30=") {
data.base64EncodedAuth = base64EncodedAuth;
}
const requestConfig = {
method: "POST",
url: `/volumes/${context.store.volume.volumeName}/pull`,
headers: {},
data: data,
};
ddClient.extension.vm.service
.request(requestConfig)
.then((result) => {
sendNotification(
`Volume ${
volumeId || context.store.volume.volumeName
} pulled as ${imageName} from registry`,
[
{
name: "See volume",
onClick: () =>
ddClient.desktopUI.navigate.viewVolume(
volumeId || context.store.volume.volumeName
),
},
]
);
})
.catch((error) => {
console.error(error);
sendNotification(
`Failed to pull volume ${
volumeId || context.store.volume.volumeName
} as ${imageName} from registry: ${
error.message
}. HTTP status code: ${error.statusCode}`,
[],
"error"
);
});
})
.catch((error) => {
console.error(error);
sendNotification(
`Failed to pull volume ${
`Failed to get Docker credentials when pulling volume ${
volumeId || context.store.volume.volumeName
} as ${imageName} from registry: ${
error.message

Просмотреть файл

@ -13,38 +13,60 @@ export const usePushVolumeToRegistry = () => {
const pushVolumeToRegistry = ({ imageName }: { imageName: string }) => {
setIsLoading(true);
return ddClient.extension.host.cli
.exec("volumes-share-client", [
"--extension-dir",
process.env["REACT_APP_EXTENSION_INSTALLATION_DIR_NAME"],
"push",
imageName,
context.store.volume.volumeName,
])
ddClient.extension.host.cli
.exec("docker-credentials-client", ["get-creds", imageName])
.then((result) => {
sendNotification(
`Volume ${context.store.volume.volumeName} pushed as ${imageName} to registry`
);
let data = { reference: imageName, base64EncodedAuth: "" };
const base64EncodedAuth = result.stdout;
// If the decoded base64 string is "e30=", it means is an empty JSON "{}"
if (base64EncodedAuth !== "e30=") {
data.base64EncodedAuth = base64EncodedAuth;
}
const requestConfig = {
method: "POST",
url: `/volumes/${context.store.volume.volumeName}/push`,
headers: {},
data: data,
};
ddClient.extension.vm.service
.request(requestConfig)
.then((result) => {
sendNotification(
`Volume ${context.store.volume.volumeName} pushed as ${imageName} to registry`
);
})
.catch((error) => {
console.error(error);
if (
error?.message.includes(
"denied: requested access to the resource is denied"
)
) {
sendNotification(
`Access denied when trying to push to ${imageName}.
Are you logged in? If so, check your permissions.`,
[],
"error"
);
} else {
sendNotification(
`Failed to push volume ${context.store.volume.volumeName} as ${imageName} to registry: ${error.message}. HTTP status code: ${error.statusCode}`,
[],
"error"
);
}
});
})
.catch((error) => {
if (
error?.stderr.includes(
"denied: requested access to the resource is denied"
)
) {
sendNotification(
`Access denied when trying to push to ${imageName}.
Are you logged in? If so, check your permissions.`,
[],
"error"
);
} else {
sendNotification(
`Failed to push volume ${context.store.volume.volumeName} as ${imageName} to registry: ${error.message}. HTTP status code: ${error.statusCode}`,
[],
"error"
);
}
console.error(error);
sendNotification(
`Failed to get Docker credentials when pushing volume ${context.store.volume.volumeName} as ${imageName} to registry: ${error.message}. HTTP status code: ${error.statusCode}`,
[],
"error"
);
})
.finally(() => {
setIsLoading(false);

Просмотреть файл

@ -1,19 +1,15 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import CssBaseline from "@mui/material/CssBaseline";
import { DockerMuiThemeProvider } from "@docker/docker-mui-theme";
import { createDockerDesktopClient } from "@docker/extension-api-client";
import { App } from "./App";
import type { IVolumeRow } from "./hooks/useGetVolumes";
import { NotificationProvider } from "./NotificationContext";
const ddClient = createDockerDesktopClient();
interface IAppContext {
store: {
volume: IVolumeRow | null;
sdkVersion: string;
};
actions: {
setVolume(v: IVolumeRow | null): void;
@ -25,7 +21,6 @@ export const MyContext = React.createContext<IAppContext>(null);
const AppProvider = (props) => {
const [store, setStore] = useState({
volume: null,
sdkVersion: "",
});
const actions = {
@ -33,19 +28,6 @@ const AppProvider = (props) => {
setStore((oldStore) => ({ ...oldStore, volume: value })),
};
useEffect(() => {
ddClient.docker.cli
.exec("extension version", [])
.then((output) => {
const sdkVersion = output.lines()[1].split(": ")[1];
setStore((oldStore) => ({ ...oldStore, sdkVersion }));
})
.catch((err) => {
console.error(err);
setStore((oldstore) => ({ ...oldstore, sdkVersion: "" }));
});
}, []);
return (
<MyContext.Provider value={{ actions, store }}>
{props.children}

Просмотреть файл

@ -8,7 +8,10 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/felipecruz91/vackup-docker-extension/internal/log"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
@ -19,6 +22,18 @@ type VolumeSize struct {
}
func GetVolumesSize(ctx context.Context, cli *client.Client, volumeName string) map[string]VolumeSize {
// Ensure the image is present before creating the container
reader, err := cli.ImagePull(ctx, "docker.io/justincormack/nsenter1", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
log.Error(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
log.Error(err)
}
resp, err := cli.ContainerCreate(ctx, &container.Config{
Tty: true,
Cmd: []string{"/bin/sh", "-c", "du -d 0 /var/lib/docker/volumes/" + volumeName},

Просмотреть файл

@ -3,9 +3,11 @@ package handler
import (
"fmt"
"github.com/docker/docker/pkg/stdcopy"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/docker/docker/api/types"
@ -38,7 +40,7 @@ func (h *Handler) ExportVolume(ctx echo.Context) error {
stoppedContainers, err := backend.StopContainersAttachedToVolume(ctx.Request().Context(), h.DockerClient, volumeName)
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
// Export
@ -58,6 +60,18 @@ func (h *Handler) ExportVolume(ctx echo.Context) error {
}
log.Infof("binds: %+v", binds)
// Ensure the image is present before creating the container
reader, err := h.DockerClient.ImagePull(ctx.Request().Context(), "docker.io/library/busybox", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
return err
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
return err
}
resp, err := h.DockerClient.ContainerCreate(ctx.Request().Context(), &container.Config{
Image: "docker.io/library/busybox",
AttachStdout: true,
@ -69,12 +83,12 @@ func (h *Handler) ExportVolume(ctx echo.Context) error {
}, nil, nil, "")
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
if err := h.DockerClient.ContainerStart(ctx.Request().Context(), resp.ID, types.ContainerStartOptions{}); err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err)
return ctx.String(http.StatusInternalServerError, err.Error())
}
var exitCode int64
@ -83,7 +97,7 @@ func (h *Handler) ExportVolume(ctx echo.Context) error {
case err := <-errCh:
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
case status := <-statusCh:
log.Infof("status: %#+v\n", status)
@ -93,30 +107,30 @@ func (h *Handler) ExportVolume(ctx echo.Context) error {
out, err := h.DockerClient.ContainerLogs(ctx.Request().Context(), resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, out)
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
if exitCode != 0 {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("container exited with status code %d\n", exitCode))
return ctx.String(http.StatusInternalServerError, fmt.Sprintf("container exited with status code %d\n", exitCode))
}
err = h.DockerClient.ContainerRemove(ctx.Request().Context(), resp.ID, types.ContainerRemoveOptions{})
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
// Start container(s)
err = backend.StartContainersAttachedToVolume(ctx.Request().Context(), h.DockerClient, stoppedContainers)
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
return ctx.String(http.StatusCreated, "")

Просмотреть файл

@ -27,10 +27,8 @@ func pullImagesIfNotPresent(ctx context.Context, cli *client.Client) {
g, ctx := errgroup.WithContext(ctx)
images := []string{
"docker.io/library/alpine",
"docker.io/library/busybox",
"docker.io/justincormack/nsenter1",
"docker.io/library/registry:2",
}
for _, image := range images {

Просмотреть файл

@ -8,8 +8,10 @@ import (
"github.com/felipecruz91/vackup-docker-extension/internal/backend"
"github.com/felipecruz91/vackup-docker-extension/internal/log"
"github.com/labstack/echo"
"io"
"net/http"
"os"
"runtime"
)
func (h *Handler) ImportTarGzFile(ctx echo.Context) error {
@ -30,7 +32,7 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error {
stoppedContainers, err := backend.StopContainersAttachedToVolume(ctx.Request().Context(), h.DockerClient, volumeName)
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
// Import
@ -39,6 +41,19 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error {
path + ":" + "/vackup",
}
log.Infof("binds: %+v", binds)
// Ensure the image is present before creating the container
reader, err := h.DockerClient.ImagePull(ctx.Request().Context(), "docker.io/library/busybox", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
return err
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
return err
}
resp, err := h.DockerClient.ContainerCreate(ctx.Request().Context(), &container.Config{
Image: "docker.io/library/busybox",
AttachStdout: true,
@ -52,12 +67,12 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error {
}, nil, nil, "")
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
if err := h.DockerClient.ContainerStart(ctx.Request().Context(), resp.ID, types.ContainerStartOptions{}); err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err)
return ctx.String(http.StatusInternalServerError, err.Error())
}
var exitCode int64
@ -66,7 +81,7 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error {
case err := <-errCh:
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
case status := <-statusCh:
log.Infof("status: %#+v\n", status)
@ -76,30 +91,30 @@ func (h *Handler) ImportTarGzFile(ctx echo.Context) error {
out, err := h.DockerClient.ContainerLogs(ctx.Request().Context(), resp.ID, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, out)
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
if exitCode != 0 {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("container exited with status code %d\n", exitCode))
return ctx.String(http.StatusInternalServerError, fmt.Sprintf("container exited with status code %d\n", exitCode))
}
err = h.DockerClient.ContainerRemove(ctx.Request().Context(), resp.ID, types.ContainerRemoveOptions{})
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
// Start container(s)
err = backend.StartContainersAttachedToVolume(ctx.Request().Context(), h.DockerClient, stoppedContainers)
if err != nil {
log.Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return ctx.String(http.StatusInternalServerError, err.Error())
}
return ctx.String(http.StatusOK, "")

Просмотреть файл

@ -13,7 +13,8 @@ import (
)
type PullRequest struct {
Reference string `json:"reference"`
Reference string `json:"reference"`
Base64EncodedAuth string `json:"base64EncodedAuth"`
}
// PullVolume pulls a volume from a registry.
@ -30,6 +31,13 @@ func (h *Handler) PullVolume(ctx echo.Context) error {
log.Infof("reference: %s", request.Reference)
logrus.Infof("received pull request for volume %s\n", volumeName)
// To provide backwards compatibility with older versions of Docker Desktop,
// we're passing the encoded auth in the body of the request instead of in the headers.
// encodedAuth := ctx.Request().Header.Get("X-Registry-Auth")
if request.Base64EncodedAuth == "" {
request.Base64EncodedAuth = "Cg==" // from running: echo "" | base64
}
if volumeName == "" {
return ctx.String(http.StatusBadRequest, "volume is required")
}
@ -41,14 +49,9 @@ func (h *Handler) PullVolume(ctx echo.Context) error {
log.Infof("parsedRef.String(): %s", parsedRef.String())
// Pull the volume (image) from registry
encodedAuth := ctx.Request().Header.Get("X-Registry-Auth")
if encodedAuth == "" {
encodedAuth = "Cg==" // from running: echo "" | base64
}
log.Infof("Pulling image %s...", parsedRef.String())
pullResp, err := h.DockerClient.ImagePull(ctxReq, parsedRef.String(), dockertypes.ImagePullOptions{
RegistryAuth: encodedAuth,
RegistryAuth: request.Base64EncodedAuth,
})
if err != nil {

Просмотреть файл

@ -13,12 +13,14 @@ import (
"github.com/felipecruz91/vackup-docker-extension/internal/backend"
"github.com/labstack/echo"
"github.com/stretchr/testify/require"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@ -51,7 +53,7 @@ func TestPullVolume(t *testing.T) {
// Setup
e := echo.New()
requestJSON := fmt.Sprintf(`{"reference": "%s"}`, imageID)
requestJSON := fmt.Sprintf(`{"reference": "%s", "base64EncodedAuth": ""}`, imageID)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(requestJSON))
req.Header.Add("Content-Type", "application/json")
rec := httptest.NewRecorder()
@ -63,6 +65,17 @@ func TestPullVolume(t *testing.T) {
// Provision a registry with an image (which represents a volume) ready to pull:
// Run a local registry
reader, err := cli.ImagePull(context.Background(), "docker.io/library/registry:2", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
}
resp2, err := cli.ContainerCreate(context.Background(), &container.Config{
Image: "docker.io/library/registry:2",
ExposedPorts: map[nat.Port]struct{}{
@ -195,10 +208,9 @@ func TestPullVolumeUsingCorrectAuth(t *testing.T) {
// Setup
e := echo.New()
requestJSON := fmt.Sprintf(`{"reference": "%s"}`, imageID)
requestJSON := fmt.Sprintf(`{"reference": "%s", "base64EncodedAuth": "%s"}`, imageID, encodedAuth)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(requestJSON))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Registry-Auth", encodedAuth)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/volumes/:volume/pull")
@ -212,6 +224,17 @@ func TestPullVolumeUsingCorrectAuth(t *testing.T) {
t.Fatal(err)
}
reader, err := cli.ImagePull(context.Background(), "docker.io/library/registry:2", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
}
resp2, err := cli.ContainerCreate(context.Background(), &container.Config{
Image: "docker.io/library/registry:2",
ExposedPorts: map[nat.Port]struct{}{
@ -352,10 +375,9 @@ func TestPullVolumeUsingWrongAuthShouldFail(t *testing.T) {
// Setup
e := echo.New()
requestJSON := fmt.Sprintf(`{"reference": "%s"}`, imageID)
requestJSON := fmt.Sprintf(`{"reference": "%s", "base64EncodedAuth": "%s"}`, imageID, encodedAuth)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(requestJSON))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Registry-Auth", encodedAuth)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/volumes/:volume/pull")
@ -369,6 +391,17 @@ func TestPullVolumeUsingWrongAuthShouldFail(t *testing.T) {
t.Fatal(err)
}
reader, err := cli.ImagePull(context.Background(), "docker.io/library/registry:2", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
}
resp2, err := cli.ContainerCreate(context.Background(), &container.Config{
Image: "docker.io/library/registry:2",
ExposedPorts: map[nat.Port]struct{}{

Просмотреть файл

@ -11,11 +11,11 @@ import (
"github.com/felipecruz91/vackup-docker-extension/internal/backend"
"github.com/felipecruz91/vackup-docker-extension/internal/log"
"github.com/labstack/echo"
"github.com/sirupsen/logrus"
)
type PushRequest struct {
Reference string `json:"reference"`
Reference string `json:"reference"`
Base64EncodedAuth string `json:"base64EncodedAuth"`
}
type PushErrorLine struct {
@ -38,7 +38,14 @@ func (h *Handler) PushVolume(ctx echo.Context) error {
volumeName := ctx.Param("volume")
log.Infof("volumeName: %s", volumeName)
log.Infof("reference: %s", request.Reference)
logrus.Infof("received push request for volume %s\n", volumeName)
log.Infof("received push request for volume %s\n", volumeName)
// To provide backwards compatibility with older versions of Docker Desktop,
// we're passing the encoded auth in the body of the request instead of in the headers.
// encodedAuth := ctx.Request().Header.Get("X-Registry-Auth")
if request.Base64EncodedAuth == "" {
request.Base64EncodedAuth = "Cg==" // from running: echo "" | base64
}
if volumeName == "" {
return ctx.String(http.StatusBadRequest, "volume is required")
@ -57,12 +64,8 @@ func (h *Handler) PushVolume(ctx echo.Context) error {
}
// Push the image to registry
encodedAuth := ctx.Request().Header.Get("X-Registry-Auth")
if encodedAuth == "" {
encodedAuth = "Cg==" // from running: echo "" | base64
}
pushResp, err := h.DockerClient.ImagePush(ctxReq, parsedRef.String(), dockertypes.ImagePushOptions{
RegistryAuth: encodedAuth,
RegistryAuth: request.Base64EncodedAuth,
})
if err != nil {
log.Error(err)

Просмотреть файл

@ -49,7 +49,7 @@ func TestPushVolume(t *testing.T) {
// Setup
e := echo.New()
requestJSON := fmt.Sprintf(`{"reference": "%s"}`, imageID)
requestJSON := fmt.Sprintf(`{"reference": "%s", "base64EncodedAuth": ""}`, imageID)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(requestJSON))
req.Header.Add("Content-Type", "application/json")
rec := httptest.NewRecorder()
@ -74,7 +74,6 @@ func TestPushVolume(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
@ -95,6 +94,17 @@ func TestPushVolume(t *testing.T) {
containerID = resp.ID
// Run a local registry
reader, err = cli.ImagePull(context.Background(), "docker.io/library/registry:2", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
}
resp2, err := cli.ContainerCreate(c.Request().Context(), &container.Config{
Image: "docker.io/library/registry:2",
ExposedPorts: map[nat.Port]struct{}{
@ -178,10 +188,9 @@ func TestPushVolumeUsingCorrectAuth(t *testing.T) {
// Setup
e := echo.New()
requestJSON := fmt.Sprintf(`{"reference": "%s"}`, imageID)
requestJSON := fmt.Sprintf(`{"reference": "%s", "base64EncodedAuth": "%s"}`, imageID, encodedAuth)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(requestJSON))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Registry-Auth", encodedAuth)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/volumes/:volume/push")
@ -204,7 +213,6 @@ func TestPushVolumeUsingCorrectAuth(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
@ -230,6 +238,17 @@ func TestPushVolumeUsingCorrectAuth(t *testing.T) {
t.Fatal(err)
}
reader, err = cli.ImagePull(context.Background(), "docker.io/library/registry:2", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
}
resp2, err := cli.ContainerCreate(c.Request().Context(), &container.Config{
Image: "docker.io/library/registry:2",
ExposedPorts: map[nat.Port]struct{}{
@ -333,10 +352,9 @@ func TestPushVolumeUsingWrongAuthShouldFail(t *testing.T) {
// Setup
e := echo.New()
requestJSON := fmt.Sprintf(`{"reference": "%s"}`, imageID)
requestJSON := fmt.Sprintf(`{"reference": "%s", "base64EncodedAuth": "%s"}`, imageID, encodedAuth)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(requestJSON))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-Registry-Auth", encodedAuth)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/volumes/:volume/push")
@ -359,7 +377,6 @@ func TestPushVolumeUsingWrongAuthShouldFail(t *testing.T) {
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
@ -385,6 +402,17 @@ func TestPushVolumeUsingWrongAuthShouldFail(t *testing.T) {
t.Fatal(err)
}
reader, err = cli.ImagePull(c.Request().Context(), "docker.io/library/registry:2", types.ImagePullOptions{
Platform: "linux/" + runtime.GOARCH,
})
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(os.Stdout, reader)
if err != nil {
t.Fatal(err)
}
resp2, err := cli.ContainerCreate(c.Request().Context(), &container.Config{
Image: "docker.io/library/registry:2",
ExposedPorts: map[nat.Port]struct{}{