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:
Родитель
7a8979969f
Коммит
66b768117a
|
@ -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
|
||||
|
||||
|
|
4
Makefile
4
Makefile
|
@ -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:
|
||||
|
|
|
@ -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`.
|
||||
|
|
142
client/client.go
142
client/client.go
|
@ -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{}{
|
||||
|
|
Загрузка…
Ссылка в новой задаче