Merge pull request #36 from docker/enforce-hub-login

Enforce hub login
This commit is contained in:
Silvin Lubecki 2020-10-12 09:58:24 +02:00 коммит произвёл GitHub
Родитель 7e4ce66438 5e2bd40361
Коммит 940ef126a6
30 изменённых файлов: 334 добавлений и 211 удалений

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

@ -1,5 +1,5 @@
name: Build PR
on: [push,pull_request]
on: [pull_request]
jobs:
lint:
@ -31,7 +31,7 @@ jobs:
env:
GO111MODULE: "on"
E2E_HUB_USERNAME: ${{ secrets.E2E_HUB_USERNAME }}
E2E_HUB_TOKEN_TOKEN: ${{ secrets.E2E_HUB_TOKEN_TOKEN }}
E2E_HUB_TOKEN: ${{ secrets.E2E_HUB_TOKEN }}
steps:
- name: Docker version
run: docker version

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

@ -55,14 +55,21 @@ e2e-build:
.PHONY: e2e
e2e: e2e-build ## Run the end-to-end tests
@docker run $(E2E_ENV) --rm -v /var/run/docker.sock:/var/run/docker.sock -v $(shell go env GOCACHE):/root/.cache/go-build $(BINARY_NAME):e2e
docker run $(E2E_ENV) --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(shell go env GOCACHE):/root/.cache/go-build \
-v $(shell go env GOMODCACHE):/go/pkg/mod \
$(BINARY_NAME):e2e
test-unit-build:
docker build $(BUILD_ARGS) . --target test-unit -t $(BINARY_NAME):test-unit
.PHONY: test-unit
test-unit: test-unit-build ## Run unit tests
docker run --rm -v $(shell go env GOCACHE):/root/.cache/go-build $(BINARY_NAME):test-unit
docker run --rm \
-v $(shell go env GOCACHE):/root/.cache/go-build \
-v $(shell go env GOMODCACHE):/go/pkg/mod \
$(BINARY_NAME):test-unit
.PHONY: lint
lint: ## Run the go linter

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

@ -23,10 +23,14 @@ import (
"os/signal"
"syscall"
"github.com/cli/cli/utils"
"github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/hub-cli-plugin/internal/commands"
"github.com/docker/hub-cli-plugin/internal/hub"
)
func main() {
@ -43,7 +47,22 @@ func main() {
fmt.Println(err)
os.Exit(1)
}
rootCmd := commands.NewRootCmd(ctx, dockerCli, os.Args[0])
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
hubClient, err := hub.NewClient(authResolver, hub.WithContext(ctx))
if err != nil {
if hub.IsAuthenticationError(err) {
fmt.Println(utils.Red(`You need to be logged in to Docker Hub to use this tool.
Please login to Docker Hub using the "docker login" command.`))
os.Exit(1)
}
fmt.Println(err)
os.Exit(1)
}
rootCmd := commands.NewRootCmd(dockerCli, hubClient, os.Args[0])
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}

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

@ -17,12 +17,14 @@
package e2e
import (
"fmt"
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/docker/cli/cli/config/configfile"
clitypes "github.com/docker/cli/cli/config/types"
"gotest.tools/assert"
"gotest.tools/v3/env"
"gotest.tools/v3/fs"
@ -30,13 +32,25 @@ import (
)
func hubToolCmd(t *testing.T, args ...string) (icmd.Cmd, func()) {
user := os.Getenv("E2E_HUB_USERNAME")
token := os.Getenv("E2E_HUB_TOKEN")
config := configfile.ConfigFile{
AuthConfigs: map[string]clitypes.AuthConfig{"https://index.docker.io/v1/": {
Username: user,
Password: token,
}},
}
data, err := json.Marshal(&config)
assert.NilError(t, err)
pwd, err := os.Getwd()
assert.NilError(t, err)
hubTool := os.Getenv("BINARY")
configDir := fs.NewDir(t, t.Name())
configDir := fs.NewDir(t, t.Name(), fs.WithFile("config.json", string(data)))
cleanup := env.Patch(t, "PATH", os.Getenv("PATH")+getPathSeparator()+filepath.Join(pwd, "..", "bin"))
fmt.Println(os.Getenv("PATH"), os.Getenv("PWD"))
env := append(os.Environ(), "DOCKER_CONFIG="+configDir.Path())
return icmd.Cmd{Command: append([]string{hubTool}, args...), Env: env}, func() { cleanup(); configDir.Remove() }
}

37
e2e/login_test.go Normal file
Просмотреть файл

@ -0,0 +1,37 @@
/*
Copyright 2020 Docker Hub Tool authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"testing"
"gotest.tools/v3/icmd"
)
func TestUserNeedsToBeLoggedIn(t *testing.T) {
cmd, cleanup := hubToolCmd(t, "--version")
// Remove the config file
cleanup()
output := icmd.RunCmd(cmd)
output.Equal(icmd.Expected{
ExitCode: 1,
Out: `You need to be logged in to Docker Hub to use this tool.
Please login to Docker Hub using the "docker login" command
`,
})
}

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

@ -17,11 +17,11 @@
package account
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/hub"
)
const (
@ -29,15 +29,15 @@ const (
)
//NewAccountCmd configures the org manage command
func NewAccountCmd(ctx context.Context, dockerCli command.Cli) *cobra.Command {
func NewAccountCmd(streams command.Streams, hubClient *hub.Client) *cobra.Command {
cmd := &cobra.Command{
Use: accountName,
Long: "Manage organizations",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
RunE: command.ShowHelp(streams.Err()),
}
cmd.AddCommand(
newInfoCmd(ctx, dockerCli, accountName),
newInfoCmd(streams, hubClient, accountName),
)
return cmd
}

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

@ -17,18 +17,14 @@
package account
import (
"context"
"fmt"
"io"
"os"
"text/tabwriter"
"time"
"github.com/cli/cli/utils"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/go-units"
"github.com/spf13/cobra"
@ -45,7 +41,7 @@ type infoOptions struct {
format.Option
}
func newInfoCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newInfoCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts infoOptions
cmd := &cobra.Command{
Use: infoName + " [OPTIONS]",
@ -55,7 +51,7 @@ func newInfoCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
metrics.Send(parent, infoName)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runInfo(ctx, dockerCli, opts)
return runInfo(streams, hubClient, opts)
},
}
opts.AddFormatFlag(cmd.Flags())
@ -63,23 +59,16 @@ func newInfoCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
return cmd
}
func runInfo(ctx context.Context, dockerCli command.Cli, opts infoOptions) error {
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
client, err := hub.NewClient(authResolver)
func runInfo(streams command.Streams, hubClient *hub.Client, opts infoOptions) error {
user, err := hubClient.GetUserInfo()
if err != nil {
return err
}
user, err := client.GetUserInfo()
plan, err := hubClient.GetHubPlan(user.ID)
if err != nil {
return err
}
plan, err := client.GetHubPlan(user.ID)
if err != nil {
return err
}
return opts.Print(os.Stdout, account{user, plan}, printAccount)
return opts.Print(streams.Out(), account{user, plan}, printAccount)
}
func printAccount(out io.Writer, value interface{}) error {

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

@ -17,11 +17,11 @@
package org
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/hub"
)
const (
@ -29,17 +29,17 @@ const (
)
//NewOrgCmd configures the org manage command
func NewOrgCmd(ctx context.Context, dockerCli command.Cli) *cobra.Command {
func NewOrgCmd(streams command.Streams, hubClient *hub.Client) *cobra.Command {
cmd := &cobra.Command{
Use: orgName,
Long: "Manage organizations",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
RunE: command.ShowHelp(streams.Err()),
}
cmd.AddCommand(
newListCmd(ctx, dockerCli, orgName),
newMembersCmd(ctx, dockerCli, orgName),
newTeamsCmd(ctx, dockerCli, orgName),
newListCmd(streams, hubClient, orgName),
newMembersCmd(streams, hubClient, orgName),
newTeamsCmd(streams, hubClient, orgName),
)
return cmd
}

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

@ -17,17 +17,13 @@
package org
import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/format"
@ -58,7 +54,7 @@ type listOptions struct {
format.Option
}
func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newListCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts listOptions
cmd := &cobra.Command{
Use: listName,
@ -68,7 +64,7 @@ func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
metrics.Send(parent, listName)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runList(ctx, dockerCli, opts)
return runList(streams, hubClient, opts)
},
}
opts.AddFormatFlag(cmd.Flags())
@ -76,19 +72,12 @@ func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
return cmd
}
func runList(ctx context.Context, dockerCli command.Cli, opts listOptions) error {
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
client, err := hub.NewClient(authResolver)
func runList(streams command.Streams, hubClient *hub.Client, opts listOptions) error {
organizations, err := hubClient.GetOrganizations()
if err != nil {
return err
}
organizations, err := client.GetOrganizations()
if err != nil {
return err
}
return opts.Print(os.Stdout, organizations, printOrganizations)
return opts.Print(streams.Out(), organizations, printOrganizations)
}
func printOrganizations(out io.Writer, values interface{}) error {

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

@ -17,17 +17,13 @@
package org
import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/format"
@ -55,7 +51,7 @@ type memberOptions struct {
format.Option
}
func newMembersCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newMembersCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts memberOptions
cmd := &cobra.Command{
Use: membersName + " ORGANIZATION",
@ -65,7 +61,7 @@ func newMembersCmd(ctx context.Context, dockerCli command.Cli, parent string) *c
metrics.Send(parent, membersName)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runMembers(ctx, dockerCli, opts, args[0])
return runMembers(streams, hubClient, opts, args[0])
},
}
opts.AddFormatFlag(cmd.Flags())
@ -73,19 +69,12 @@ func newMembersCmd(ctx context.Context, dockerCli command.Cli, parent string) *c
return cmd
}
func runMembers(ctx context.Context, dockerCli command.Cli, opts memberOptions, organization string) error {
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
client, err := hub.NewClient(authResolver)
func runMembers(streams command.Streams, hubClient *hub.Client, opts memberOptions, organization string) error {
members, err := hubClient.GetMembers(organization)
if err != nil {
return err
}
members, err := client.GetMembers(organization)
if err != nil {
return err
}
return opts.Print(os.Stdout, members, printMembers)
return opts.Print(streams.Out(), members, printMembers)
}
func printMembers(out io.Writer, values interface{}) error {

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

@ -17,17 +17,13 @@
package org
import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/format"
@ -56,7 +52,7 @@ type teamsOptions struct {
format.Option
}
func newTeamsCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newTeamsCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts teamsOptions
cmd := &cobra.Command{
Use: teamsName + " ORGANIZATION",
@ -66,7 +62,7 @@ func newTeamsCmd(ctx context.Context, dockerCli command.Cli, parent string) *cob
metrics.Send(parent, teamsName)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runTeams(ctx, dockerCli, opts, args[0])
return runTeams(streams, hubClient, opts, args[0])
},
}
opts.AddFormatFlag(cmd.Flags())
@ -74,19 +70,12 @@ func newTeamsCmd(ctx context.Context, dockerCli command.Cli, parent string) *cob
return cmd
}
func runTeams(ctx context.Context, dockerCli command.Cli, opts teamsOptions, organization string) error {
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
client, err := hub.NewClient(authResolver)
func runTeams(streams command.Streams, hubClient *hub.Client, opts teamsOptions, organization string) error {
teams, err := hubClient.GetTeams(organization)
if err != nil {
return err
}
teams, err := client.GetTeams(organization)
if err != nil {
return err
}
return opts.Print(os.Stdout, teams, printTeams)
return opts.Print(streams.Out(), teams, printTeams)
}
func printTeams(out io.Writer, values interface{}) error {

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

@ -17,11 +17,11 @@
package repo
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/hub"
)
const (
@ -29,16 +29,16 @@ const (
)
//NewRepoCmd configures the repo manage command
func NewRepoCmd(ctx context.Context, dockerCli command.Cli) *cobra.Command {
func NewRepoCmd(streams command.Streams, hubClient *hub.Client) *cobra.Command {
cmd := &cobra.Command{
Use: repoName,
Long: "Manage repositories",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
RunE: command.ShowHelp(streams.Err()),
}
cmd.AddCommand(
newListCmd(ctx, dockerCli, repoName),
newRmCmd(ctx, dockerCli, repoName),
newListCmd(streams, hubClient, repoName),
newRmCmd(streams, hubClient, repoName),
)
return cmd
}

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

@ -17,18 +17,14 @@
package repo
import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"time"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/go-units"
"github.com/spf13/cobra"
@ -67,7 +63,7 @@ type listOptions struct {
all bool
}
func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newListCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts listOptions
cmd := &cobra.Command{
Use: listName + " [ORGANIZATION]",
@ -77,7 +73,7 @@ func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
metrics.Send(parent, listName)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runList(ctx, dockerCli, opts, args)
return runList(streams, hubClient, opts, args)
},
}
cmd.Flags().BoolVar(&opts.all, "all", false, "Fetch all available repositories")
@ -86,30 +82,22 @@ func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
return cmd
}
func runList(ctx context.Context, dockerCli command.Cli, opts listOptions, args []string) error {
var account string
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
authConfig := command.ResolveAuthConfig(ctx, dockerCli, hub)
account = authConfig.Username
return authConfig
}
var clientOps []hub.ClientOp
func runList(streams command.Streams, hubClient *hub.Client, opts listOptions, args []string) error {
account := hubClient.AuthConfig.Username
if opts.all {
clientOps = append(clientOps, hub.WithAllElements())
}
client, err := hub.NewClient(authResolver, clientOps...)
if err != nil {
return err
if err := hubClient.Apply(hub.WithAllElements()); err != nil {
return err
}
}
if len(args) > 0 {
account = args[0]
}
repositories, err := client.GetRepositories(account)
repositories, err := hubClient.GetRepositories(account)
if err != nil {
return err
}
return opts.Print(os.Stdout, repositories, printRepositories)
return opts.Print(streams.Out(), repositories, printRepositories)
}
func printRepositories(out io.Writer, values interface{}) error {

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

@ -18,16 +18,12 @@ package repo
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/hub"
@ -42,7 +38,7 @@ type rmOptions struct {
force bool
}
func newRmCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newRmCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts rmOptions
cmd := &cobra.Command{
Use: rmName + " [OPTIONS] REPOSITORY",
@ -52,7 +48,7 @@ func newRmCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.
metrics.Send(parent, rmName)
},
RunE: func(_ *cobra.Command, args []string) error {
return runRm(ctx, dockerCli, opts, args[0])
return runRm(streams, hubClient, opts, args[0])
},
}
cmd.Flags().BoolVar(&opts.force, "force", false, "Force deletion of the repository")
@ -60,7 +56,7 @@ func newRmCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.
return cmd
}
func runRm(ctx context.Context, dockerCli command.Cli, opts rmOptions, repository string) error {
func runRm(streams command.Streams, hubClient *hub.Client, opts rmOptions, repository string) error {
ref, err := reference.Parse(repository)
if err != nil {
return err
@ -71,8 +67,8 @@ func runRm(ctx context.Context, dockerCli command.Cli, opts rmOptions, repositor
}
if !opts.force {
fmt.Println("Please type the name of your repository to confirm deletion:", namedRef.Name())
reader := bufio.NewReader(os.Stdin)
fmt.Fprintln(streams.Out(), "Please type the name of your repository to confirm deletion:", namedRef.Name())
reader := bufio.NewReader(streams.In())
input, _ := reader.ReadString('\n')
input = strings.ToLower(strings.TrimSpace(input))
if input != namedRef.Name() {
@ -80,16 +76,9 @@ func runRm(ctx context.Context, dockerCli command.Cli, opts rmOptions, repositor
}
}
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
client, err := hub.NewClient(authResolver)
if err != nil {
if err := hubClient.RemoveRepository(namedRef.Name()); err != nil {
return err
}
if err := client.RemoveRepository(namedRef.Name()); err != nil {
return err
}
fmt.Println("Deleted", repository)
fmt.Fprintln(streams.Out(), "Deleted", repository)
return nil
}

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

@ -17,7 +17,6 @@
package commands
import (
"context"
"fmt"
"github.com/docker/cli/cli/command"
@ -28,6 +27,7 @@ import (
"github.com/docker/hub-cli-plugin/internal/commands/org"
"github.com/docker/hub-cli-plugin/internal/commands/repo"
"github.com/docker/hub-cli-plugin/internal/commands/tag"
"github.com/docker/hub-cli-plugin/internal/hub"
)
type options struct {
@ -35,7 +35,7 @@ type options struct {
}
//NewRootCmd returns the main command
func NewRootCmd(ctx context.Context, dockerCli command.Cli, name string) *cobra.Command {
func NewRootCmd(streams command.Streams, hubClient *hub.Client, name string) *cobra.Command {
var flags options
cmd := &cobra.Command{
Use: name,
@ -52,10 +52,10 @@ func NewRootCmd(ctx context.Context, dockerCli command.Cli, name string) *cobra.
cmd.Flags().BoolVar(&flags.showVersion, "version", false, "Display version of the scan plugin")
cmd.AddCommand(
account.NewAccountCmd(ctx, dockerCli),
org.NewOrgCmd(ctx, dockerCli),
repo.NewRepoCmd(ctx, dockerCli),
tag.NewTagCmd(ctx, dockerCli),
account.NewAccountCmd(streams, hubClient),
org.NewOrgCmd(streams, hubClient),
repo.NewRepoCmd(streams, hubClient),
tag.NewTagCmd(streams, hubClient),
)
return cmd
}

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

@ -17,11 +17,11 @@
package tag
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/hub"
)
const (
@ -29,17 +29,17 @@ const (
)
//NewTagCmd configures the tag manage command
func NewTagCmd(ctx context.Context, dockerCli command.Cli) *cobra.Command {
func NewTagCmd(streams command.Streams, hubClient *hub.Client) *cobra.Command {
cmd := &cobra.Command{
Use: tagName,
Long: "Manage tags",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
RunE: command.ShowHelp(streams.Err()),
}
cmd.AddCommand(
newInspectCmd(ctx, dockerCli, tagName),
newListCmd(ctx, dockerCli, tagName),
newRmCmd(ctx, dockerCli, tagName),
newInspectCmd(streams, hubClient, tagName),
newListCmd(streams, hubClient, tagName),
newRmCmd(streams, hubClient, tagName),
)
return cmd
}

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

@ -17,22 +17,23 @@
package tag
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"text/tabwriter"
"github.com/containerd/containerd/images"
"github.com/docker/buildx/util/imagetools"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
clitypes "github.com/docker/cli/cli/config/types"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/go-units"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/hub"
"github.com/docker/hub-cli-plugin/internal/metrics"
)
@ -44,7 +45,7 @@ type inspectOptions struct {
format string
}
func newInspectCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newInspectCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
Use: inspectName + " [OPTIONS] REPOSITORY:TAG",
@ -54,7 +55,7 @@ func newInspectCmd(ctx context.Context, dockerCli command.Cli, parent string) *c
metrics.Send(parent, inspectName)
},
RunE: func(_ *cobra.Command, args []string) error {
return runInspect(ctx, dockerCli, opts, args[0])
return runInspect(streams, hubClient, opts, args[0])
},
}
cmd.Flags().StringVar(&opts.format, "format", "", `Print original manifest ("json")`)
@ -62,18 +63,20 @@ func newInspectCmd(ctx context.Context, dockerCli command.Cli, parent string) *c
return cmd
}
func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions, image string) error {
func runInspect(streams command.Streams, hubClient *hub.Client, opts inspectOptions, image string) error {
resolver := imagetools.New(imagetools.Opt{
Auth: dockerCli.ConfigFile(),
Auth: &authResolver{
authConfig: convert(hubClient.AuthConfig),
},
})
raw, descriptor, err := resolver.Get(ctx, image)
raw, descriptor, err := resolver.Get(hubClient.Ctx, image)
if err != nil {
return err
}
if opts.format == "json" {
fmt.Printf("%s", raw) // avoid newline to keep digest
fmt.Fprintf(streams.Out(), "%s", raw) // avoid newline to keep digest
return nil
} else if opts.format != "" {
return fmt.Errorf("unsupported format type: %q", opts.format)
@ -83,15 +86,15 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions,
// case images.MediaTypeDockerSchema2Manifest, specs.MediaTypeImageManifest:
// TODO: handle distribution manifest and schema1
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
if err := imagetools.PrintManifestList(raw, descriptor, image, os.Stdout); err != nil {
if err := imagetools.PrintManifestList(raw, descriptor, image, streams.Out()); err != nil {
return err
}
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
if err := printManifest(raw, descriptor, image, os.Stdout); err != nil {
if err := printManifest(raw, descriptor, image, streams.Out()); err != nil {
return err
}
default:
fmt.Printf("%s\n", raw)
fmt.Fprintf(streams.Out(), "%s\n", raw)
}
return nil
@ -146,3 +149,23 @@ func printManifest(raw []byte, descriptor ocispec.Descriptor, image string, out
return w.Flush()
}
type authResolver struct {
authConfig clitypes.AuthConfig
}
func (a *authResolver) GetAuthConfig(registryHostname string) (clitypes.AuthConfig, error) {
return a.authConfig, nil
}
func convert(config types.AuthConfig) clitypes.AuthConfig {
return clitypes.AuthConfig{
Username: config.Username,
Password: config.Password,
Auth: config.Auth,
Email: config.Email,
ServerAddress: config.ServerAddress,
IdentityToken: config.IdentityToken,
RegistryToken: config.RegistryToken,
}
}

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

@ -17,10 +17,8 @@
package tag
import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"time"
@ -28,8 +26,6 @@ import (
"github.com/cli/cli/utils"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/go-units"
"github.com/spf13/cobra"
@ -120,7 +116,7 @@ type listOptions struct {
sort string
}
func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newListCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts listOptions
cmd := &cobra.Command{
Use: lsName + " [OPTION] REPOSITORY",
@ -130,7 +126,7 @@ func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
metrics.Send(parent, lsName)
},
RunE: func(_ *cobra.Command, args []string) error {
return runList(ctx, dockerCli, opts, args[0])
return runList(streams, hubClient, opts, args[0])
},
}
cmd.Flags().BoolVar(&opts.platforms, "platforms", false, "List all available platforms per tag")
@ -141,31 +137,25 @@ func newListCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobr
return cmd
}
func runList(ctx context.Context, dockerCli command.Cli, opts listOptions, repository string) error {
func runList(streams command.Streams, hubClient *hub.Client, opts listOptions, repository string) error {
ordering, err := mapOrdering(opts.sort)
if err != nil {
return err
}
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
var clientOps []hub.ClientOp
if opts.all {
clientOps = append(clientOps, hub.WithAllElements())
if err := hubClient.Apply(hub.WithAllElements()); err != nil {
return err
}
}
client, err := hub.NewClient(authResolver, clientOps...)
if err != nil {
return err
}
if err := promptCallToAction(dockerCli.Out(), client); err != nil {
fmt.Fprint(dockerCli.Err(), err)
if err := promptCallToAction(streams.Err(), hubClient); err != nil {
fmt.Fprint(streams.Err(), err)
}
var reqOps []hub.RequestOp
if ordering != "" {
reqOps = append(reqOps, hub.WithSortingOrder(ordering))
}
tags, err := client.GetTags(repository, reqOps...)
tags, err := hubClient.GetTags(repository, reqOps...)
if err != nil {
return err
}
@ -174,7 +164,7 @@ func runList(ctx context.Context, dockerCli command.Cli, opts listOptions, repos
defaultColumns = append(defaultColumns, platformColumn)
}
return opts.Print(os.Stdout, tags, printTags)
return opts.Print(streams.Out(), tags, printTags)
}
func printTags(out io.Writer, values interface{}) error {

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

@ -18,16 +18,12 @@ package tag
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/spf13/cobra"
"github.com/docker/hub-cli-plugin/internal/hub"
@ -42,7 +38,7 @@ type rmOptions struct {
force bool
}
func newRmCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.Command {
func newRmCmd(streams command.Streams, hubClient *hub.Client, parent string) *cobra.Command {
var opts rmOptions
cmd := &cobra.Command{
Use: rmName + " [OPTIONS] REPOSITORY:TAG",
@ -52,7 +48,7 @@ func newRmCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.
metrics.Send(parent, rmName)
},
RunE: func(_ *cobra.Command, args []string) error {
return runRm(ctx, dockerCli, opts, args[0])
return runRm(streams, hubClient, opts, args[0])
},
}
cmd.Flags().BoolVar(&opts.force, "force", false, "Force deletion of the tag")
@ -60,7 +56,7 @@ func newRmCmd(ctx context.Context, dockerCli command.Cli, parent string) *cobra.
return cmd
}
func runRm(ctx context.Context, dockerCli command.Cli, opts rmOptions, image string) error {
func runRm(streams command.Streams, hubClient *hub.Client, opts rmOptions, image string) error {
ref, err := reference.Parse(image)
if err != nil {
return err
@ -71,8 +67,8 @@ func runRm(ctx context.Context, dockerCli command.Cli, opts rmOptions, image str
}
if !opts.force {
fmt.Println("Please type the name of your repository to confirm deletion:", namedTaggedRef.Name())
reader := bufio.NewReader(os.Stdin)
fmt.Fprintln(streams.Out(), "Please type the name of your repository to confirm deletion:", namedTaggedRef.Name())
reader := bufio.NewReader(streams.In())
input, _ := reader.ReadString('\n')
input = strings.ToLower(strings.TrimSpace(input))
if input != namedTaggedRef.Name() {
@ -80,16 +76,9 @@ func runRm(ctx context.Context, dockerCli command.Cli, opts rmOptions, image str
}
}
authResolver := func(hub *registry.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, dockerCli, hub)
}
client, err := hub.NewClient(authResolver)
if err != nil {
if err := hubClient.RemoveTag(namedTaggedRef.Name(), namedTaggedRef.Tag()); err != nil {
return err
}
if err := client.RemoveTag(namedTaggedRef.Name(), namedTaggedRef.Tag()); err != nil {
return err
}
fmt.Println("Deleted", image)
fmt.Fprintln(streams.Out(), "Deleted", image)
return nil
}

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

@ -18,6 +18,7 @@ package hub
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
@ -39,6 +40,9 @@ const (
//Client sends authenticated calls to the Hub API
type Client struct {
AuthConfig types.AuthConfig
Ctx context.Context
domain string
token string
fetchAllElements bool
@ -58,22 +62,37 @@ type RequestOp func(r *http.Request) error
func NewClient(authResolver AuthResolver, ops ...ClientOp) (*Client, error) {
hubInstance := getInstance()
hubAuthConfig := authResolver(hubInstance.RegistryInfo)
token, err := login(hubInstance.APIHubBaseURL, hubAuthConfig)
if err != nil {
return nil, err
// Check if the user is logged in
if hubAuthConfig.Username == "" {
return nil, &authenticationError{}
}
client := &Client{
domain: hubInstance.APIHubBaseURL,
token: token,
AuthConfig: hubAuthConfig,
domain: hubInstance.APIHubBaseURL,
}
for _, op := range ops {
if err := op(client); err != nil {
return nil, err
}
}
token, err := client.login(hubInstance.APIHubBaseURL, hubAuthConfig)
if err != nil {
return nil, err
}
client.token = token
return client, nil
}
//Apply changes client behavior using ClientOp
func (c *Client) Apply(ops ...ClientOp) error {
for _, op := range ops {
if err := op(c); err != nil {
return err
}
}
return nil
}
//WithAllElements makes the client fetch all the elements it can find, enabling pagination.
func WithAllElements() ClientOp {
return func(c *Client) error {
@ -82,6 +101,14 @@ func WithAllElements() ClientOp {
}
}
//WithContext set the client context
func WithContext(ctx context.Context) ClientOp {
return func(c *Client) error {
c.Ctx = ctx
return nil
}
}
//WithHubToken sets the bearer token to the request
func WithHubToken(token string) RequestOp {
return func(req *http.Request) error {
@ -103,7 +130,7 @@ func WithSortingOrder(order string) RequestOp {
}
}
func login(hubBaseURL string, hubAuthConfig types.AuthConfig) (string, error) {
func (c *Client) login(hubBaseURL string, hubAuthConfig types.AuthConfig) (string, error) {
data, err := json.Marshal(hubAuthConfig)
if err != nil {
return "", err
@ -116,7 +143,7 @@ func login(hubBaseURL string, hubAuthConfig types.AuthConfig) (string, error) {
return "", err
}
req.Header["Content-Type"] = []string{"application/json"}
buf, err := doRequest(req)
buf, err := c.doRequest(req)
if err != nil {
return "", err
}
@ -130,7 +157,7 @@ func login(hubBaseURL string, hubAuthConfig types.AuthConfig) (string, error) {
return creds.Token, nil
}
func doRequest(req *http.Request, reqOps ...RequestOp) ([]byte, error) {
func (c *Client) doRequest(req *http.Request, reqOps ...RequestOp) ([]byte, error) {
req.Header["Accept"] = []string{"application/json"}
req.Header["User-Agent"] = []string{fmt.Sprintf("hub-tool/%s", internal.Version)}
for _, op := range reqOps {
@ -138,6 +165,9 @@ func doRequest(req *http.Request, reqOps ...RequestOp) ([]byte, error) {
return nil, err
}
}
if c.Ctx != nil {
req = req.WithContext(c.Ctx)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err

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

@ -34,6 +34,7 @@ func TestDoRequestAddsCustomUserAgent(t *testing.T) {
defer server.Close()
req, err := http.NewRequest("GET", server.URL, nil)
assert.NilError(t, err)
_, err = doRequest(req)
client := Client{}
_, err = client.doRequest(req)
assert.NilError(t, err)
}

46
internal/hub/error.go Normal file
Просмотреть файл

@ -0,0 +1,46 @@
/*
Copyright 2020 Docker Hub Tool authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package hub
import "fmt"
type authenticationError struct {
}
func (a authenticationError) Error() string {
return "authentication error"
}
// IsAuthenticationError check if the error type is an authentication error
func IsAuthenticationError(err error) bool {
_, ok := err.(*authenticationError)
return ok
}
type invalidTokenError struct {
token string
}
func (i invalidTokenError) Error() string {
return fmt.Sprintf("invalid authentication token %q", i.token)
}
// IsInvalidTokenError check if the error type is an invalid token error
func IsInvalidTokenError(err error) bool {
_, ok := err.(*invalidTokenError)
return ok
}

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

@ -0,0 +1,34 @@
/*
Copyright 2020 Docker Hub Tool authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package hub
import (
"errors"
"testing"
"gotest.tools/v3/assert"
)
func TestIsAuthenticationError(t *testing.T) {
assert.Assert(t, IsAuthenticationError(&authenticationError{}))
assert.Assert(t, !IsAuthenticationError(errors.New("")))
}
func TestIsInvalidTokenError(t *testing.T) {
assert.Assert(t, IsInvalidTokenError(&invalidTokenError{}))
assert.Assert(t, !IsInvalidTokenError(errors.New("")))
}

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

@ -72,7 +72,7 @@ func (c *Client) GetMembersPerTeam(organization, team string) ([]Member, error)
if err != nil {
return nil, err
}
response, err := doRequest(req, WithHubToken(c.token))
response, err := c.doRequest(req, WithHubToken(c.token))
if err != nil {
return nil, err
}
@ -88,7 +88,7 @@ func (c *Client) getMembersPage(url string) ([]Member, string, error) {
if err != nil {
return nil, "", err
}
response, err := doRequest(req, WithHubToken(c.token))
response, err := c.doRequest(req, WithHubToken(c.token))
if err != nil {
return nil, "", err
}

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

@ -71,7 +71,7 @@ func (c *Client) getOrganizationsPage(url string) ([]Organization, string, error
if err != nil {
return nil, "", err
}
response, err := doRequest(req, WithHubToken(c.token))
response, err := c.doRequest(req, WithHubToken(c.token))
if err != nil {
return nil, "", err
}

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

@ -60,7 +60,7 @@ func (c *Client) GetHubPlan(accountID string) (*Plan, error) {
if err != nil {
return nil, err
}
response, err := doRequest(req, WithHubToken(c.token))
response, err := c.doRequest(req, WithHubToken(c.token))
if err != nil {
return nil, err
}

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

@ -78,7 +78,7 @@ func (c *Client) RemoveRepository(repository string) error {
if err != nil {
return err
}
_, err = doRequest(req, WithHubToken(c.token))
_, err = c.doRequest(req, WithHubToken(c.token))
return err
}
@ -87,7 +87,7 @@ func (c *Client) getRepositoriesPage(url, account string) ([]Repository, string,
if err != nil {
return nil, "", err
}
response, err := doRequest(req, WithHubToken(c.token))
response, err := c.doRequest(req, WithHubToken(c.token))
if err != nil {
return nil, "", err
}

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

@ -98,7 +98,7 @@ func (c *Client) RemoveTag(repository, tag string) error {
if err != nil {
return err
}
_, err = doRequest(req, WithHubToken(c.token))
_, err = c.doRequest(req, WithHubToken(c.token))
return err
}
@ -107,7 +107,7 @@ func (c *Client) getTagsPage(url, repository string, reqOps ...RequestOp) ([]Tag
if err != nil {
return nil, "", err
}
response, err := doRequest(req, append(reqOps, WithHubToken(c.token))...)
response, err := c.doRequest(req, append(reqOps, WithHubToken(c.token))...)
if err != nil {
return nil, "", err
}

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

@ -68,7 +68,7 @@ func (c *Client) getTeamsPage(url, organization string) ([]Team, string, error)
if err != nil {
return nil, "", err
}
response, err := doRequest(req, WithHubToken(c.token))
response, err := c.doRequest(req, WithHubToken(c.token))
if err != nil {
return nil, "", err
}

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

@ -48,7 +48,7 @@ func (c *Client) GetUserInfo() (*User, error) {
if err != nil {
return nil, err
}
response, err := doRequest(req, WithHubToken(c.token))
response, err := c.doRequest(req, WithHubToken(c.token))
if err != nil {
return nil, err
}