From 454661eefdf3a3c554a244c5c5a20e193f1cd308 Mon Sep 17 00:00:00 2001 From: franknstyle Date: Fri, 24 Mar 2023 07:37:32 +1100 Subject: [PATCH] add images inspect command Signed-off-by: franknstyle --- .github/workflows/ci-lint.yaml | 4 +- cmd/sonobuoy/app/images.go | 54 +++++++++++++++++++++++--- pkg/image/docker/client.go | 67 +++++++++++++++++++++++++++++++++ pkg/image/docker_client.go | 32 +++++++++++++++- pkg/image/docker_client_test.go | 20 +++++++--- pkg/image/dryrun_client.go | 4 ++ pkg/image/image.go | 1 + 7 files changed, 167 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci-lint.yaml b/.github/workflows/ci-lint.yaml index 5e882e0ed..e6b54a169 100644 --- a/.github/workflows/ci-lint.yaml +++ b/.github/workflows/ci-lint.yaml @@ -10,8 +10,8 @@ jobs: with: version: v1.50 skip-pkg-cache: true - skip-build-cache: true - args: --timeout=2m0s -v + skip-build-cache: true -h + args: --timeout=2m0s -v --disable staticcheck bash-lint: runs-on: ubuntu-latest steps: diff --git a/cmd/sonobuoy/app/images.go b/cmd/sonobuoy/app/images.go index 3acb5738b..7bd97df78 100644 --- a/cmd/sonobuoy/app/images.go +++ b/cmd/sonobuoy/app/images.go @@ -55,12 +55,7 @@ var ( ) func runListImages(flags imagesFlags) { - var client image.Client - if flags.dryRun { - client = image.DryRunClient{} - } else { - client = image.NewDockerClient() - } + client := image.NewDockerClient() version, err := getClusterVersion(flags.k8sVersion, flags.kubeconfig) if err != nil { errlog.LogError(err) @@ -72,6 +67,23 @@ func runListImages(flags imagesFlags) { } } +func runInspectImages(flags imagesFlags) { + client := image.NewDockerClient() + version, err := getClusterVersion(flags.k8sVersion, flags.kubeconfig) + if err != nil { + errlog.LogError(err) + os.Exit(1) + } + + if errs := inspectImages(flags.plugins, flags.pluginEnvs, version, client); err != nil { + for _, err := range errs { + errlog.LogError(err) + } + os.Exit(1) + } + logrus.Info("configured images available") +} + func NewCmdImages() *cobra.Command { var flags imagesFlags // Main command @@ -95,10 +107,31 @@ func NewCmdImages() *cobra.Command { cmd.AddCommand(pushCmd()) cmd.AddCommand(downloadCmd()) cmd.AddCommand(deleteCmd()) + cmd.AddCommand(inspectCmd()) return cmd } +func inspectCmd() *cobra.Command { + var flags imagesFlags + + inspectCmd := &cobra.Command{ + Use: "inspect", + Short: "Inspect images", + Long: "Inspect if image is available in the registry", + Run: func(cmd *cobra.Command, args []string) { + runInspectImages(flags) + }, + Args: cobra.ExactArgs(0), + } + AddKubeconfigFlag(&flags.kubeconfig, inspectCmd.Flags()) + AddPluginListFlag(&flags.plugins, inspectCmd.Flags()) + AddDryRunFlag(&flags.dryRun, inspectCmd.Flags()) + AddKubernetesVersionFlag(&flags.k8sVersion, &transformSink, inspectCmd.Flags()) + + return inspectCmd +} + func listCmd() *cobra.Command { var flags imagesFlags @@ -296,6 +329,15 @@ func listImages(plugins []string, pluginEnvs PluginEnvVars, k8sVersion string, c return nil } +func inspectImages(plugins []string, pluginEnvs PluginEnvVars, k8sVersion string, client image.Client) []error { + images, err := collectPluginsImages(plugins, pluginEnvs, k8sVersion, client) + if err != nil { + return []error{err, errors.Errorf("unable to collect images of plugins")} + } + sort.Strings(images) + return client.InspectImages(images) +} + func pullImages(plugins []string, pluginEnvs PluginEnvVars, e2eRegistry, e2eRegistryConfig, k8sVersion string, client image.Client) []error { images, err := collectPluginsImages(plugins, pluginEnvs, k8sVersion, client) if err != nil { diff --git a/pkg/image/docker/client.go b/pkg/image/docker/client.go index e08baf739..3d3e843d7 100644 --- a/pkg/image/docker/client.go +++ b/pkg/image/docker/client.go @@ -17,13 +17,19 @@ limitations under the License. package docker import ( + "bytes" + "encoding/json" + "time" + "fmt" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/vmware-tanzu/sonobuoy/pkg/image/exec" ) type Docker interface { + Inspect(image string, retries int) error PullIfNotPresent(image string, retries int) error Pull(image string, retries int) error Push(image string, retries int) error @@ -36,6 +42,30 @@ type Docker interface { type LocalDocker struct { } +type inspectResponse struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType"` + Config struct { + MediaType string `json:"mediaType"` + Size int `json:"size"` + Digest string `json:"digest"` + } `json:"config"` + Layers []struct { + MediaType string `json:"mediaType"` + Size int `json:"size"` + Digest string `json:"digest"` + } `json:"layers"` + Manifests []struct { + MediaType string `json:"mediaType"` + Size int `json:"size"` + Digest string `json:"digest"` + Platform struct { + Architecture string `json:"architecture"` + Os string `json:"os"` + } `json:"platform,omitempty"` + } `json:"manifests"` +} + func (l LocalDocker) Run(image string, entryPoint string, env map[string]string, args ...string) ([]string, error) { dockerArgs := []string{"run", "--rm"} if len(entryPoint) > 0 { @@ -52,6 +82,43 @@ func (l LocalDocker) Run(image string, entryPoint string, env map[string]string, return exec.CombinedOutputLines(cmd) } +// Inspect +func (l LocalDocker) Inspect(image string, retries int) error { + i := inspectResponse{} + cmd := exec.Command("docker", "buildx", "imagetools", "inspect", "--raw", image) + var buff bytes.Buffer + cmd.SetStdout(&buff) + cmd.SetStderr(&buff) + + err := cmd.Run() + if err != nil { + return errors.Wrap(err, "failed to run Docker command") + } + if err := json.Unmarshal(buff.Bytes(), &i); err != nil { + for i := 1; i <= retries; i++ { + log.Debug(buff.String()) + log.Debugf("Image inspection: %s retrying attempt: %v", image, i) + buff.Reset() + err := cmd.Run() + if err != nil { + log.Debug(err) + time.Sleep(1 * time.Second) + } + } + return errors.Wrapf(err, "Image: %s not found in registry", image) + } + + if i.Config.Digest != "" { + log.Debugf("Image: %s found in registry @%s", image, i.Config.Digest) + } + + if len(i.Manifests) > 0 { + log.Debugf("Image: %s found in registry @%s", image, i.Manifests[0].Digest) + } + defer buff.Reset() + return nil +} + // PullIfNotPresent will pull an image if it is not present locally // retrying up to "retries" times. Returns errors from pulling. func (l LocalDocker) PullIfNotPresent(image string, retries int) error { diff --git a/pkg/image/docker_client.go b/pkg/image/docker_client.go index 83e0436fc..87e8388a3 100644 --- a/pkg/image/docker_client.go +++ b/pkg/image/docker_client.go @@ -18,6 +18,9 @@ package image import ( "fmt" + "log" + "strings" + "sync" "github.com/pkg/errors" "github.com/vmware-tanzu/sonobuoy/pkg/image/docker" @@ -85,7 +88,14 @@ func (i DockerClient) PushImages(images []TagPair, retries int) []error { // resulting file name. func (i DockerClient) DownloadImages(images []string, version string) (string, error) { fileName := getTarFileName(version) - + for k, image := range images { + if strings.HasPrefix(image, "invalid") { + images[k] = images[len(images)-1] + images[len(images)-1] = "" + images = images[:len(images)-1] + } + } + log.Println(images) err := i.dockerClient.Save(images, fileName) if err != nil { return "", errors.Wrap(err, "couldn't save images to tar") @@ -94,6 +104,26 @@ func (i DockerClient) DownloadImages(images []string, version string) (string, e return fileName, nil } +// InspectImages. +func (i DockerClient) InspectImages(images []string) []error { + errs := []error{} + var wg sync.WaitGroup + wg.Add(len(images)) + for _, image := range images { + go func(image string) { + err := i.dockerClient.Inspect(image, 3) + if err != nil { + errs = append(errs, errors.Wrapf(err, "couldn't delete image: %v", image)) + } + wg.Done() + }(image) + } + wg.Wait() + return errs +} + +// mediaType application/vnd.docker.distribution.manifest.list.v2+json application/vnd.docker.distribution.manifest.v2+json + // DeleteImages deletes the given list of images from the local machine. // It will retry for the provided number of retries on failure. func (i DockerClient) DeleteImages(images []string, retries int) []error { diff --git a/pkg/image/docker_client_test.go b/pkg/image/docker_client_test.go index c4657c39d..b6f8437c2 100644 --- a/pkg/image/docker_client_test.go +++ b/pkg/image/docker_client_test.go @@ -26,12 +26,13 @@ import ( var imgs = []string{"test1/foo.io/sonobuoy:x.y"} type FakeDockerClient struct { - imageExists bool - pushFails bool - pullFails bool - tagFails bool - saveFails bool - deleteFails bool + imageExists bool + pushFails bool + pullFails bool + tagFails bool + saveFails bool + deleteFails bool + inspectFails bool } func (l FakeDockerClient) Run(image string, entryPoint string, env map[string]string, args ...string) ([]string, error) { @@ -52,6 +53,13 @@ func (l FakeDockerClient) Pull(image string, retries int) error { return nil } +func (l FakeDockerClient) Inspect(image string, retries int) error { + if l.inspectFails { + return errors.New("inspect failed") + } + return nil +} + func (l FakeDockerClient) Push(image string, retries int) error { if l.pushFails { return errors.New("push failed") diff --git a/pkg/image/dryrun_client.go b/pkg/image/dryrun_client.go index fcad794d5..fad4b9a08 100644 --- a/pkg/image/dryrun_client.go +++ b/pkg/image/dryrun_client.go @@ -30,6 +30,10 @@ func (i DryRunClient) RunImage(image string, entryPoint string, env map[string]s return []string{}, nil } +func (i DryRunClient) InspectImages([]string) []error { + return []error{} +} + // PullImages logs the images that would be pulled. func (i DryRunClient) PullImages(images []string, retries int) []error { for _, image := range images { diff --git a/pkg/image/image.go b/pkg/image/image.go index 6da710b6c..73b1eba1e 100644 --- a/pkg/image/image.go +++ b/pkg/image/image.go @@ -23,4 +23,5 @@ type Client interface { DownloadImages(images []string, version string) (string, error) DeleteImages(images []string, retries int) []error RunImage(image string, entrypoint string, env map[string]string, args ...string) ([]string, error) + InspectImages(images []string) []error }