Skip to content

Commit

Permalink
Add pull command
Browse files Browse the repository at this point in the history
  • Loading branch information
jpreese committed Jul 4, 2020
1 parent 477c43a commit f845712
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 289 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ _NOTE: The update command will ONLY update image **versions**. This allows for p

If desired, you can set a new target in the image manifest by using --target during an update.

### Pull command

Pulls the source images found in the image manifest. This is useful if you want to perform additional actions on the image(s) before performing a push operation (e.g. scanning for vulnerabilities)

```
$ sinker pull
```

### List command

Prints a list of either the `source` or `target` images that exist in the image manifest. This can be useful for piping into additional tooling that acts on image urls.
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ module github.com/plexsystems/sinker
go 1.14

require (
github.com/Microsoft/hcsshim v0.8.9 // indirect
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb // indirect
github.com/coreos/prometheus-operator v0.39.0
github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.4.2-0.20190916154449-92cc603036dd
github.com/genuinetools/reg v0.16.1
github.com/ghodss/yaml v1.0.0
github.com/google/go-cmp v0.4.0 // indirect
github.com/hashicorp/go-version v1.2.0
github.com/plexsystems/imagesync v0.4.0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.4.0
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71
Expand Down
151 changes: 1 addition & 150 deletions go.sum

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions internal/commands/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,19 @@ func (c ContainerImage) RepositoryWithTag() string {

// Source returns the source image
func (c ContainerImage) Source() string {
return c.SourceRegistry + "/" + c.RepositoryWithTag()
}
if c.SourceRegistry == "docker.io" && !strings.Contains(c.Repository, "/") {
return c.SourceRegistry + "/library/" + c.RepositoryWithTag()
}

// Target returns the target image
func (c ContainerImage) Target(target Target) string {
if c.SourceRegistry == "" {
return c.RepositoryWithTag()
}

return c.SourceRegistry + "/" + c.RepositoryWithTag()
}

// Target returns the target image
func (c ContainerImage) Target(target Target) string {
return target.String() + "/" + c.RepositoryWithTag()
}

Expand Down Expand Up @@ -167,6 +171,7 @@ func autoDetectSourceRegistry(repository string) string {
repositoryMappings := map[string]string{
"kubernetes-ingress-controller": "quay.io",
"coreos": "quay.io",
"open-policy-agent": "quay.io",
"twistlock": "registry.twistlock.com",
}

Expand Down
3 changes: 2 additions & 1 deletion internal/commands/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewDefaultCommand() *cobra.Command {
Use: path.Base(os.Args[0]),
Short: "sinker",
Long: "A CLI tool to sync container images to another registry",
Version: "0.5.0",
Version: "0.6.0",
}

ctx := context.Background()
Expand All @@ -24,6 +24,7 @@ func NewDefaultCommand() *cobra.Command {
cmd.AddCommand(newCreateCommand())
cmd.AddCommand(newUpdateCommand())
cmd.AddCommand(newListCommand())
cmd.AddCommand(newPullCommand(ctx, logger))
cmd.AddCommand(newPushCommand(ctx, logger))
cmd.AddCommand(newCheckCommand(ctx, logger))

Expand Down
186 changes: 186 additions & 0 deletions internal/commands/pull.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package commands

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"os"
"time"

"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/wait"
)

func newPullCommand(ctx context.Context, logger *log.Logger) *cobra.Command {
cmd := cobra.Command{
Use: "pull",
Short: "Pull the source images found in the image manifest",
Args: cobra.OnlyValidArgs,

RunE: func(cmd *cobra.Command, args []string) error {
if err := runPullCommand(ctx, logger); err != nil {
return fmt.Errorf("pull: %w", err)
}

return nil
},
}

return &cmd
}

func runPullCommand(ctx context.Context, logger *log.Logger) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("new docker client: %w", err)
}

manifest, err := getManifest()
if err != nil {
return fmt.Errorf("get manifest: %w", err)
}

if len(manifest.Images) == 0 {
return fmt.Errorf("no images found in manifest (%s)", manifestFileName)
}

if err := pullSourceImages(ctx, cli, logger, manifest.Images); err != nil {
return fmt.Errorf("pull source images: %w", err)
}

return nil
}

func getEncodedImageAuth(image ContainerImage) (string, error) {
username := os.Getenv(image.Auth.Username)
password := os.Getenv(image.Auth.Password)

authConfig := Auth{
Username: username,
Password: password,
}

jsonAuth, err := json.Marshal(authConfig)
if err != nil {
return "", fmt.Errorf("marshal auth: %w", err)
}

return base64.URLEncoding.EncodeToString(jsonAuth), nil
}

func getEncodedAuthForRegistry(registry string) (string, error) {
if registry == "" {
registry = "https://index.docker.io/v2/"
}

cfg, err := config.Load(config.Dir())
if err != nil {
return "", fmt.Errorf("loading docker config: %w", err)
}

if !cfg.ContainsAuth() {
cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore)
}

authConfig, err := cfg.GetAuthConfig(registry)
if err != nil {
return "", fmt.Errorf("getting auth config: %w", err)
}

jsonAuth, err := json.Marshal(authConfig)
if err != nil {
return "", fmt.Errorf("marshal auth: %w", err)
}

return base64.URLEncoding.EncodeToString(jsonAuth), nil
}

func imageExistsLocally(ctx context.Context, cli *client.Client, image ContainerImage) (bool, error) {
imageList, err := cli.ImageList(ctx, types.ImageListOptions{})
if err != nil {
return false, fmt.Errorf("image list: %w", err)
}

// When an image is sourced from docker hub, the image tag does
// not include docker.io on the local machine
if image.SourceRegistry == "docker.io" {
image.SourceRegistry = ""
}

for _, imageSummary := range imageList {
for _, localImage := range imageSummary.RepoTags {
if localImage == image.Source() {
return true, nil
}
}
}

return false, nil
}

func pullSourceImages(ctx context.Context, cli *client.Client, logger *log.Logger, images []ContainerImage) error {
for _, image := range images {
exists, err := imageExistsLocally(ctx, cli, image)
if err != nil {
return fmt.Errorf("checking local image: %w", err)
}

if exists {
logger.Printf("Image %s exists locally. Skipping ...", image.Source())
continue
}

if err := pullSourceImageAndWait(ctx, logger, cli, image); err != nil {
return fmt.Errorf("waiting for source image pull: %w", err)
}
}

return nil
}

func pullSourceImageAndWait(ctx context.Context, logger *log.Logger, cli *client.Client, image ContainerImage) error {
var encodedAuth string
var err error
if image.Auth.Password != "" {
encodedAuth, err = getEncodedImageAuth(image)
} else {
encodedAuth, err = getEncodedAuthForRegistry(image.SourceRegistry)
}
if err != nil {
return fmt.Errorf("get encoded auth: %w", err)
}

opts := types.ImagePullOptions{
RegistryAuth: encodedAuth,
}

reader, err := cli.ImagePull(ctx, image.Source(), opts)
if err != nil {
return fmt.Errorf("image pull: %w", err)
}

if err := waitForImagePulled(ctx, logger, cli, image); err != nil {
return fmt.Errorf("wait for source image pull: %w", err)
}
reader.Close()

return nil
}

func waitForImagePulled(ctx context.Context, logger *log.Logger, cli *client.Client, image ContainerImage) error {
return wait.PollImmediate(5*time.Second, 5*time.Minute, func() (bool, error) {
exists, err := imageExistsLocally(ctx, cli, image)
if err != nil {
return false, fmt.Errorf("checking local image: %w", err)
}

logger.Printf("Pulling %s ...\n", image.Source())
return exists, nil
})
}
Loading

0 comments on commit f845712

Please sign in to comment.